ADR020 MarkerPosition
Title: Marker position
- Status: Accepted
- Decided: D2026-09
- Affects:
Receipts
Context
A receipt marker is @sprig followed by a dotted identifier, written in a
comment. The scanner treated the tag as a marker anywhere it appeared in that
comment. The exclusion it did have — a lookahead that skipped @sprigfoo —
was never designed. It excluded letters, digits and underscore because those
were the case somebody thought of. It did not exclude a hyphen, a backtick, a
slash, or a newline.
That gap met a package whose documentation is largely sentences containing the
tag. sprig observe stopped working on its own repository. Five comment lines
that mentioned @sprig — an npm scope inside a JSDoc import() type, the tag
named inside backticks, a sentence that wrapped after the word — became hard
errors and exited 1. A sixth, packages/cli/src/receipts.ts:57, said "for
@sprig doc-comments" and was silently accepted as a receipt for a concept
named doc-comments.
Four of the five errors were inside packages/receipts itself. The scanner
could not survive reading its own documentation.
This is not the same question 009
answered. That record rules on how the field guide renders receipts it is
given. The connection worth naming is that 009's own evidence is the
twenty-four receipts in packages/prose-parser/src/errors.js, and those are
the same twenty-four this rule has to leave untouched.
Decision
A marker begins its own line. After comment decoration is stripped, @sprig
must be the first thing on the line (leading whitespace allowed), followed by
a space or tab and a dotted identifier. Anywhere else in a line, @sprig is
prose and is not a marker.
This is the JSDoc block-tag convention that @param and @returns already
follow, in exactly the files this scanner reads.
Consequences
What this makes easy. Writing about @sprig is ordinary prose. An import
from the @sprig-and-prose scope in a JSDoc type is not a marker. The
scanner can read its own comments. sprig observe works on this repository.
What this costs. A tag indented under a Markdown list item inside a doc comment is no longer a marker. A JSDoc block whose example shows a marker at line start still registers it, and there is no way to write that example inertly. The malformed-target error survives, but now fires only on a deliberate line-initial tag, which is narrower than before.
One-time id churn. A block that mentioned @sprig above its real marker
used to record the decoy's line and column. Those fields feed the
content-addressed receipt id, so those ids change once, to the correct value.
Markers that were already valid and already on their own line keep their ids.
What to watch. The identifier grammar inside the target is still hand-duplicated from the prose-parser scanner. That is a consistency problem, and it is a different problem.
Alternatives considered
Widen the TAG_RE lookahead to exclude -, backtick, /, : and
newline. Rejected. It fixes five of six cases and leaves the sixth — the
one that writes a wrong receipt rather than an error — completely untouched,
because that line is lexically a valid marker. It also grows a list that has
to be guessed at correctly forever, and every future punctuation mark somebody
writes next to the word is a new bug of the same kind.
An escape syntax, \@sprig or @@sprig. Rejected. It puts the burden on
every person who writes a sentence containing the word, in a package whose
documentation is largely sentences containing the word, and it would have
required editing five existing comments in this repo to appease the tool. The
line rule needs no escape because prose already never starts a line with the
bare tag.
Ignore any tag that sits inside backticks or an import() type. Rejected.
It means teaching the comment scanner about Markdown inline code and
TypeScript type syntax, inside comments, per language. That is a parser for
the host language, which the comment scanner is explicit about not being, and
it still misses the unquoted doc-comments case.
Evidence
Across 325 scannable files in this repository there were 32 lines containing
an @sprig tag. The line rule classified all 32 correctly, with no
exceptions: the six prose lines above became invisible, and the 26 real
markers — 24 in packages/prose-parser/src/errors.js plus the two canonical
fixtures — were untouched.
The result that contradicted the obvious approach is the doc-comments case.
Widening the lookahead cannot fix it, because "for @sprig doc-comments" is
lexically a valid marker. The two strings are identical. Something other than
the characters around the tag has to decide.
Receipt ids were measured before and after, keyed by
file:line:column:target. 26 of 27 came back byte-identical. The 27th was
the doc-comments receipt, and it disappeared.