Skip to content

Commit ece4dad

Browse files
os-warrenclaude
andauthored
fix(spec): re-export the three types the root entry's own inferred types mention (#11350) (#11716)
* fix(spec): re-export the three types the root entry's own inferred types 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 * chore(spec): regenerate export-origins for the three root re-exports; add changeset (#11350) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy * docs(spec): point the nameability pin's premise-delta note at #11709 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy * fix(spec): make the nameability pin an environment-gated measurement (#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 * fix(metadata-protocol): pin the emitted specifier for FormFieldInput 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c4db311 commit ece4dad

8 files changed

Lines changed: 325 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
Pin the declaration emitter's module specifier for `FormFieldInput` to `@objectstack/spec/ui` (#11350). When #11350 made the three ui/automation input types nameable from `@objectstack/spec`'s root entry, tsc's declaration emitter for this package switched its synthesized reference for `FormFieldInput` from the `/ui` slice to the root entry — both portable, but the root specifier pulls spec's entire root module graph into every downstream TypeScript program that reads this package's declarations (measured: +190k types, +805k instantiations, roughly +560MB on one real program). A local type-only import binding keeps the emitted reference on the narrow `/ui` entry. Type-only and erased at runtime: every emitted JS file is byte-identical; the package's public export surface is unchanged.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
Re-export `FormFieldInput`, `NavigationItemInput` and `StateNodeConfig` from the package root entry (#11350). These types appear structurally in the root entry's own public declarations — `defineStack` returns `ObjectStackDefinition`, declared `z.input<typeof ObjectStackDefinitionSchema>`, which the declaration emitter expands structurally rather than preserving as an alias — but they were previously nameable only via the `/ui` and `/automation` subpaths. Any consumer letting TypeScript infer a type through a root-entry function (an un-annotated `export default defineStack(...)`) therefore hit TS2883 naming a hash-named internal dist chunk. With the re-exports, that consumer shape declaration-emits cleanly, with no annotation required. Invariant recorded: a type that appears structurally in an entry's public declarations must be nameable from that same entry.

.github/workflows/lint.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3737,6 +3737,24 @@ jobs:
37373737
- name: Check @objectstack/spec public API surface
37383738
run: pnpm --filter @objectstack/spec run check:api-surface
37393739

3740+
# [#11350] Consumer-shaped declaration-emit pin against the BUILT root
3741+
# entry (an un-annotated `export default defineStack(...)` must compile
3742+
# with `declaration: true` — the TS2883 class). The pin is environment-
3743+
# gated the way the live-dialect cells are: under Test Core, spec's own
3744+
# dist is deliberately never built (turbo's `test` depends on `^build`,
3745+
# dependencies only), so there it declares a named skip. THIS lane builds
3746+
# the full packages closure above, so here the built dist is guaranteed —
3747+
# the flag turns "dist absent/stale" from a skip into a failure, which is
3748+
# what stops the pin from quietly degrading to never-measured if the
3749+
# build steps above are ever dropped (#4690). Sits with its family:
3750+
# `check:api-surface` / `check:skill-examples`, the other gates that read
3751+
# the surface a consumer actually installs. Adds no required context —
3752+
# a step in an existing lane (#9325).
3753+
- name: Root-entry type nameability pin (built dist, declaration emit)
3754+
env:
3755+
OS_EXPECT_ROOT_NAMEABILITY: '1'
3756+
run: pnpm --filter @objectstack/spec exec vitest run scripts/root-entry-type-nameability.pin.test.ts
3757+
37403758
# Same surface, the other axis: api-surface/ records that an export
37413759
# EXISTS, never what it resolves to — so four exported types sat at `any`
37423760
# across a whole major with every gate green (#4171). #4115 tells consumers

packages/metadata-protocol/src/protocol.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,23 @@ import {
7272
import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL, canonicalMetaUrlType, metaUrlSpellingRefusal, unrecognisedMetaTypeRefusal } from '@objectstack/spec/shared';
7373
import { applyConversionsToStoredItem, type ConversionNotice } from '@objectstack/spec';
7474
import { type FormView, isAggregatedViewContainer, expandViewContainer } from '@objectstack/spec/ui';
75+
// [#11350] Emitted-specifier pin. This module's inferred public declarations
76+
// structurally mention `FormFieldInput` (FormView `sections[].fields`), and
77+
// this file imports BOTH `@objectstack/spec` (root, for
78+
// `applyConversionsToStoredItem` above) and `@objectstack/spec/ui`. Once
79+
// #11350 made `FormFieldInput` nameable from the root entry, tsc's
80+
// declaration emitter switched its synthesized reference from the `/ui` slice
81+
// to the root — both are portable, but the root specifier drags spec's ENTIRE
82+
// root module graph into every downstream tsc program that reads this
83+
// package's dts (measured on PR #11716: +190k types, +805k instantiations,
84+
// +~560MB on the debt-ledger re-measure of @objectstack/http-conformance —
85+
// past a 4GB heap). An IMPORT (not a bare re-export — that creates no local
86+
// binding) makes the emitter reuse this binding, keeping the emitted
87+
// reference on the narrow `/ui` entry; the export statement is what keeps
88+
// no-unused-locals green. index.ts deliberately does not re-export it
89+
// (curated entry, unchanged).
90+
import { type FormFieldInput } from '@objectstack/spec/ui';
91+
export type { FormFieldInput };
7592
import { METADATA_FORM_REGISTRY, CORE_SERVICE_PROVIDER, serviceUnavailableMessage, inProcessServiceMessage } from '@objectstack/spec/system';
7693
import { DEFAULT_METADATA_TYPE_REGISTRY, getMetadataTypeSchema, getMetadataTypeActions, getMetadataCreateSeed, PROTOCOL_VERSION } from '@objectstack/spec/kernel';
7794
import {

packages/spec/api-surface/root.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"ExpressionSchema (const)",
5959
"F (const)",
6060
"FIELD_KEY_GUIDANCE (const)",
61+
"FormFieldInput (type)",
6162
"GUEST_POSITION (const)",
6263
"LintableAuthoringCollection (interface)",
6364
"MAP_SUPPORTED_FIELDS (const)",
@@ -78,6 +79,7 @@
7879
"MigrationHopResult (interface)",
7980
"MigrationStep (interface)",
8081
"MigrationTodo (interface)",
82+
"NavigationItemInput (type)",
8183
"NormalizeStackInputOptions (interface)",
8284
"OBJECT_KEY_GUIDANCE (const)",
8385
"ORGANIZATION_ADMIN (const)",
@@ -117,6 +119,7 @@
117119
"SpecSurfaceAddSchema (const)",
118120
"SpecSurfaceRemove (type)",
119121
"SpecSurfaceRemoveSchema (const)",
122+
"StateNodeConfig (type)",
120123
"StoredConversionOptions (type)",
121124
"SurfaceDiff (interface)",
122125
"TemplateExpressionInputSchema (const)",

packages/spec/export-origins/root.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"ExpressionSchema": "src/shared/expression.zod.ts#ExpressionSchema (const)",
5959
"F": "src/shared/expression.zod.ts#F (const)",
6060
"FIELD_KEY_GUIDANCE": "src/data/authoring-key-lint.ts#FIELD_KEY_GUIDANCE (const)",
61+
"FormFieldInput": "src/ui/view.zod.ts#FormFieldInput (type)",
6162
"GUEST_POSITION": "src/identity/position.zod.ts#GUEST_POSITION (const)",
6263
"LintableAuthoringCollection": "src/kernel/metadata-authoring-lint.ts#LintableAuthoringCollection (interface)",
6364
"MAP_SUPPORTED_FIELDS": "src/shared/metadata-collection.zod.ts#MAP_SUPPORTED_FIELDS (const)",
@@ -78,6 +79,7 @@
7879
"MigrationHopResult": "src/migrations/types.ts#MigrationHopResult (interface)",
7980
"MigrationStep": "src/migrations/types.ts#MigrationStep (interface)",
8081
"MigrationTodo": "src/migrations/types.ts#MigrationTodo (interface)",
82+
"NavigationItemInput": "src/ui/app.zod.ts#NavigationItemInput (type)",
8183
"NormalizeStackInputOptions": "src/shared/metadata-collection.zod.ts#NormalizeStackInputOptions (interface)",
8284
"OBJECT_KEY_GUIDANCE": "src/data/authoring-key-lint.ts#OBJECT_KEY_GUIDANCE (const)",
8385
"ORGANIZATION_ADMIN": "src/identity/eval-user.zod.ts#ORGANIZATION_ADMIN (const)",
@@ -117,6 +119,7 @@
117119
"SpecSurfaceAddSchema": "src/migrations/spec-changes.ts#SpecSurfaceAddSchema (const)",
118120
"SpecSurfaceRemove": "src/migrations/spec-changes.ts#SpecSurfaceRemove (type)",
119121
"SpecSurfaceRemoveSchema": "src/migrations/spec-changes.ts#SpecSurfaceRemoveSchema (const)",
122+
"StateNodeConfig": "src/automation/state-machine.zod.ts#StateNodeConfig (type)",
120123
"StoredConversionOptions": "src/conversions/stored.ts#StoredConversionOptions (type)",
121124
"SurfaceDiff": "src/migrations/spec-changes.ts#SurfaceDiff (interface)",
122125
"TemplateExpressionInputSchema": "src/shared/expression.zod.ts#TemplateExpressionInputSchema (const)",
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Root-entry nameability pin (#11350) — the consumer shape, verbatim.
5+
*
6+
* ## The invariant this pins
7+
*
8+
* A type that appears structurally in an entry's public declarations must be
9+
* nameable from that same entry (maintainer ruling 2026-08-23, recorded on
10+
* #11350). The measured breakage: `defineStack` returns
11+
* `ObjectStackDefinition`, declared `z.input<typeof
12+
* ObjectStackDefinitionSchema>` — a generic instantiation the declaration
13+
* emitter does not preserve as an alias — so an un-annotated
14+
* `export default defineStack(...)` is emitted as the STRUCTURAL expansion.
15+
* That expansion mentions `FormFieldInput` / `NavigationItemInput` /
16+
* `StateNodeConfig`, and until #11350 the root entry did not re-export them,
17+
* so tsc could only name them through the hash-named internal dist chunk that
18+
* physically declares them — unaddressable through the package's `exports`
19+
* map → TS2883 ("likely not portable") in every consumer inferring a type
20+
* through a root-entry function. Nine build-time configs hit it before the
21+
* first one was diagnosed (#10868).
22+
*
23+
* ## What each program proves
24+
*
25+
* - **consumer** — the repro's exact shape: an un-annotated
26+
* `export default defineStack(...)`, compiled with `declaration: true`
27+
* against the BUILT root entry, resolved the way a real consumer resolves it
28+
* (a `node_modules/@objectstack/spec` symlink + the package's own `exports`
29+
* map — the same physical resolution a pnpm workspace consumer performs;
30+
* measured on #11350: this program produced exactly 3 × TS2883 against the
31+
* pre-fix dist and 0 diagnostics against the fixed one). Asserted green.
32+
*
33+
* The program is two files on purpose, mirroring the real consumers: every
34+
* one of the nine i18n-extract configs' programs also contains its object
35+
* modules, which import `@objectstack/spec/data` — and #11350's control
36+
* measured that a program file importing a subpath entry makes that entry's
37+
* names NAMEABLE program-wide. `context.ts` reproduces that, which is what
38+
* scopes this pin to the ruled three (ui/automation names, reachable only
39+
* via the root re-exports under pin). Measured against this same dist: the
40+
* MINIMAL one-file program leaks two MORE names through `/data`
41+
* (`BaseValidationRuleShape`, `FilterCondition`) that the fixed root entry
42+
* still cannot name — deliberately NOT pinned here; that is #11350's
43+
* recorded premise delta, filed as #11709 for its own ruling. For the same
44+
* reason the program contains no `@objectstack/spec/ui` or `/automation`
45+
* import and no direct `import type { FormFieldInput, … }` — any of those
46+
* would mask the very symptom under pin. Direct existence of the three root
47+
* exports is owned by `api-surface/root.json` + `check:api-surface` instead.
48+
*
49+
* - **canary** — the anti-phantom probe. TS2883 is a DECLARATION-EMIT
50+
* diagnostic: drop `declaration: true` from the harness profile and the
51+
* consumer program goes green forever, regression or no regression — a gate
52+
* only ever observed green is indistinguishable from one that matches
53+
* nothing. The canary is a hermetic fixture whose only error is also
54+
* declaration-emit-only — TS4094, a private member on an exported anonymous
55+
* class type (measured: exit 2 with `declaration: true`, exit 0 without) —
56+
* so it stays red exactly as long as the harness keeps checking the axis
57+
* the pin lives on. Asserted red.
58+
*
59+
* ## Dist freshness — an environment-gated measurement, the live-dialect-cell
60+
* shape
61+
*
62+
* The subject under test is `dist/index.d.ts`, not `src/` — the same artifact
63+
* `check:api-surface` reads, judged by the same staleness rule (#7122/#7181):
64+
* a stale dist would let a root re-export removed from `src/index.ts` sit
65+
* green here until the next rebuild.
66+
*
67+
* But absence of that artifact is an ENVIRONMENT fact, not a defect: turbo's
68+
* `test` task depends on `^build` (dependencies only), so the Test Core lane
69+
* deliberately runs spec's own suite with spec's own dist unbuilt — a throw
70+
* here reds a whole CI shard for a measurement that lane was never equipped
71+
* to make (measured on PR #11716's first round: 420/421 files passed, only
72+
* this file failed, at the old beforeAll throw). So the pin follows
73+
* `live-dialect-matrix.testkit.ts`'s discipline — REPORTED, never omitted,
74+
* with no third outcome:
75+
*
76+
* - dist fresh → the two programs run, both modes.
77+
* - dist missing/stale, default → a NAMED SKIP whose title carries the
78+
* refusal reason ("it was not run" stays readable in the output). Never a
79+
* silent pass, never a throw.
80+
* - dist missing/stale under `OS_EXPECT_ROOT_NAMEABILITY=1` → a FAILURE
81+
* quoting the freshness refusal: that flag is set only by a runner that
82+
* declared it builds spec's dts first, so a skip there would be the pin
83+
* quietly degrading to never-measured — the #4690 shape.
84+
*
85+
* Where it runs for real in CI: the `Type Check · consumer gates` lane
86+
* (lint.yml `typecheck-consumers`) sets the flag right after its full
87+
* packages-closure builds, beside `check:api-surface` / `check:skill-examples`
88+
* — the other consumer-shaped gates that read the built dist.
89+
*/
90+
91+
import { spawnSync } from 'node:child_process';
92+
import fs from 'node:fs';
93+
import { createRequire } from 'node:module';
94+
import os from 'node:os';
95+
import path from 'node:path';
96+
import { fileURLToPath } from 'node:url';
97+
98+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
99+
100+
import { inspectDistFreshness } from './lib/dist-freshness';
101+
102+
const HERE = path.dirname(fileURLToPath(import.meta.url));
103+
const PKG_DIR = path.resolve(HERE, '..');
104+
const RERUN =
105+
'pnpm --filter @objectstack/spec test scripts/root-entry-type-nameability.pin.test.ts';
106+
107+
/** One tsc program = its fixture files + one tsconfig, in a shared sandbox. */
108+
interface Program {
109+
files: Record<string, string>;
110+
tsconfigName: string;
111+
}
112+
113+
const CONSUMER: Program = {
114+
files: {
115+
// The repro's shape, verbatim: un-annotated default export of a
116+
// root-entry inference. Any structural mention the declaration emitter
117+
// cannot name from within this program turns it red with the leaked name
118+
// in the output.
119+
'consumer.ts': `import { defineStack } from '@objectstack/spec';
120+
121+
export default defineStack({ objects: [] });
122+
`,
123+
// The real programs' shape: the configs' object modules import
124+
// `@objectstack/spec/data`, making /data's names nameable in-program
125+
// (#11350's control) — see the docblock for why this scopes the pin.
126+
'context.ts': `import type { Field } from '@objectstack/spec/data';
127+
128+
export type AuditObjectShape = { fields: Record<string, Field> };
129+
`,
130+
},
131+
tsconfigName: 'tsconfig.consumer.json',
132+
};
133+
134+
const CANARY: Program = {
135+
files: {
136+
// Declaration-emit-only error: TS4094, private member on an exported
137+
// anonymous class type. Runs the same compiler profile as the consumer
138+
// program; red here proves the profile still checks declaration emit.
139+
'canary.ts': `export const probe = new (class { private x = 1; })();
140+
`,
141+
},
142+
tsconfigName: 'tsconfig.canary.json',
143+
};
144+
145+
let sandbox = '';
146+
147+
function writeProgram(program: Program): void {
148+
for (const [name, source] of Object.entries(program.files)) {
149+
fs.writeFileSync(path.join(sandbox, name), source);
150+
}
151+
const tsconfig = {
152+
compilerOptions: {
153+
target: 'ES2022',
154+
module: 'NodeNext',
155+
moduleResolution: 'NodeNext',
156+
strict: true,
157+
// Load-bearing: TS2883 (and the canary's TS4094) exist only on the
158+
// declaration-emit axis. `noEmit` keeps the sandbox clean; tsc still
159+
// runs the declaration emitter's checks when `declaration` is on.
160+
declaration: true,
161+
noEmit: true,
162+
skipLibCheck: true,
163+
types: [],
164+
},
165+
include: Object.keys(program.files),
166+
};
167+
fs.writeFileSync(
168+
path.join(sandbox, program.tsconfigName),
169+
JSON.stringify(tsconfig, null, 2),
170+
);
171+
}
172+
173+
function runTsc(program: Program): { code: number; output: string } {
174+
const require = createRequire(import.meta.url);
175+
const tscBin = require.resolve('typescript/bin/tsc');
176+
const res = spawnSync(
177+
process.execPath,
178+
[tscBin, '--pretty', 'false', '-p', path.join(sandbox, program.tsconfigName)],
179+
{ cwd: sandbox, encoding: 'utf-8' },
180+
);
181+
return { code: res.status ?? 1, output: `${res.stdout ?? ''}${res.stderr ?? ''}` };
182+
}
183+
184+
/**
185+
* Read the environment ONCE, at collection time, exactly as
186+
* `live-dialect-matrix.testkit.ts` reads its cell URLs: the branch below is
187+
* total — measured when the dist is readable, a named skip or an expected-mode
188+
* failure when it is not — so there is no third outcome and no throw that
189+
* could red a lane never equipped to measure this.
190+
*/
191+
const EXPECT_BUILT_DIST = process.env.OS_EXPECT_ROOT_NAMEABILITY === '1';
192+
const FRESHNESS = inspectDistFreshness(PKG_DIR, 'check', RERUN);
193+
194+
if (!FRESHNESS.fresh) {
195+
describe('root-entry type nameability (#11350)', () => {
196+
it.skipIf(!EXPECT_BUILT_DIST)(
197+
`spec's dist declarations are ${FRESHNESS.state} — this pin reads the BUILT root entry; ` +
198+
`build @objectstack/spec first, then: ${RERUN} (skipped by default; ` +
199+
`OS_EXPECT_ROOT_NAMEABILITY=1 turns this into a failure)`,
200+
() => {
201+
expect.fail(
202+
`OS_EXPECT_ROOT_NAMEABILITY=1 while spec's dist declarations are ${FRESHNESS.state}: ` +
203+
`this runner declared it builds spec's dts before the suite, so this pin must not ` +
204+
`be skipped (a skip here would be the pin quietly degrading to never-measured, #4690).\n` +
205+
FRESHNESS.message,
206+
);
207+
},
208+
);
209+
});
210+
} else {
211+
describe('root-entry type nameability (#11350)', () => {
212+
beforeAll(() => {
213+
sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'os-root-nameability-'));
214+
// A real consumer's resolution, physically: a node_modules symlink into
215+
// the built package, so tsc walks the package's own `exports` map and
216+
// lands on `dist/index.d.ts` — the same realpath a pnpm workspace
217+
// symlink produces (the #11350 measurement fired TS2883 through exactly
218+
// this layout).
219+
const scope = path.join(sandbox, 'node_modules', '@objectstack');
220+
fs.mkdirSync(scope, { recursive: true });
221+
fs.symlinkSync(PKG_DIR, path.join(scope, 'spec'), 'dir');
222+
223+
writeProgram(CONSUMER);
224+
writeProgram(CANARY);
225+
});
226+
227+
afterAll(() => {
228+
if (sandbox) fs.rmSync(sandbox, { recursive: true, force: true });
229+
});
230+
231+
it('an un-annotated `export default defineStack(...)` declaration-emits clean against the built root entry', () => {
232+
const { code, output } = runTsc(CONSUMER);
233+
expect(
234+
code,
235+
`expected 0 diagnostics; a TS2883 naming a dist chunk means a type the root entry's ` +
236+
`public declarations mention structurally is no longer nameable from the root entry ` +
237+
`(re-export it from src/index.ts — see #11350). tsc said:\n${output}`,
238+
).toBe(0);
239+
expect(output).not.toMatch(/error TS\d+/);
240+
});
241+
242+
it('canary: the harness profile still checks the declaration-emit axis', () => {
243+
const { code, output } = runTsc(CANARY);
244+
expect(
245+
code,
246+
`the canary fixture's declaration-emit error disappeared — if the harness profile ` +
247+
`lost \`declaration: true\`, the consumer pin above is green no matter what leaks. ` +
248+
`tsc said:\n${output}`,
249+
).not.toBe(0);
250+
// Measured: TS4094 ("Property 'x' of exported anonymous class type may
251+
// not be private or protected"). Pin the TS4xxx declaration-emit family +
252+
// the message's substance rather than the bare number, so a
253+
// compiler-version renumbering does not false-red this line.
254+
expect(output).toMatch(/error TS4\d{2,3}: .*private/);
255+
});
256+
});
257+
}

0 commit comments

Comments
 (0)