Skip to content

fix(spec): re-export the three types the root entry's own inferred types mention (#11350) - #11716

Merged
os-warren merged 5 commits into
mainfrom
claude/issue-11350-root-type-reexports
Aug 24, 2026
Merged

fix(spec): re-export the three types the root entry's own inferred types mention (#11350)#11716
os-warren merged 5 commits into
mainfrom
claude/issue-11350-root-type-reexports

Conversation

@os-warren

@os-warrenos-warren commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Fixes#11350

Ruling implemented

Maintainer 2026-08-23 (recorded in issue comment 5386669705, verbatim 「10950 不考虑存量,其他接受你的建议」): add the three type re-exports (FormFieldInput, NavigationItemInput, StateNodeConfig) to the spec root entry — the measured fix (3 errors → 0).

Invariant recorded (and carried as a comment on the new export block in src/index.ts): a type that appears structurally in an entry's public declarations must be nameable from that same entry.defineStack returns ObjectStackDefinition, declared as the z.input instantiation of ObjectStackDefinitionSchema — a generic instantiation the declaration emitter does not preserve as an alias — so an un-annotated export default defineStack(...) is emitted as the structural expansion, which mentions these three types. All three were already public on /ui and /automation; the root entry simply could not name them, so tsc reached for the hash-named dist chunk (TS2883, unaddressable through the exports map).

Changes

  • packages/spec/src/index.ts — the three export type re-exports, from the modules that declare them, with the invariant + card reference in the comment.
  • packages/spec/api-surface/root.json — regenerated via gen:api-surface; grows by exactly 3 rows (FormFieldInput (type), NavigationItemInput (type), StateNodeConfig (type)).
  • packages/spec/export-origins/root.json — regenerated via gen:export-origins (check:generated proved it stale; +3 origin rows).
  • packages/spec/scripts/root-entry-type-nameability.pin.test.ts — pin test compiling the consumer shape verbatim (un-annotated export default defineStack(...)) against the built root dts, resolved through a real node_modules symlink + the package's own exports map, with declaration: true. A second program is a hermetic declaration-emit canary (TS4094; measured red with declaration: true, green without), so dropping the declaration axis from the harness cannot silently turn the pin into a phantom check. Dist staleness is refused via the shared inspectDistFreshness rule.
  • .changeset/root-entry-type-reexports-11350.md — minor for @objectstack/spec (public face +3, additive; not breaking, so no ADR-0087 disposition marker is owed — check:adr-0087-registration green).

Measurements

  • Before (base 4ceae8ab0, annotation from the earlier consumer-side repair reverted on packages/plugins/plugin-audit/scripts/i18n-extract.config.ts, dependency closure freshly built): npx tsc --noEmit -p tsconfig.repro.json → exactly 3 × TS2883 naming the three types from dist/view.zod-DlqDxILi.js / dist/app.zod-CeA4DPnB.js / dist/state-machine.zod-BTAjKYaW.js; exit 2.
  • After (same mutated consumer, spec rebuilt with the re-exports, names confirmed present in dist/index.d.ts): same command → 0 diagnostics, exit 0. The consumer file was then restored to its committed annotated form (untouched by this PR).
  • Reverse verification (from the committed fix): the three names were stripped from the built dist/index.d.ts final export statement (mutation proven on disk: replacement count 3, final-line grep 0, file differed from backup), the pin test went red with exactly the 3 TS2883s naming the three types, and the dist was restored byte-identically (cmp clean) with the pin green again.

Premise delta — reported, not silently fixed

The pin's minimal one-file program surfaced two more names leaking through the chunked build that the fixed root entry still cannot name: BaseValidationRuleShape and FilterCondition (both public on /data). The nine i18n-extract programs never see them because their object modules import @objectstack/spec/data — the card's own control effect. #11709 is not addressed here and remains open, filed with the measurement and options (including the card's Option C gate, which its dispatch reserved for a proposal rather than this PR). The pin test's consumer program deliberately mirrors the real consumers (a context.ts importing /data), scoping the pin to the ruled three; its docblock records the delta and points at #11709.

Verification at final head ddf0566b2

  • pnpm --filter @objectstack/spec exec vitest run — 421 files / 11224 tests passed (ran at d62f9cc42; the only later commit changes one docblock line inside the pin test, which was rerun green at ddf0566b2).
  • pnpm --filter @objectstack/spec typecheck — exit 0 (tsc --noEmit + scripts + test-layer; ledger line: "55 file(s) / 263 error(s) held", unchanged).
  • pnpm --filter @objectstack/spec check:generated — exit 0, all 14 artifacts current (check:api-surface ✓).
  • Derived local gates (node scripts/pm/dispatch-gates.mjs, no hand-built path list; derivation stamped objectstack-ai/objectstack): all 22 matched + test-kind convention gates green at ddf0566b2, except check-dev-prereqs.mjs exit 1 solely on its environmental precondition ("The workspace is not built — 1 unmet precondition": 52/67 package dists absent in this fresh worktree; spec's own dist is built and stamped; CI runs it after pnpm build).
  • Targeted eslint narrowing, declared: population = pnpm lint (eslint . --no-inline-config, one flat config); files linted from --format json = 2 (the only lintable files in this diff — the md/json artifacts report "File ignored because no matching configuration was supplied"); invariance = the config's own text states type-aware linting is never enabled for any file, so this diff cannot move untouched files' verdicts. Result: 0 errors / 0 warnings, exit 0.
  • check:type-check-debt --re-measure not run locally (needs the full workspace closure built), declared: the only ledger entries this diff can reach are spec's, re-measured green inside pnpm --filter @objectstack/spec typecheck; the change is additive on dist exports, which cannot remove or narrow anything a consumer package's program reads.

Generated by Claude Code

…pes mention (#11350)
An un-annotated `export default defineStack(...)` is emitted as the
structural expansion of `ObjectStackDefinition` (declared `z.input<...>`,
which the declaration emitter does not preserve as an alias). That expansion
mentions `FormFieldInput` / `NavigationItemInput` / `StateNodeConfig`,
which were public on their domain subpaths but not on the root entry — so
tsc could only name them through a hash-named internal dist chunk (TS2883
in every consumer inferring through a root-entry function).
Invariant recorded (maintainer ruling 2026-08-23): a type that appears
structurally in an entry's public declarations must be nameable from that
same entry. Measured: the plugin-audit i18n-extract repro goes 3 errors -> 0.
api-surface/root.json regenerated via gen:api-surface (+3 rows). Pin test
compiles the consumer shape against the built root dts, with a
declaration-emit canary guarding the harness axis.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
… add changeset (#11350)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 24, 2026
@github-actions

github-actionsBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 2 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/metadata-protocol/src/protocol.ts, packages/spec/api-surface/root.json, packages/spec/export-origins/root.json, …) — pages documenting those are invisible to this run

Coarse fallback — 127 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8b41bcd85160ac1c475d26b2968d74f84954bbfapackageMentionDocs.

…11350)
Test Core deliberately never builds spec's own dist (turbo's test task
depends on ^build, dependencies only), so the pin's beforeAll freshness
throw redded a whole CI shard for a measurement that lane cannot make.
The pin now follows the live-dialect-cell discipline — reported, never
omitted, no third outcome: dist fresh = both programs run; dist absent
by default = a NAMED skip carrying the refusal reason; dist absent under
OS_EXPECT_ROOT_NAMEABILITY=1 = a failure quoting the refusal. The flag
is set in lint.yml's 'Type Check - consumer gates' lane right after its
full packages-closure builds, beside check:api-surface and
check:skill-examples — the other consumer-shaped gates reading the
built dist — so the pin still runs for real in CI and cannot quietly
degrade to never-measured.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
…to the /ui entry (#11350)
protocol.ts imports both the spec root (applyConversionsToStoredItem)
and /ui, and its inferred public declarations structurally mention
FormFieldInput. Once the root entry exported that name, tsc's
declaration emitter switched its synthesized reference from /ui to the
root — both portable, but the root specifier drags spec's entire root
module graph into every downstream tsc program reading this package's
dts (measured on the debt-ledger re-measure of
@objectstack/http-conformance: +2 program files, +190k types, +805k
instantiations, +~560MB — past a 4GB child heap, which is what redded
Type Check - debt ledger on PR #11716). A local type-only import
binding makes the emitter reuse it, keeping the reference on the narrow
/ui entry. Erased at runtime: emitted JS byte-identical; the curated
index.ts entry is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
@os-warren
os-warren marked this pull request as ready for review August 24, 2026 21:39
@os-warren
os-warren added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit ece4dadAug 24, 2026
51 of 53 checks passed
@os-warren
os-warren deleted the claude/issue-11350-root-type-reexports branch August 24, 2026 22:00
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddocumentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

3 participants

@os-warren@os-zhuang@claude