diff --git a/.agents/guides/codebase-patterns.md b/.agents/guides/codebase-patterns.md new file mode 100644 index 0000000..a5548cf --- /dev/null +++ b/.agents/guides/codebase-patterns.md @@ -0,0 +1,144 @@ +# Codebase Patterns — `mod.ts` + +Reference for the key architecture, data flow, and internal patterns in +`mod.ts`. Read this before making any non-trivial change. + +## Two processing paths + +``` +Input + │ + ├─ Tagged template (undent`...`) + │ │ + │ ├─ TSA cache hit? ──yes──▶ reuse stripped segments + │ │ │ + │ │ no + │ │ ▼ + │ │ compute column offsets on static segments only + │ │ strip common indent from segments + │ │ cache result keyed on TSA (WeakMap) + │ │ │ + │ └─────── ▼ + │ join segments + interpolated values + │ apply align/embed padding + │ trim wrapper blank lines + │ return string + │ + └─ Plain string (undent.string(s) / dedentString(s)) + │ + ▼ + splitLines → find min column offset → strip → rejoinLines + trim wrapper blank lines per TrimMode + return string +``` + +Key distinction: **interpolated values are never stripped** — only the +static template segments are processed. Values pass through as-is unless +wrapped with `align()` or `embed()`. + +## TSA WeakMap cache + +`TemplateStringsArray` is a frozen object created once per call site. The same +literal always produces the same TSA object, so a `WeakMap` keyed on it acts as +per-call-site memoization with zero string identity cost. The cache stores +already-stripped static segments; repeated calls (e.g. inside a hot loop) only +pay for joining and value interpolation, not the indent-detection pass. + +**LICM risk**: the JIT can hoist cache hits out of loops entirely, making +benchmarks misleadingly fast. Use mitata's computed parameters to prevent this. +See `benchmarking.instructions.md`. + +## `indent` symbol — explicit baseline + +When `${undent.indent}` (or `${indent}`) appears as the first interpolation on +its own line, that line's column position becomes the dedent baseline instead of +auto-detecting from all content lines. Content at the same column becomes column +0; deeper content retains relative spacing. + +## `align` and `embed` — multi-line value helpers + +Both return an `AlignedValue` — a branded object with `[ALIGNED]: true` +and a `value: string` field. + +``` +align(v) ─▶ { [ALIGNED]: true, value: String(v) } + ↑ no stripping + +embed(v) ─▶ dedentString(v) + │ + ▼ + { [ALIGNED]: true, value: stripped } + ↑ own indent removed first +``` + +At join time, `undent` checks `[ALIGNED]` and pads lines 2…N of the +value with the insertion column's worth of whitespace. + +`embed` also has a bounded LRU-style cache (`EMBED_CACHE`, max 256 +entries) for repeated static snippets. + +## `AlignedValue` per-value text cache + +Each `AlignedValue` carries a small bounded cache stored as a non-enumerable +symbol property. It maps column positions to already-padded strings, so the +same value used repeatedly at the same insertion column avoids re-padding on +every call. Check `mod.ts` for the current cap. + +## Character code constants + +Hot scanning loops use integer character codes instead of string methods: + +| Constant | Hex | Character | +| ----------- | ------ | --------- | +| `CC_TAB` | `0x09` | `\t` | +| `CC_LF` | `0x0a` | `\n` | +| `CC_CR` | `0x0d` | `\r` | +| `CC_SPACE` | `0x20` | ` ` | + +`string.charCodeAt(i)` is used instead of `string[i]` comparisons. + +## `splitLines` / `rejoinLines` + +- `splitLines(s)` splits on `\n`, `\r\n`, and bare `\r`, returning each + segment with its trailing newline sequence attached. +- `rejoinLines(...segments)` concatenates them back — `splitLines` → + `rejoinLines` is a guaranteed lossless roundtrip for any input. +- These are the canonical way to iterate lines without losing newline + sequences. + +## `columnOffset` + +Returns the number of leading whitespace characters (spaces + tabs) before +the first non-whitespace character on a line. Returns `Infinity` for +blank/whitespace-only lines (so they don't pull the minimum indent down). + +## `TrimMode` and `TrimSides` + +``` +"all" — strip every leading/trailing blank line +"one" — strip at most one blank line per edge +"none" — leave edges untouched +``` + +Can be set independently per side via `TrimSides`. The template engine +applies trimming after joining, not during segment processing. + +## `DEFAULTS` and `resolveOptions` + +`DEFAULTS` is the exported `ResolvedOptions` object with every field set to its +sensible out-of-the-box value. Check `mod.ts` for the current defaults — they +grow as new options are added. + +`resolveOptions(options, base?)` merges user `UndentOptions` onto a base +(defaulting to `DEFAULTS`), normalizing the `trim` shorthand into +`trimLeading`/`trimTrailing` fields. + +## `createUndent` + +The factory that builds a bound `Undent` object from a `ResolvedOptions`. +`undent`, `dedent`, and `outdent` are all instances created with different +defaults via `createUndent`. When adding configuration-dependent behaviour, +add it here. + +For the current full public API, run `deno doc mod.ts` or read the exports at +the top of `mod.ts` directly — duplicating that list here would only drift. diff --git a/.agents/memory/INDEX.md b/.agents/memory/INDEX.md index 664493c..ca3eeac 100644 --- a/.agents/memory/INDEX.md +++ b/.agents/memory/INDEX.md @@ -17,3 +17,7 @@ - [DECISIONS](DECISIONS/) - [CHECKLISTS](CHECKLISTS/) + +## Reference guides + +- [codebase-patterns](../../.agents/guides/codebase-patterns.md) — architecture, cache, processing paths in `mod.ts` diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 21ef250..963702e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -13,12 +13,8 @@ entire public API lives in `mod.ts` — there is no separate build step and no other source files to edit. It does one thing: strip source-code indentation from template literals and -strings. The exports fall into three groups: - -- **Tag functions** — `undent`, `dedent`, `outdent`, `createUndent` -- **Value helpers** — `align`, `embed`, `isAligned` -- **String/text utilities** — `dedentString`, `alignText`, `splitLines`, - `rejoinLines`, `columnOffset`, `newlineLengthAt`, `resolveOptions`, `DEFAULTS` +strings. The full public API is in `mod.ts` — run `deno doc mod.ts` to see +all exports. ## Commands @@ -88,14 +84,6 @@ For complex logic, include: - Prefer explicit configuration when it materially changes behavior. - Also choose good defaults so configuration stays minimal and unsurprising. -### Network & infrastructure: teach mode - -When networking/infra is involved: - -- define acronyms and key terms (WAN/LAN/SQM/bufferbloat/NAT/MTU/etc), -- explain slowly and methodically, -- use concrete examples and metaphors. - ## Breaking changes When making a behavioral change, touch all four of these before closing the @@ -134,9 +122,12 @@ When acting as an agent on multi-step work: - Do not store secrets, tokens, or private URLs in `.agents/memory/` - Keep scratch notes in `.agents/memory/SESSIONS/` (gitignored) -## Where to look for more targeted rules +## Where to look + +### Instructions (always-on rules, auto-loaded by `applyTo`) -Targeted rules live under `.github/instructions/`: +Targeted rules live under `.github/instructions/`. These are prescriptive — +follow them whenever you work on a matching file. | File | Applies to | | ----------------------------------- | -------------------------------- | @@ -148,3 +139,13 @@ Targeted rules live under `.github/instructions/`: | `changelog-commits.instructions.md` | `**` (all files) | | `pull-requests.instructions.md` | `**` (all files) | | `code-review.instructions.md` | `**` (all files) | + +### Guides (situational reference, read on demand) + +Reference material lives under `.agents/guides/`. These are descriptive — +read them when the task calls for it, not necessarily on every edit. + +| File | When to read | +| ----------------------- | ----------------------------------------------------- | +| `codebase-patterns.md` | Before touching `mod.ts` — architecture, cache, paths | +| `code-review.instructions.md` | `**` (all files) |