fix(tooling): a template-literal mode for the vocabulary gate's four shared textual scanners - #14739
Conversation
…shared textual scanners (#14626) `scanTopLevel`, `enclosingOpeners`, `sliceBalanced` and `splitTopLevel` each carried the same seek-to-matching-quote skip, which a NESTED template literal walks straight out of: the outer template closed at the first inner backtick and every `code:` position after it in that file went unplaced. All four now call ONE shared `skipStringLiteral`, a small state machine that tracks `${ … }` nesting and recurses into literals opened inside an interpolation. `enclosingOpeners` answers every requested index with a TAGGED result, so "genuinely inside a string" and "the scanner lost its place" stop arriving as the same `undefined`; the other three take an optional `scanReport()`. The tag is certified — `inside-string` is asserted only for a walk that ends with no unterminated literal, no bracket underflow and an empty stack. `--self-test` gains a nested-template fixture per primitive, each PAIRED with a positive control run through the pre-fix `legacySeekQuote` kept in test scope. `OBJECT_LITERAL_CODE_HELPER_BLINDNESS` and `INLINE_LITERAL_EXPRESSION_CENSUS` are re-derived on this tree, with tree drift separated from what the fix moved, and `SCANNER_LITERAL_BLIND_SPOTS` records the residual class the sweep found (a regex literal carrying a quote) rather than leaving it to be rediscovered. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
…anner-nested-template-mode
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 33696604625 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes #14626
A nested template literal desynchronised the vocabulary gate's shared textual
scanners, and every
code:position after it in that file went unplaced. Thisadds a template-literal mode to all four primitives through ONE shared skipper,
makes the two
undefinedanswers distinguishable, and re-derives both pinnedcensuses on this tree.
The primitive population, re-derived by grep (not from the card's list)
On the branch base
4d0d9445a,grep -n "c === \"'\" || c === '\"' || c === '+ "" +'"finds the quote-skip at exactly FOUR sites, and the seek idiomwhile (i < src.length && src[i] !== quote) i += ... ? 2 : 1` at the four linesbeside them — the four the triage named, at this base's line numbers:
if (c === quote)scanTopLevelenclosingOpenerssliceBalancedsplitTopLevelAfter the change,
grep -c "!== quote) i +="over the file returns 1, andthat one is
legacySeekQuoteinside--self-test— the positive control.The helper's state machine, in five lines
skipStringLiteral(src, at)returns{ end, closed }, whereendis the indexof the CLOSING quote (the same post-condition the four seek loops had, so each
caller still advances past it with its own
i += 1).quoteandtemplateare stringmodes,
interpis a code mode.'/"— walk to the matching quote;\escapes the next byte.`— TEMPLATE:\escapes, a bare`closes,${pushes aninterpframe with brace depth 1.${ … }— CODE:{/}move the brace depth (so`${ {a: 1}.a }`closes where it should), and a quote there pushes a NEW literal frame, which
recurses through the same stack to any depth.
closed: false— theonly self-evident signature of a scan that lost its place.
The two
undefineds, told apart — and CERTIFIEDenclosingOpenersnow answers EVERY requested index with a tagged value:{kind:'bracket', ch, at}·{kind:'top-level'}·{kind:'inside-string'}·{kind:'desync'}. The other three primitives take an optionalscanReport()(
desynchronised,unterminatedAt,underflow,unbalanced), because theirreturn values have no room for a per-position tag.
inside-stringis asserted only for a walk that ends health-certified: nounterminated literal, no bracket underflow, an empty stack at EOF. Comments are
masked and brackets inside real literals are skipped, so a correct walk of
well-formed source ends empty; not ending empty is proof it did not. In a walk
that is not certified, every unvisited index is tagged
desync— "cannot placethis, and cannot certify why" — rather than being called
inside-string, whichwould be the same over-claim one layer up.
--self-test: a fixture AND a positive control per primitiveThe pre-fix
legacySeekQuoteis kept in test scope, with all four primitivesre-implemented around it differing in exactly the skip. Fixture:
`Unknown key${n > 1 ? 's' : ''} ${keys.map((k) => `\`${k}\).join(', ')} …` ``scanTopLevel;past the templateenclosingOpenerscode:after it is{-enclosedundefinedsliceBalancedsplitTopLevelPlus:
inside-stringon acode:written inside a string;desyncon aposition behind an unterminated template; the skipper's two interpolation cases
(braces and quotes inside
${ … }); and thescanReporton all three.Mutation on the committed tree. Deleting the
${-tracking branch fromskipStringLiteral(the skipper falls back to seek-to-matching-backtick)reddens exactly the four primitive cases:
Mutation confirmed on disk before the run (
grep -con the deleted text: 1 → 0;on the injected marker: 0 → 1; blob hash
9d4b8492→1dce95d3). Restored bygit checkout HEAD -- ABSOLUTE_PATHunder an EXIT/INT/TERM trap and proven back:blob hash
9d4b8492again,git diff HEAD --name-onlyempty.Censuses re-derived — tree drift separated from what the fix moved
Both readings come from the same sweep, run with the PRE-fix primitives and the
POST-fix ones over today's
packages/**non-test source.INLINE_LITERAL_EXPRESSION_CENSUSfilesScannedobjectLiteralCodeTokensanchorHitsnotObjectLiteralparameterListinsideAStringbehindScannerDesyncownedBySibling.objlitconstcandidatesReachingReductiondeclinedByReductiondeclinedTypeAnnotationdeclinedRuntimeValuelivePositionsdistinctValuesnewVerdictRowsunregisteredWireCodesHidingpositionsBehindScannerDesyncpositionsRecoveredByTemplateModeNewly PLACED positions, classified
Over the whole 906-token
code:population, 19 positions go from unplacedto placed. Two of them reach
objlitexpr— exactly the two the card predicted:domains/automation.ts:1384code: targetName === undefined ? 'required' : 'invalid_type':1125instance the gate already saw ⇒ derives the site keys that already exist ⇒ 0 new verdict rows.domains/automation.ts:1399code: targetLabel === undefined ? 'required' : 'invalid_type'The other 17 are
objlit/objlitconsttokens (those shapes are not guarded byenclosingOpeners, so they were never blind there) or type annotations. Twoenter
objlithelper's candidate population, which is whylocalTwinCensusmoves by 2.
No new value surfaced anywhere.
deriveSitesdiffed key-by-key across thechange: sites 59 → 59, unresolved 5 → 5, added
[], removed[], per-shapecounts identical. So the ⛔ escalation clause (an unregistered value at a newly
placed position ⇒ report
blocked) was not reached, andpackages/runtime/src/dispatcher-error-vocabulary.tsis untouched.OBJECT_LITERAL_CODE_HELPER_BLINDNESSmeasured.*anddeclarationFormClosure.*are historical deltas of #13233 /#13226, not censuses of today's tree; re-checked rather than assumed —
objlithelperderives the same 29 sites and the same 5unresolvedbefore andafter — and left as they were, with that check written into the block.
localTwinCensusIS a census of this tree, and it had already drifted:candidatePositionsalreadyCoveredByObjlitconstnewlyReachednewlyReachedDistinctFileIdentnewlyReachedDistinctFilesreduceToEmptyByClass.typeKeywordPositionreduceToEmptyByClass.namedTypePositionreduceToEmptyByClass.runtimeValueLocalreduceToEmptyByClass.bindingWithoutDeclaratorreducenewVerdictRows/unregisteredWireCodesHidingThe headline zero survives at both ends — the reducible subclass is still
empty — so no verdict row is owed. The instrument was the #13478 replica: the
real
objlithelperbranch with theresolveConstantfallback switched on,driven through the real
deriveSites; its zero is backed by the positivecontrol already in
--self-test(resolveConstantstill reducesconst c = cond ? A : Bto both branches).SCANNER_LITERAL_BLIND_SPOTS— the class this card did NOT closeThe sweep found a second desync class and it is declared with numbers rather
than left to be rediscovered: a regex literal whose character class carries a
quote or a backtick. The live one is
packages/rest/src/error-response.ts'sRELATION_DOES_NOT_EXIST, whose classcontains
",'and a backtick; a textual walk opens a string the source doesnot have.
code:positions unplaced pre-fix⛔ Deliberately not fixed here: telling
/re/from division needs the precedingtoken's grammatical class, which is lexer state this scan does not carry — a
heuristic would trade a blind spot that is now COUNTED for one that is not.
Filed as its own card.
--self-testpins the declaration in the direction itcan fail: a regex-literal fixture must still come back
desync, with the samesource minus the regex as the positive control, so the block cannot quietly
outlive the blind spot it declares.
Scope
git diff --stat: 1 file changed,scripts/check-dispatcher-error-vocabulary.mjs.scripts/check-error-code-casing.mjs— untouched. Confirmed by the diffand by
pnpm check:error-code-casingin the gate family below.packages/runtime/src/dispatcher-error-vocabulary.ts— untouched. Noledger row added or moved; no site or unresolved entry changed.
objlit,objlitconst,objlittemplate,objlithelperandobjlitexprregexes are byte-identical.Verification
The union below ran at HEAD
160c99d5(git rev-parse --short HEAD), thefinal commit on this branch — a merge of
origin/mainafter the fix commit0dc603cd2. The whole union was run again on that merged head rather thancarried over from the pre-merge run. Each exit code was captured by redirect
BEFORE any pipe.
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands scripts/check-dispatcher-error-vocabulary.mjsderives 18 commands from thistree (
objectstack-ai/objectstackat160c99d51,--repoasserted and held —the derivation is re-run from the merged head, never a
git difflist fed in byhand). All 18 run, all green except one NOT MEASURED:
node scripts/pm/dispatch-gates.mjs --ran RECORD_FILEon the--commandsoutputreconciles the run: "Run reconciliation — 18 derived, 18 run, 0 NOT-MEASURED,
0 UNRUN", exit 0.
check-test-completenessexit 3 is its own declared NOT-MEASURED branch, inits own words: "Arrived here from the gate family
scripts/pm/dispatch-gates.mjsderives? … There is no local log to hand it, so the local reading for this gate
is NOT MEASURED. ⛔ It is not a red." CI tees a real
turbo run testlog intoit, so CI measures it.
Named by the dispatch and outside the derived family, run anyway:
The gate's own verdict lines:
Lint, narrowed and the narrowing measured (not "not run"):
count; the diff is 1 file, so
npx eslint --no-inline-config --format json scripts/check-dispatcher-error-vocabulary.mjsis the whole of it.exit 0.
eslint.config.*states it runs with noparserOptions.projectand no typed
@typescript-eslintrules, so type-aware linting is off and aone-file diff cannot move the verdict on any untouched file. Repo-wide
pnpm lintis CI's run.🤖 Generated with Claude Code
https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
Generated by Claude Code