Skip to content

tooling(vocabulary-gate): census the lower-case-LOCAL object-literal blind spot — CENSUSED-AND-EMPTY - #13789

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-13478-objlit-local-code-census
Aug 31, 2026
Merged

tooling(vocabulary-gate): census the lower-case-LOCAL object-literal blind spot — CENSUSED-AND-EMPTY#13789
os-project-manager merged 1 commit into
mainfrom
claude/issue-13478-objlit-local-code-census

Conversation

@claude

@claude claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13478 — and deliberately not a closing reference. The triage ruling pre-decided both endings, and the census came back on the preventive side, whose ending is close as not planned, not close as completed. That disposition is the PM's to apply on the card; this PR only lands the reading it turns on.

Scope

⚠️ This is a census, not a fix. No shape is widened. SHAPES still has 9 members and the gate still derives 51 sites — before and after, unchanged. The gate's reconciliation runs in both directions, so a widening cannot land before its declaration rows and the rows cannot land before the widening; that lock is exactly why the measurement had to come first, and why the measurement is all that is here.

The question

OBJECT_LITERAL_CODE_HELPER_BLINDNESS declared a remainder as UNCLOSED and UNCENSUSED: a code: in an object literal fed by a lower-case LOCAL — the twin of the assignconst local closed for the assignment position.

const c = intent === 'x' ? 'NOT_CREATABLE' : 'NOT_OVERRIDABLE';
return { code: c, message };            // reached by nothing

objlithelper declines it (the identifier is not a parameter), objlitconst needs SCREAMING_SNAKE, objlit needs a quote. So: no site and no unresolved — the one failure this gate cannot notice by itself.

It is still unclosed. It is no longer uncensused.

The reading

candidate positions reaching the hole 125
already covered at the same token by objlitconst 36
genuinely newly reached (69 distinct file+identifier, 45 files) 89
of those, reduce to a literal set 0
new verdict rows owed in dispatcher-error-vocabulary.ts 0
unregistered wire codes hiding 0
the card's exact class — a lower-case local holding a reducible literal set 0

preventive, not a live defect. The escalation branch (unregistered wire codes above zero) is not reached.

⚠️ The zero is not "the position is empty"

The position is populated: 24 value-position candidates feed a code: through a local or a destructured binding. Every one holds a runtime value, which is the declared #9460 bound rather than a hole. What is empty is the reducible subclass — not one local in this tree holds the ternary-or-chain of literals the widening would newly admit.

Entries reducing to nothing — counted as their own class, and classified

Each would be a red with no verdict available unless it can be classified, so each is classified. The four sum to 89:

class count why it does not reduce
TS keyword in a TYPE position 49 code: string / number / undefined — not a value stamp; the candidate regex reads code: Ident, and in a type literal that Ident is a type name
named TYPE in a type position 16 code: FieldErrorCode / ErrorCode / FlowRefusalCode / StandardErrorCode / PreflightViolationCode
local holding a RUNTIME value 15 const code = e?.body?.code ?? 'INTERNAL_ERROR' and kin — the declared #9460 bound
binding with no declarator 9 destructured const { code } = resolveThrownHttpError(...) — nothing for the resolver to anchor on

That 65 of the 89 are not value stamps at all is itself a finding about cost: a widening here would carry materially lower precision than the assignment position's did.

How it was measured

The instrument is the gate's own primitives (SHAPES, enclosingOpeners, helperCodesFor, resolveConstant), with the objlithelper branch replicated and exactly one line changed — where it declines a non-parameter identifier, the replica falls back to resolveConstant, as the assignment position's codehelper branch already does. Two instrument checks, because a zero from a broken instrument and a zero from a clean corpus are the same number:

  • Conformance — with the fallback OFF, the replica reproduced the real deriveSites objlithelper output exactly: 29 sites, 5 unresolved, identical after the dedup the gate itself applies.
  • Positive control — a synthetic corpus carrying this very shape yielded 5 emissions with the fallback ON (2 SCREAMING, 2 lowercase, 1 shorthand) and 0 with it OFF. A same-name-shadowing negative control yielded 0.

The control earned its keep: its first run failed, and the failure was real signal — resolveConstant resolves a name file-scoped, so two same-named locals in one file collapse to null even when each is individually reducible. That is a precision bound of the fallback, now pinned as its own control rather than mistaken for an instrument fault.

Verifying the ruling's mechanical premise directly rather than from the brief: resolveConstant('c', ...) on the card's own shape returns ["NOT_CREATABLE","NOT_OVERRIDABLE"]. The machinery is ready; the corpus is what is empty.

What lands

The census is recorded beside the constant it answers — localTwinCensus — flipping the declaration from UNCENSUSED to CENSUSED-AND-EMPTY, and the figures are printed in the gate's own bounds block so a partial gate cannot read as a complete one. Nothing in packages/ is touched; the declaration ledger is what this diff is measured against, never a fix site.

--self-test gains 8 assertions (217 to 225 on this tree; still 9 shapes) so the reading stays re-checkable instead of five numbers going stale in a comment. They fail in opposite directions:

  • the hole is still open — no site and no unresolved for the card's exact shape. Close it, and the pin says so and demands a re-census rather than an in-place edit of the figures;
  • the machinery is liveresolveConstant still reduces that same local to both branches. This is the positive control for the zero: a broken resolver would also report "nothing reduces", and without this half "censused and empty" is indistinguishable from "measured with a dead instrument".

Plus three coherence pins: the classes sum to the newly-reached count, the candidate split adds up, and reduce == 0 forces both decision rows to zero.

The pins were ablated, not assumed

Each mutation was proved on disk before the run and each restore proved by blob hash against HEAD plus an empty git diff HEAD — never by an exit code.

ablation result
close the hole (make objlithelper fall back to resolveConstant) self-test exit 1 — "now derives a SITE" fired
break ternary reduction in literalCodeValues self-test exit 1 — the ⭐ positive-control assertion fired
corrupt one figure (newlyReached 89 to 88) self-test exit 1 — the class-sum assertion fired

The first attempt at ablation 1 named a variable that does not exist in that scope and died with a ReferenceError before reaching any assertion. That was recorded as a broken instrument and re-run correctly, not read as a red.

Verification

Gate family derived after the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (commit e2c2b3441, 1 path). Every exit code below was captured before any pipe.

GREEN (exit 0): check:dispatcher-error-vocabulary (runs --self-test then the gate) · check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · 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 · check:nul-bytes · check-self-test-wired.mjs

Convention-triggered by editing a gate script, both GREEN: scripts/pm/bare-root-worklist.mjs --self-test · check:pm-dispatch-gates.

NOT MEASURED: check-test-completeness.mjs — exit 3, the gate's own distinct code for "no saved turbo run test log to read". Its failure text states this is not a finding and there is nothing to fix; recorded as not measured rather than as a red.

Repo-wide pnpm lint (eslint . --no-inline-config) run in full rather than narrowed: exit 0, 105s. pnpm --filter @objectstack/spec exec vitest run src/api/error-code-ledger.test.ts: 18/18 pass — the only file in the tree naming this script.

No changeset: scripts/ publishes from no package. Confirmed against what Check Changeset actually does rather than assumed — it counts added .changeset/*.md against the merge base with no path exemption, so skip-changeset is the mechanism and the label is applied.

Found while censusing, filed rather than fixed

A sibling spelling reached by no shape: an inline literal ternary at the stamp itself, code: cond ? 'A' : 'B', as opposed to a local holding one. objlit needs the quote immediately, objlitconst needs SCREAMING_SNAKE, objlithelper captures the condition's identifier and declines. Measured with its own positive control: 2 positions in the tree, 5 distinct values, and 0 unregistered wire codes — one is three ledger-registered codes at the REST door, the other a D6 field-addressed catalog entry ({ field: 'name', code: ... }) that is legitimately not a wire code. Preventive, like this card, and out of scope here.


Generated by Claude Code

…blind spot

`OBJECT_LITERAL_CODE_HELPER_BLINDNESS` declared the object-literal twin of the
`assignconst` local as UNCLOSED and UNCENSUSED. It is still unclosed; it is no
longer uncensused. Measured on this tree through the gate's own primitives, with
the `objlithelper` branch replicated and one line changed (fall back to
`resolveConstant` where it declines a non-parameter identifier, exactly as the
assignment position does):

  125 candidate positions - 36 already objlitconst's = 89 newly reached
  0 of the 89 reduce  =>  0 verdict rows owed  =>  0 unregistered wire codes
  the card's exact class (a lower-case local holding a reducible literal set): 0

So the widening would be PREVENTIVE, not a live defect.

The position is populated - 24 value-position candidates feed a `code:` through
a local or a destructured binding - but every one holds a RUNTIME value, which
is the declared #9460 bound rather than a hole. What is empty is the REDUCIBLE
subclass. The other 65 are not value stamps at all: the candidate regex reads
`code: Ident`, and in a type literal that Ident is a type name.

No shape is widened and no grammar changes; the reconciliation this gate runs in
both directions is untouched (51 sites before and after).

`--self-test` gains 8 assertions that keep the reading re-checkable instead of
leaving five numbers behind. They fail in opposite directions: one set asserts
the HOLE is still open (no site AND no unresolved for the card's exact shape),
so closing it forces a re-census rather than a silent edit of the figures; the
other is the POSITIVE CONTROL for the zero - `resolveConstant` still reduces that
same local to both branches - because a corpus zero and a dead-instrument zero
are the same number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants