Skip to content

Record the object-literal code-helper blindness with its measured blast radius (no widening) - #13228

Merged
os-elon merged 1 commit into
mainfrom
claude/issue-13131-codehelper-objlit-radius
Aug 29, 2026
Merged

Record the object-literal code-helper blindness with its measured blast radius (no widening)#13228
os-elon merged 1 commit into
mainfrom
claude/issue-13131-codehelper-objlit-radius

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#13131

This round is scoped to measurement, per triage's grading comment: 「本轮范围只到测量 …… 先量出加宽后的爆炸半径,⛔ 不先加宽。数没量出来之前,"该不该加宽"不是一个可裁的问题。」

⛔ No shape is widened. ⛔ No verdict row is added. The gate's verdict on the tree is unchanged: 22 classified sites, 0 unresolved, exit 0 — byte-identical to main's verdict.

What lands

One file: scripts/check-dispatcher-error-vocabulary.mjs.

  • OBJECT_LITERAL_CODE_HELPER_BLINDNESS — a frozen declaration carrying the corpora, the method and the measured numbers, following the KEBAB_DIAGNOSTIC_VOCABULARY precedent in the same file.
  • A --self-test block pinning both blindnesses, each against a same-genre positive control, so a later widening of either half cannot land silently.
  • A line in the run's printed bounds, so a partial gate cannot read as a complete one.

The card's four-corpus table, reproduced through the real deriveSites and the real findViolations

#helper bodycodevocabulary gatecasing gate
A (control)(e as any).code = codeHELPER_SCREAMING1 site, shape codehelper0
Breturn { severity: 'error', code, message }HELPER_SCREAMING0 sites, 0 unresolved0
Csame, shorthandhelper_lowercase0 sites, 0 unresolved0
Dreturn { severity: 'error', code: code, message }HELPER_SCREAMING0 sites, 0 unresolved0

A reproduces exactly, so B/C/D's zeros are readings rather than a dead instrument.

The blast radius — the two counts, reported separately

Predicate: a code property of an object literal whose value is an identifier that is a parameter of the enclosing function. Evaluated on the real TypeScript AST over packages/** non-test source (2082 files, 297 registered codes), with call-site arguments reduced by this gate's own literal grammar (^[A-Za-z][A-Za-z0-9_]*$, so a hyphen does not reduce) and checked against the registered vocabulary and the declaration table.

predicate matches 13 helpers (12 named, 1 anonymous)
(a) call sites newly reached 106 - 76 reduce to a literal, 30 do not
(b) NEW verdict rows needed 29 - 0 of them already carry a row
(c) undischargeable unresolved 4

Split by whether enclosingDeclaration can see the declaration at all:

buckethelpers(a) call sites(b) new rows(c) unresolved
reachable today (position blindness only)981291
blocked by the class-method blindness42503

⚠️(b) is not the whole cost, and reading it as the whole cost is the trap. All 29 new rows are lowercase FieldErrorCode diagnostics (required, invalid_type, min_value, …) in four files — ADR-0112 D6 genre, cheap paperwork. The 4 unresolved findings are the expensive half: an unresolved entry is pushed unconditionally and no declaration row discharges it (see reconcile), so each is a RED gate with no verdict available. That includes Parser#error, whose 16 call sites all pass kebab literals this gate's grammar refuses to reduce.

A second, independent blindness — the card's own live instance needs both

enclosingDeclaration's DECL_HEADER_RE recognises function NAME(, constructor( and const|let|var NAME = ( — and no class method. Measured with a same-genre control:

#declaration formbodysites
K (control)free functione.code = code1, codehelper
Iclass methode.code = code0, and 0 unresolved
Jclass method{ severity, code, message }0, and 0 unresolved

Parser#error in packages/sdui-parser/src/parse.ts is a class method and stamps through an object literal, so widening the position alone would not reach it. Filed separately as #13226. parseParamNames mis-parsing two-modifier TypeScript parameter properties is #13227 — same failure class, third mechanism.

Triage's p2 basis — verified, and refined

The grade rests on there being no visible victim today. Verified across the whole repo, not relayed: of the 33 SCREAMING_SNAKE values the widening would reach, 33 are already registered, so the count that are both ADR-0112 D1 shaped and unregistered is 0. check:error-code-casing's findViolations returns 0 on every file involved. The p2 grade stands.

Two refinements to the card's reasoning, neither of which moves the grade:

  1. Kebab-case is not a full exclusion in the helper position. Measured: the A-control spelling with a kebab call-site literal yields 0 sites but 1 unresolved finding. Kebab keeps the values out of the site population; it does not keep the helper out of the finding population.
  2. The reason Parser#error is silent today is the blindness itself (both halves), not only its casing.

Verification — all on the final commit 5f70b8bfb9

Exit codes captured before any pipe, and each verdict quoted from the gate's own line.

check-dispatcher-error-vocabulary --self-test exit=0 "8 shapes + 175 assertions OK"
check-dispatcher-error-vocabulary exit=0 "OK - 22 unregistered code-stamping site(s), all classified"
check:self-test-wired --self-test / (scan) exit=0/0 "every one of the 148 script(s) CI runs ... has that self-test run by CI"
check:nul-bytes exit=0

Derived family, node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (13 matched + 2 convention-triggered for editing a gate script) — all exit=0:
check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:dispatcher-error-vocabulary, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:watch-hint-literal, check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, check-shard-attestation.mjs, pm/bare-root-worklist.mjs --self-test, check:pm-dispatch-gates.

check-test-completeness.mjs exits 3 with PREREQUISITE NOT MET — its own text says this branch is NOT MEASURED, not a red (#13110).

The pins are shown capable of failing

A green self-test over a healthy tree proves nothing, so both halves were ablated. Each mutation was proved on disk by an anchored grep -c on the injected text plus a blob-hash change; each restore leg used git checkout HEAD -- ABSOLUTE_PATH under an EXIT INT TERM trap and was proved by hash equality with the HEAD blob and an empty git diff HEAD, with the self-test green again afterwards.

  • Mutation A — widen codehelper with an object-literal shorthand anchor: self-test exit 1, 4 assertions fire, naming the measured numbers and the decision.
  • Mutation B — give DECL_HEADER_RE a class-method header: self-test exit 1, the class-method assertions fire, including the mechanism-level one on enclosingDeclaration.

The counting instrument was falsified before its output was read: 5/5 corpora (A/B/C/D plus the class-method case) and 8/8 negative controls — call-argument list, array element, template interpolation, destructured local, destructured param, module constant, property-access value, object pattern in an assignment. An earlier regex instrument was discarded because it matched ${code} in a template and f(a, code, b) in an argument list, then lost a true positive when a bracket inside a regex literal unbalanced its scan.

Bounds this measurement does not have

helperCodesFor scans only the declaring file, so every count is over in-file call sites. An exported helper called from another package contributes 0 — sendError in packages/types/src/response-envelope.ts has 0 in-file calls and is one of the four unresolved. Counting cross-file callers needs resolution this source scan does not have: that number is unmeasured, and (a) is a lower bound on it, never an upper one. Measuring it would need a real module graph or a type-aware pass.

Scope

packages/sdui-parser/** was read as evidence only and is untouched here (held by #13154). No changeset: this edits a CI-internal gate script and releases nothing, which is the skip-changeset case lint.yml prescribes; the label is applied on this PR.

The widening question is now decidable, and by triage's own framing it belongs on its own card together with the domain:cli verdict rows. Not filed here — that is the PM's call, and it is raised in the dispatch report.


Generated by Claude Code

… measured blast radius
`check-dispatcher-error-vocabulary.mjs`'s `codehelper` shape is anchored on an
assignment (`.code = code`), so a helper that stamps through an object literal
(`return { severity, code, message }`, shorthand or longhand) matches no shape
in either gate: no site AND no unresolved entry. A second, independent
blindness sits beside it — `enclosingDeclaration` has no header form for a
class METHOD, so a method helper is unreachable even in the assignment position
the gate already implements.
Measured rather than widened, per triage's scoping of this round. The numbers,
the method and both blindnesses are recorded in
`OBJECT_LITERAL_CODE_HELPER_BLINDNESS`, printed in the run's declared bounds,
and pinned by `--self-test` with a same-genre positive control on each axis, so
a later widening of either half cannot land silently.
No shape is widened and no verdict row is added: the gate's verdict on the tree
is unchanged at 22 classified sites.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CPrUz21stTFhJRUirdc4yw
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] a code-carrying helper that stamps through object-literal shorthand ({ code }) is invisible to both code-vocabulary gates, for any casing

2 participants

@os-elon@claude