WORK-587
ID:WORK-587Status:done

Anchor resolution, the auto extent, and the corpus test

The core of SPEC-131 phase 1. After this item, snippet and file-ref can address a region by name instead of by coordinate, and a symbol that moves fails loudly instead of rendering plausible wrong code.

One change, two consumers: resolution lands in read-file.ts behind the existing ReadFileOptions shape, and both call sites (snippet-pipeline.ts:174, file-ref-resolve.ts:209) gain the capability by passing the new fields through.

Two, not three — expand is not a consumer of this. The spec's own Proposal says so and its acceptance criteria said otherwise; the criteria were wrong. expand-pipeline.ts:347 calls readWholeSandboxedFile, a different function that returns raw text straight into Markdoc.parse with no slicing and no [start, end]. expand embeds a document rather than a region, has no lines= and therefore none of the exposure this item removes. Do not wire it up; extending it to partial includes is a separate feature.

Priority:highComplexity:complexMilestone:v0.37.0Source:SPEC-131
claude/v0-37-0-review-vqpl41 View source

Criteria completion

Criteria completion: 22 of 22 (100%) checked; history from Sep 22 to Sep 240%25%50%75%100%Sep 22Sep 24
Branches 4
History 7
  1. 16cc31b
    Content editedby bjornolofandersson
  2. bad92a2
    • ☑ `readSnippetFile` resolves `symbol` and `match` anchors in addition to `lines`, and both `snippet` and `file-ref` gain the capability from that one change
    • ☑ `expand` is unchanged and gains no anchor attributes
    • ☑ `symbol` builds its anchor from the keyword table; `match` accepts a raw regex; the two are mutually exclusive with each other and with `lines`
    • ☑ Anchors match against raw source; a match landing inside a masked region is skipped, covered by tests for `match='"scripts"'` on `package.json` (must resolve) and an anchor mentioned only in a comment (must be skipped)
    • ☑ The extent terminates only on a `;` at anchor depth or a `}` closing a brace block opened at anchor depth; parens and brackets nest without terminating
    • ☑ Depth is measured relative to the anchor line, covered by a test anchoring on a nested target (a `package.json` key, a class method, a rule inside `@media`)
    • ☑ Brace-less statements use continuation lookahead that skips blank and comment lines
    • ☑ `type` aliases do not terminate on a brace
    • ☑ An `auto` extent reaching EOF without terminating refuses and names `extent="dedent"`
    • ☑ Under `extent="auto"`, an extracted slice that is not delimiter-balanced is refused, never rendered
    • ☑ An anchor matching more than once takes the first and warns, naming every matching line
    • ☑ `occurrence` selects the Nth match, 1-based, defaulting to 1, reading from the same enumeration the warning names
    • ☑ `occurrence` does not suppress the ambiguity warning
    • ☑ An `occurrence` beyond the number of matches refuses, naming how many were found and where — never clamping to the last or falling back to the first
    • ☑ `occurrence` is documented beside `lines` / `until` / `through` as an escape hatch, not beside `symbol` as a naming form
    • ☑ Annotations (`@Component`, `#[derive]`, `@dataclass`) attach to the symbol always, independent of `doc`
    • ☑ `doc` is tri-state: unset defaults to on for `symbol` and off for `match`; `doc=true` and `doc=false` force the choice
    • ☑ A test covers the abutting-comment limit (D9) so the behaviour is pinned rather than accidental, including the file-header variant
    • ☑ Every refusal names the file, the anchor, the reason, and the `until=` / `through=` / `lines=` fallback
    • ☑ Failures use the existing error-fence path and emit a `ctx.error` diagnostic — no new failure channel
    • ☑ A regression corpus test scores the resolver against the repo's own sources and asserts the silent-wrong count stays at or below its recorded baseline
    • ☑ The engine is proven delimiter-agnostic by a test extracting a CSS rule via `match=`
    by bjornolofandersson
  3. aebbae3
    Created (ready)by bjornolofandersson
  4. 81a138f
    Content editedby Claude
    docs(plan): renumber the masker item to WORK-597, resolving the collisio
  5. c680110
    Content editedby Claude
    docs(plan): correct the expand scope error and soften WORK-596's blocker
  6. e60f670
    Content editedby Claude
    docs(plan): add SPEC-131 D17 — occurrence= for ambiguous anchors
  7. d40fe65
    Content editedby Claude
    docs(plan): break the documentation-drift specs into v0.37.0

The three termination rules are not guessable

Each cost an iteration of the spec's prototype to find, and re-deriving them from first principles will cost more than reading them off the spec:

  • Only ; at anchor depth, or } closing a brace block opened at anchor depth, terminates. Parens and brackets nest but never end a statement. Worth 38 percentage points on its own.
  • A brace-less statement ends at the first line back at anchor depth that is not a continuation — looking ahead past blank and comment lines, treating a next line starting | & . ? : , ) ] } => extends as a continuation.
  • type aliases never terminate on a brace. A type-level object literal is not a body.

Two corrections the spec makes to its own prototype

Both are live failures, not hypotheticals, and both are easy to reimplement wrongly:

Depth is relative to the anchor, not to the file. The prototype counted from file start, indistinguishable from anchor-relative for the top-level symbols it was measured on. packages/lumina/styles/runes/hint.css is the proof: the whole file is wrapped in @layer skin {, so .rf-hint sits at depth 1 and absolute counting returns the entire file instead of the rule's own four lines.

Anchors match raw source; the mask only rejects. Matching against masked text blanks every string literal, so match='"scripts"' against package.json could never match. The mask is still needed in the other direction — an anchor matching inside a masked region is a false positive and must be skipped.

Reaching EOF is a refusal, never a return

{% snippet path="classes.py" symbol="HttpClient" /%} is the most natural invocation a Python author can write: symbol resolves it, and then auto looks for a ; or } that does not exist anywhere in the file. Returning the remainder is precisely the plausible-wrong render this spec exists to prevent. The refusal must name extent="dedent".

The corpus test ships here

SPEC-131 is explicit that this is "part of this phase, not a follow-up": it is the only thing that will catch a regression in a heuristic, and it is cheap because the repo is its own corpus. Record the baseline counts so a change trading loud-fail for silent-wrong cannot pass unnoticed.

The published baseline, against 1,360 symbols: 95.37% exact, 4.56% loud refusal, 0.07% silent-wrong, 0% false alarm.

Acceptance Criteria

  • readSnippetFile resolves symbol and match anchors in addition to lines, and both snippet and file-ref gain the capability from that one change
  • expand is unchanged and gains no anchor attributes
  • symbol builds its anchor from the keyword table; match accepts a raw regex; the two are mutually exclusive with each other and with lines
  • Anchors match against raw source; a match landing inside a masked region is skipped, covered by tests for match='"scripts"' on package.json (must resolve) and an anchor mentioned only in a comment (must be skipped)
  • The extent terminates only on a ; at anchor depth or a } closing a brace block opened at anchor depth; parens and brackets nest without terminating
  • Depth is measured relative to the anchor line, covered by a test anchoring on a nested target (a package.json key, a class method, a rule inside @media)
  • Brace-less statements use continuation lookahead that skips blank and comment lines
  • type aliases do not terminate on a brace
  • An auto extent reaching EOF without terminating refuses and names extent="dedent"
  • Under extent="auto", an extracted slice that is not delimiter-balanced is refused, never rendered
  • An anchor matching more than once takes the first and warns, naming every matching line
  • occurrence selects the Nth match, 1-based, defaulting to 1, reading from the same enumeration the warning names
  • occurrence does not suppress the ambiguity warning
  • An occurrence beyond the number of matches refuses, naming how many were found and where — never clamping to the last or falling back to the first
  • occurrence is documented beside lines / until / through as an escape hatch, not beside symbol as a naming form
  • Annotations (@Component, #[derive], @dataclass) attach to the symbol always, independent of doc
  • doc is tri-state: unset defaults to on for symbol and off for match; doc=true and doc=false force the choice
  • A test covers the abutting-comment limit (D9) so the behaviour is pinned rather than accidental, including the file-header variant
  • Every refusal names the file, the anchor, the reason, and the until= / through= / lines= fallback
  • Failures use the existing error-fence path and emit a ctx.error diagnostic — no new failure channel
  • A regression corpus test scores the resolver against the repo's own sources and asserts the silent-wrong count stays at or below its recorded baseline
  • The engine is proven delimiter-agnostic by a test extracting a CSS rule via match=

Approach

The anchor is an afternoon. The termination rules and the corpus test are the work.

Build the corpus test early rather than last — it is the instrument that tells you whether each termination rule helped, and the spec's percentages are only reproducible with it in place.

{name} interpolation into the symbol anchor template must be escaped: a symbol name containing . or ( spliced raw into a regex is injection from content.

Enumerate matches once and let both features read it. D14's warning has to name every matching line, so the full match list exists already; occurrence= (D17) is an index into it. Written as two passes — first-match-and-warn, then a separate scan for the Nth — they drift apart, and the warning stops agreeing with the selection.

Blocked by

  • WORK-597 — the masker and the table this reads

Notes

D6's guarantee rests on the error fence, not the diagnostic. WORK-554 established that a ctx.error fails nothing; WORK-575 has since made it visible in dev, but not fatal. Emit the diagnostic as specified — just do not write anything here as though it gates a build. WORK-573 owns that question.

Non-goals worth restating so they do not creep in: no cross-file resolution, no type-aware resolution, no rename tracking, and lines= is not being replaced.

occurrence= is ordinal addressing and must be documented as such. It is a better coordinate than a line number — inserting unrelated content above does not move it — but it still drifts when another match is inserted above the one being addressed. D17 classifies it with D4's escape hatches deliberately; presenting it as an equal of symbol= would undersell that. The docs steer at a distinguishing regex first, and reach for the ordinal only when the anchors are genuinely identical.

The case that motivated it: site/content/index.md carries four {% feature %} blocks, and WORK-588's paired is what makes them quotable at all — so the item that unlocks quoting rune blocks is the one that creates the ambiguity.

References

  • SPEC-131 — steps 2–3 and 7–8, D1 (regex not parser), D2 (self-check scoped to auto), D3 (match is the contract), D4 (never a dead end), D6 (the error-fence path), D9 (doc defaults and the abutting-comment limit), D14 (ambiguous anchors warn), D17 (occurrence=, and why it is an escape hatch)
  • WORK-597 — the masker and language table
  • WORK-588 — the extents that make non-brace formats addressable
  • packages/runes/src/lib/read-file.ts — readSnippetFile, where resolution lands
  • packages/runes/src/snippet-pipeline.ts, packages/runes/src/file-ref-resolve.ts — the two call sites
  • packages/lumina/styles/runes/hint.css — the anchor-relative depth proof

Resolution

Completed: 2026-09-24

Branch: claude/v0-37-0-review-vqpl41 PR: refrakt-md/refrakt#647 (batched with WORK-588 and WORK-589)

What was done

  • packages/runes/src/lib/anchor.ts (new) — resolveAnchor(). Anchor enumeration, the three termination rules, anchor-relative depth, the self-check, the head, and every refusal message.
  • packages/runes/src/lib/read-file.ts — readSnippetFile takes an anchor option and returns start/end/anchored. One change, both call sites.
  • packages/runes/src/tags/snippet.ts, tags/file-ref.ts — the attribute surface.
  • packages/runes/src/lang-map.ts — added .py, .rs, .go, .mts, .cts.
  • Tests: anchor.test.ts (70), anchor-corpus.test.ts (4), snippet-anchor-pipeline.test.ts (13).

Notes

  • Batched with WORK-588 deliberately. This item's criteria require every refusal to name until= / through=, which WORK-588 implements. Split, this would ship error messages pointing at attributes that do not exist, which D4 calls worse than no fallback at all.
  • Two masks, and this was not in the plan. The spec says an anchor inside "a masked region" is a false positive. Implemented literally that makes match='"scripts"' unresolvable, because in JSON every key is a string — and that exact case is one of this item's criteria. The resolution: the depth counter reads the full mask, the anchor step reads a comment-only mask (maskComments). Commentary is what must be rejected; literals are legitimate things to name.
  • .py was missing from LANG_MAP, so Python's table entry was unreachable by path and a symbol= anchor refused with "declares no keywords" — the wrong reason. The unit tests missed it because they pass table entries directly; the end-to-end test caught it. There is now a guard asserting every table language is reachable by extension.
  • The corpus numbers are not comparable to the spec's. 99.23% exact here vs 95.37% in SPEC-131, because this oracle only checks the start and the balance where the spec used the TypeScript compiler. Higher and weaker. The assertion that matters is silentWrong === 0; raising that number is a decision about the spec's central guarantee, not a test fix.
  • A JSON key with a scalar value has nothing for auto to balance and refuses. Pinned by a test, with until= shown as the way through.

Verification

4820 tests pass, 0 failures. Typecheck, format:check, biome lint clean. Contracts up to date; refrakt validate --site main 0 errors, 0 warnings.