diff --git a/docs/REVIEW_LOG.md b/docs/REVIEW_LOG.md index 2ea6ea4..544a1ba 100644 --- a/docs/REVIEW_LOG.md +++ b/docs/REVIEW_LOG.md @@ -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. diff --git a/tools/check-scenarios.mjs b/tools/check-scenarios.mjs index e136810..0a5dfb4 100644 --- a/tools/check-scenarios.mjs +++ b/tools/check-scenarios.mjs @@ -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 `` 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; }