Uh oh!
There was an error while loading. Please reload this page.
Gate the entry-nameability invariant across every public spec entry - #12418
Merged
Conversation
Generalize the root-entry declaration-emit probe into a per-entry `check:` gate: for each of the 17 module entries in `packages/spec`'s `exports` map, compile a one-file consumer program that CALLS every callable export and assert the declaration emitter reports nothing it cannot name. Entries are deliberately never batched into one program — a program file importing a subpath entry makes that entry's names nameable program-wide, so a batched run reports zero leaks at full cost. The `--self-test` measures that in both directions alongside a positive control, the repaired-fixture negative control, and the declaration-emit canary. Measured: 7 pre-existing leaks of the same class across 4 entries, recorded in a hand-ratcheted, shrink-only ledger that is closed to new rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5LFCYBJ3q2s6yW6oMLxwy
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Aug 26, 2026
huangyiirene
marked this pull request as ready for review
August 26, 2026 02:38
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#11986
Generalizes the root-entry declaration-emit probe into a per-entry
check:gate, so the entry-nameability invariant is held closed for every public entry ofpackages/specinstead of repaired one name at a time.The invariant, maintainer ruling 2026-08-23 recorded on #11350 (quoted verbatim, untranslated):
Chartered by the 2026-08-25 ruling on #11709 — 「11709 11742 同意」 — which accepted A′ (the two root re-exports, landed as #12327) plus C, this gate.
What it does
For each of the 17 module entries in the
exportsmap, the gate compiles a one-file consumer program withdeclaration: true, resolved the way a real consumer resolves it (anode_modules/@objectstack/specsymlink, so tsc walks the package's ownexportsmap), and asserts the declaration emitter reports nothing it cannot name. 422 call probes in total, 31–47s wall clock.Design decisions, each settled by measurement
Two probe shapes were measured; the call shape was chosen because the other one is blind to the defect.
export const r = E.NAME;, one per value export. Re-prints each export's declared type. Measured with A′ reverted and the dist rebuilt: the root entry's leak set does not move.defineStack's declared return type isObjectStackDefinition, which is nameable from the root entry, so the printer names it instead of expanding it. The leak exists only at a call.export const c = E.FN(null as never, …);for every export whose type has a call signature, with as manyneverarguments as the signature has required parameters.neveris assignable to every parameter type, so this needs no per-factory fixture — and the emitted declaration is byte-identical to the pin's realdefineStack({ objects: [] })call (verified by diffing the two.d.tsoutputs). This is the shape that moves under the ablation.The entries are never batched into one program, and that is load-bearing rather than an optimisation left on the table. The dispatch suggested batching with per-entry namespaces if the cost were CI-material. Measured, batching would silently empty the gate: a program file that imports a subpath entry makes that entry's names nameable program-wide (#11350's control). A single program importing all 17 entries can name every type any of them declares and reports zero leaks no matter how many exist — full cost, no measurement. The
--self-testnow measures this in both directions permanently, so the claim cannot rot into prose.Enumeration is type-level, via the checker, not
typeof x === 'function'. Zod v4 schema objects are runtime functions — 375 of/api's 389 runtime functions are zod schemas with no call signature — so a runtime predicate generates hundreds of bogusTS2349: This expression is not callablediagnostics per entry and the checker never reaches declaration emit for the real probes.Diagnostics land in three buckets, never two. The
cannot be namedfamily (matched on message substance, not a fixed code list, so a compiler renumbering cannot empty it) is the finding. TS7056 — the compiler refusing to serialize an inferred type at all — is reported as NOT MEASURED, because the printer gives up before name resolution and neither "clean" nor "leaking" is supportable. Anything else is a refusal naming the diagnostic: a probe program that fails to compile for an unrelated reason has measured nothing (#4690).The premise the card carried is refuted, and the gate ships with a ledger because of it
The card expected the gate to be green at zero the moment A′ landed. It is not. The call-shape probe finds seven leaks across four entries on
cdbd9204:@objectstack/specBook,FormField,NavigationItem@objectstack/spec/kernelUnknownAuthoringKeyFinding@objectstack/spec/aiFilterCondition,StateNodeConfig@objectstack/spec/uiFilterConditionThat is not a different defect class — it is the card's own thesis arriving early. A′ repaired the
defineStackinstance and six siblings of it were live the whole time with nothing measuring them.They are recorded in
entry-nameability.baseline.json: hand-ratcheted, shrink-only in both directions (a new name fails; a recorded name that no longer leaks also fails, with an instruction to delete the row), and closed to new rows. Repairing them here would edit four entry barrels and regenerateapi-surface/— a different file surface and a different gate family from this card's, and two of those barrels were claimed by other in-flight work in this batch — so they are filed for triage as #12414 instead. The ledger is the ratchet, not the destination.The 27 pairs the reference shape finds are a separate scope question, filed as #12416 with the measurement.⚠️ They are deliberately not added to this ledger: a shrink-only ledger with no shrink path is a permitted accommodation surface, and the ledger's whole warrant is that every row it carries is repaired by one re-export line.
Acceptance criteria, each with its measured result
BaseValidationRuleShapeandFilterCondition— ✅ met exactly. Mutation proven on disk (git hash-objectmoveda2a790b1…→6b8da6af…, both anchor greps 0), spec rebuilt on the mutated source, dist proven mutated (root-entry export statements naming the two types: 0 and 0), gate exit 1 reporting2 NEW unnameable structural mention(s) across 1 public entry—@objectstack/spec:BaseValidationRuleShape,FilterCondition, and nothing else. Restore leg:git checkout HEAD --against an absolute path,git diff HEADempty, hash back toa2a790b1…, rebuilt, dist export statements back to 1 and 1.0 new unnameable structural mentions, exit 0, 422 probes, 17 entries.{ filter: FilterCondition }injected intosrc/security/index.ts(an entry that is clean today), rebuilt, marker proven present indist/security/index.d.ts; gate exit 1 naming@objectstack/spec/security→FilterCondition. Restored, rebuilt, marker proven absent from dist. The--self-test's positive control mechanizes the same thing permanently on a hermetic fixture, with a repaired-fixture negative control beside it.Anti-phantom controls
TS2883 is a declaration-emit diagnostic: drop
declaration: trueand every probe is green forever. Two independent controls, both required to pass on every run:The self-test runs on every invocation rather than only under
--self-test, socheck:entry-nameabilitycannot be run without its own positive control.Wiring
check:entry-nameabilityinpackages/spec/package.json, beside the spec gate family.NO_GENERATORincheck-generated.ts(reconciliation green: 27check:+ 17gen:, all classified). The classification is the safety property, not bookkeeping — agen:that rewrote the ledger from whatever the tree currently leaks would admit the eighth leak by running a command, which is the whack-a-mole this gate was filed to end.typecheck-consumerslane, withcheck:api-surface/check:exported-any/check:dual-source-exports— the other gates that read the built dist a consumer's import resolves to. Adds no required context (a step in an existing lane, TheESLintrequired context carries ~54check:*gates, so every one of their failures mis-routes its own diagnosis — and the rename needs a Settings change no agent seat can make #9325). The#11350pin above it is untouched and keeps its own charter: it reproduces the real configs' program shape verbatim, fixture file included.Gates run locally
All at
0eb9f84, the final commit, exit codes captured before any pipe:check:entry-nameability(exit 0) ·check:scripts-typecheck(exit 0) ·check:generated --reconcile-only(exit 0) ·check:merge-driver·check:published-files·check:nul-bytes·check:pnpm-filter-targets·check:workflow-status-functions·check-self-test-wired·check-self-test-workflow-commands·check-step-collectors·check-aggregator-roster·check-required-contexts·check-cross-package-test-inputs·check-ci-filter-parity— all exit 0. Families re-derived from the real change set withnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackrather than from the dispatch list.No changeset: the diff is tooling only — a gate script, its ledger, script wiring, a gate classification and one CI step. Nothing user-visible ships, so the PR carries
skip-changeset.Generated by Claude Code