R-292: share where a tag starts, not what it says

c0 asked whether tagsIn should return indices so beatLikeIn could take spans from
it. No -- that widens this file's signature to serve another, and R-291 already
fails when the two disagree. But detection is not prevention, and there is a third
thing to share.

The two patterns differ in their bodies for good reasons: tagsIn captures the whole
tag for skillsIn to split, outcome-coverage stops at the em-dash because the
outcome follows. What was copied into both files, and what drifted, is the opening
-- the literal \[CUS:, widened by R-290 here and left behind there. TAG_OPEN is now
exported as a string with tagRe(body) composing a fresh matcher onto it; fresh
because a shared /g regex carries lastIndex between callers.

22 tags before and after, c0's body composed onto TAG_OPEN gives the same 22 its
own pattern does, 19 guards green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
slaguru666
2026-09-13 15:55:02 +01:00
co-authored by Claude Opus 5
parent 8a051f7f2b
commit 3a31e0c9b0
2 changed files with 42 additions and 1 deletions
+28
View File
@@ -6866,3 +6866,31 @@ silence — the build stops, which is the safe direction — but the message nam
thing, and the fix is the one check-rollable already made for the cast: read through the other
guard's function rather than a second regex. That file is c0's and the call is theirs; it is
recorded here so it is a decision rather than something nobody wrote down.
## R-292 — share where a tag starts, not what it says
c0 asked whether `tagsIn` should return indices so `beatLikeIn` could take its spans from it,
as check-rollable does with `castMarkersIn`. No — and c0 had already reasoned its way to the
same answer. Returning indices widens this file's signature to serve another file, and
R-291's cross-reader test already fails the moment the two disagree.
But detection is not prevention, and there is a third thing to share that neither of us
proposed. The two patterns **differ in their bodies for good reasons**: `tagsIn` captures the
whole tag and lets `skillsIn` split it, `outcome-coverage` stops at the em-dash because the
outcome text follows. Sharing the whole pattern would be wrong. What was copied into both
files, and what actually drifted, is the **opening** — the literal `\[CUS:`, widened here by
R-290 and left behind there, after which three spellings had one guard reading a tag and the
other calling it unparseable.
So `TAG_OPEN` is exported as a string and `tagRe(body)` composes a fresh matcher onto it —
fresh because a shared `/g` regex carries `lastIndex` between callers, which is its own quiet
defect. Each reader keeps its own body. The half that must agree is the only half shared.
Verified nothing moved: 22 tags before and after, c0's body composed onto `TAG_OPEN` gives the
same 22 its own pattern does, the four tolerated spellings still read, `[CUS Spot]` still
refused, 19 guards green.
**Also corrected here**: I told c0 the count had gone 21 → 22 because they were writing v0.18.
Wrong. `beatsIn` returns every marker and `measure()` filters the quoted one — Anomaly Lore at
line 758, Act Three's quotation of the format. 22 markers, 21 real beats. Both figures were
right and the disagreement was mine.
+14 -1
View File
@@ -62,9 +62,22 @@ export function skillsIn(body) {
through. A beat spelled any of those four ways was checked by neither guard while both
printed OK. Same fail-open as R-289's `<!-- cast : -->` and check-cited's three spellings of
`cite:`; found the same way, by putting the spellings to the reader instead of to the tree. */
/* THE OPENING OF A TAG, AS ONE STRING, because that is the half that drifted. Two guards read
`[CUS: ...]` and their BODIES differ for good reasons — this one captures the whole tag and
lets skillsIn split it, outcome-coverage stops at the em-dash because the outcome text
follows. Sharing the whole pattern would be wrong. Sharing where a tag STARTS is not: the
literal `\[CUS:` was copied into both files, R-290 widened this one, and for three spellings
the two guards then disagreed about whether a tag existed at all — one reading it, the other
calling it unparseable. Compose your own body onto this and that cannot happen again.
R-291's cross-reader test still stands behind it; this is the prevention, that is the check. */
export const TAG_OPEN = String.raw`\[\s*CUS\s*:\s*`;
/** A fresh matcher each call — a shared /g regex carries `lastIndex` between callers. */
export const tagRe = (body = String.raw`([^\]]+)\]`) => new RegExp(TAG_OPEN + body, "gi");
export function tagsIn(text) {
const out = [];
for (const m of text.matchAll(/\[\s*CUS\s*:\s*([^\]]+)\]/gi)) out.push(m[1]);
for (const m of text.matchAll(tagRe())) out.push(m[1]);
return out;
}