Skip to content

Commit 5150053

Browse files
huangyiireneclaude
andauthored
Gate the entry-nameability invariant across every public spec entry (#12418)
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. Claude-Session: https://claude.ai/code/session_01E5LFCYBJ3q2s6yW6oMLxwy Co-authored-by: Claude <noreply@anthropic.com>
1 parent bc56e18 commit 5150053

5 files changed

Lines changed: 859 additions & 0 deletions

File tree

.github/workflows/lint.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4258,6 +4258,28 @@ jobs:
42584258
OS_EXPECT_ROOT_NAMEABILITY: '1'
42594259
run: pnpm --filter @objectstack/spec exec vitest run scripts/root-entry-type-nameability.pin.test.ts
42604260

4261+
# [#11986] The same invariant, generalized from ONE entry to all 17 — the
4262+
# C half of the 2026-08-25 ruling on #11709 (A′ was the two re-exports,
4263+
# this is the gate that stops a fourth round of per-name repair). The pin
4264+
# above keeps its own charter: it reproduces the REAL configs' program
4265+
# shape verbatim, fixture file included. This one compiles a probe per
4266+
# public entry over every callable export, so no entry sits unmeasured.
4267+
#
4268+
# ⛔ Never batch the entries into one program to save time. A program file
4269+
# importing a subpath entry makes that entry's names nameable PROGRAM-WIDE
4270+
# (#11350's control; the gate's --self-test measures it in both
4271+
# directions), so a single batched program reports zero leaks no matter how
4272+
# many exist — full cost, no measurement. Measured here: 47s for all 17
4273+
# entries, 422 probes.
4274+
#
4275+
# Sits with its family — `check:api-surface` / `check:exported-any` /
4276+
# `check:dual-source-exports` — because it reads the built dist a
4277+
# consumer's import actually resolves to, so it needs the build steps
4278+
# above and refuses (never skips) on a stale or unbuilt tree. Adds no
4279+
# required context: a step in an existing lane (#9325).
4280+
- name: Check public-entry type nameability (built dist, declaration emit)
4281+
run: pnpm --filter @objectstack/spec run check:entry-nameability
4282+
42614283
# Same surface, the other axis: api-surface/ records that an export
42624284
# EXISTS, never what it resolves to — so four exported types sat at `any`
42634285
# across a whole major with every gate green (#4171). #4115 tells consumers
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"$comment": [
3+
"Shrink-only ledger for `check:entry-nameability` (#11986). Each entry records a",
4+
"(public entry, type name) pair that the entry-nameability invariant already",
5+
"violates on `main` — a type structurally mentioned in the declaration emitted for",
6+
"a consumer that CALLS one of that entry's exported functions, which cannot be",
7+
"named through the package's `exports` map.",
8+
"",
9+
"HAND-EDITED, NEVER GENERATED, and deliberately so — the same reason",
10+
"`dual-source-exports.baseline.json` is: a `gen:` that rewrote this file would let a",
11+
"new leak be admitted by running a command instead of by a decision, which is the",
12+
"whack-a-mole #11986 exists to end.",
13+
"",
14+
"SHRINK-ONLY IN BOTH DIRECTIONS. A name that appears and is not recorded here fails",
15+
"the gate. A name recorded here that no longer leaks ALSO fails the gate, with an",
16+
"instruction to delete the line: a ledger that keeps entries after they are repaired",
17+
"stops describing the tree and starts covering for it.",
18+
"",
19+
"THE SHRINK PATH IS ONE LINE PER ROW. Each of these is repaired exactly the way",
20+
"#11350 and #11709 were: re-export the named type, on the leaking entry, from the",
21+
"module that declares it. They are NOT repaired here because doing so edits four",
22+
"entry barrels and regenerates `api-surface/` — a different file surface and a",
23+
"different gate family from this card's, and two of those barrels were claimed by",
24+
"other in-flight work when this landed. They are filed for triage instead; see the",
25+
"PR body for the measurement and the issue link.",
26+
"",
27+
"MEASURED ON: cdbd9204b65ae4ebec63f90f61669fb498efaa55 (2026-08-26)."
28+
],
29+
"entries": {
30+
"@objectstack/spec": {
31+
"leaks": ["Book", "FormField", "NavigationItem"],
32+
"why": "Root-entry factories whose return type expands to mention a `/system`, `/ui` type the root entry does not re-export. Same class and same repair as #11350's three names and #11709's two — A′ closed the `defineStack` instance only. Repair: re-export each from its declaring module in `src/index.ts`."
33+
},
34+
"@objectstack/spec/kernel": {
35+
"leaks": ["UnknownAuthoringKeyFinding"],
36+
"why": "A `/kernel` export's call result mentions a finding type declared in an internal module and not re-exported from the `/kernel` entry. Repair: re-export it from `src/kernel/index.ts`."
37+
},
38+
"@objectstack/spec/ai": {
39+
"leaks": ["FilterCondition", "StateNodeConfig"],
40+
"why": "`/ai` factory return types expand to mention a `/data` filter type and an `/automation` state-machine type; both are public on their own subpaths but not nameable from `/ai`. Repair: re-export both from `src/ai/index.ts`."
41+
},
42+
"@objectstack/spec/ui": {
43+
"leaks": ["FilterCondition"],
44+
"why": "`/ui` factory return types expand to mention `/data`'s `FilterCondition`, which `/ui` does not re-export. Repair: re-export it from `src/ui/index.ts`."
45+
}
46+
}
47+
}

packages/spec/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@
266266
"check:export-origins": "tsx scripts/build-export-origins.ts --self-test && tsx scripts/build-export-origins.ts --check",
267267
"check:exported-any": "tsx scripts/check-exported-any.ts --self-test && tsx scripts/check-exported-any.ts",
268268
"check:dual-source-exports": "tsx scripts/check-dual-source-exports.ts --self-test && tsx scripts/check-dual-source-exports.ts",
269+
"check:entry-nameability": "tsx scripts/check-entry-nameability.ts",
269270
"check:browser-reachable-entries": "tsx scripts/check-browser-reachable-entries.ts --self-test && tsx scripts/check-browser-reachable-entries.ts",
270271
"check:objectui-pin-citations": "tsx scripts/check-objectui-pin-citations.ts --self-test && tsx scripts/check-objectui-pin-citations.ts",
271272
"check:llms-txt": "tsx scripts/check-llms-txt.ts --self-test && tsx scripts/check-llms-txt.ts",

0 commit comments

Comments
 (0)