diff --git a/.changeset/scaffold-emission-policy-one-definition.md b/.changeset/scaffold-emission-policy-one-definition.md new file mode 100644 index 0000000000..c62f323ba8 --- /dev/null +++ b/.changeset/scaffold-emission-policy-one-definition.md @@ -0,0 +1,18 @@ +--- +"@objectstack/cli": patch +--- + +`objectstack init` and `objectstack create` now read one emission policy instead of each restating it. + +Both commands write a `tsconfig.json` and a set of third-party dependency ranges into a new project. Each had written those in its own words, and the words had come apart. Measured on the tree: the TypeScript range — the value that decides whether a scaffolded project type-checks at all — was written in six places across three scaffolders and had split into three values (`^5.3.0`, `^5.8.0`, `^6.0.0`); the vitest range into two. Dated off `git log -G` as of 2026-09-05: the two CLI values were written in the same commit and stayed apart for 210 days, and the third value is 53 days old — the bundled template landed at `^5.3.0` like the others and was moved to `^6.0.0` later, in a commit that records no reasoning about TypeScript. + +The control for that reading was already in the same file: `SCAFFOLD_PNPM_RANGE` and `renderPnpmWorkspaceYaml()` are imported by the second scaffolder rather than restated, and across the same five emissions, the same window and the same authors, they had not drifted at all. So the policy moved to where those already live — `renderScaffoldTsconfig()` and one `SCAFFOLD_*_RANGE` constant per dependency, in `init.ts`, imported by `create.ts`. + +Two emitted values had to survive the merge, and both are argued rather than picked: + +- **TypeScript `^5.3.0`.** `TypeScript 5.3+` is already this project's published floor — `content/docs/getting-started/index.mdx` says so, and `content/docs/deployment/troubleshooting.mdx` repeats it. `^5.8.0` matched no statement anywhere, and `^5.3.0` was already what three of the five emissions carried. Measured rather than assumed: TypeScript 5.3.3 type-checks every shape these two commands emit with results identical to 6.0.3. +- **vitest `^4.0.0`.** Neither value was a recorded decision and both were written in the same commit; `^4.0.18` claimed a patch-level floor nothing justifies and was strictly the narrower of the two. + +**Nothing a scaffolded project installs changes.** `^5.3.0` and `^5.8.0` both resolve to typescript 5.9.3, and `^4.0.0` and `^4.0.18` both to vitest 4.1.11 — what moves is the floor each project declares, which is a support promise, so the surviving one is the promise the docs already make. Driving all five emissions and hashing the trees before and after: every `tsconfig.json` is byte-identical, `os init -t app` and `os init -t empty` are byte-identical in full, and exactly three `package.json` files change by exactly the one line each. + +`npx create-objectstack` is deliberately untouched. It cannot import from `@objectstack/cli` — the dependency edge runs the other way — and its `^6.0.0` is a different question: unifying it would change which major of TypeScript a scaffolded project installs. diff --git a/packages/cli/src/commands/create.ts b/packages/cli/src/commands/create.ts index 9189d2f740..250081090a 100644 --- a/packages/cli/src/commands/create.ts +++ b/packages/cli/src/commands/create.ts @@ -58,12 +58,18 @@ * * ## Why the standalone shape reuses `init`'s renderers * - * `renderPnpmWorkspaceYaml()` and `SCAFFOLD_PNPM_RANGE` are `init.ts`'s, and - * they are CALLED here rather than restated. A restatement is the two-producer - * defect `test/scaffold-workspace-consistency.test.ts` exists to catch, and it - * has already been paid for once in this repo: the build-approval block landed - * in one scaffold path and not the other, and one of them shipped the pre-fix - * shape for months. + * `renderPnpmWorkspaceYaml()`, `SCAFFOLD_PNPM_RANGE`, `renderScaffoldTsconfig()` + * and the `SCAFFOLD_*_RANGE` constants are `init.ts`'s, and they are CALLED here + * rather than restated. A restatement is the two-producer defect + * `test/scaffold-workspace-consistency.test.ts` exists to catch, and it has + * already been paid for twice in this repo: the build-approval block landed in + * one scaffold path and not the other, and one of them shipped the pre-fix shape + * for months; and the TypeScript range a scaffold installs was written in six + * places and split into three values — the two CLI values 210 days apart, the + * third 53 and recorded nowhere — on the value that decides whether the + * scaffold type-checks at all. The emission policy has + * one home now — see the block above `renderScaffoldPackageJson` in `init.ts` + * for the measurement and for which values survived. * * ## The pin * @@ -84,8 +90,16 @@ import { getCliVersion, NPM_PACKAGE_NAME_MAX_LENGTH, renderPnpmWorkspaceYaml, + renderScaffoldTsconfig, sanitizeNamespace, SCAFFOLD_PNPM_RANGE, + SCAFFOLD_TSCONFIG_INCLUDE_SRC_ONLY, + SCAFFOLD_TSCONFIG_INCLUDE_WITH_ROOT_CONFIG, + SCAFFOLD_TSX_RANGE, + SCAFFOLD_TYPES_NODE_RANGE, + SCAFFOLD_TYPESCRIPT_RANGE, + SCAFFOLD_VITEST_RANGE, + SCAFFOLD_ZOD_RANGE, validateProjectName, } from './init.js'; @@ -121,22 +135,6 @@ export function rootTsconfigExtends(inRepoDir: string, projectDirName: string): return `${'../'.repeat(depth)}tsconfig.json`; } -/** - * The compiler options a standalone scaffold carries in full, because it - * extends nothing. Deliberately the same set `objectstack init` writes: two - * scaffolders that disagree about `moduleResolution` is a support question - * nobody can answer, and `bundler` is what resolves the `exports` subpaths - * (`@objectstack/spec/contracts`, `/kernel`) the templates import. - */ -const STANDALONE_COMPILER_OPTIONS = { - target: 'ES2022', - module: 'ESNext', - moduleResolution: 'bundler', - strict: true, - esModuleInterop: true, - skipLibCheck: true, -} as const; - /** A rendered file: JSON objects are stringified on write, strings land as-is. */ type FileRenderer = (name: string) => unknown; @@ -253,26 +251,20 @@ export const templates: Record = { license: 'MIT', dependencies: { '@objectstack/spec': objectstackDependencySpec(placement), - zod: '^4.3.6', + zod: SCAFFOLD_ZOD_RANGE, }, devDependencies: { - '@types/node': '^22.0.0', - typescript: '^5.8.0', - vitest: '^4.0.0', + '@types/node': SCAFFOLD_TYPES_NODE_RANGE, + typescript: SCAFFOLD_TYPESCRIPT_RANGE, + vitest: SCAFFOLD_VITEST_RANGE, }, }), 'tsconfig.json': (name: string) => standalone - ? { - compilerOptions: { - ...STANDALONE_COMPILER_OPTIONS, - outDir: 'dist', - rootDir: 'src', - declaration: true, - }, - include: ['src/**/*'], - exclude: ['dist', 'node_modules'], - } + ? renderScaffoldTsconfig({ + rootDir: 'src', + include: SCAFFOLD_TSCONFIG_INCLUDE_SRC_ONLY, + }) : { extends: rootTsconfigExtends(PLUGIN_IN_REPO_DIR, `plugin-${name}`), compilerOptions: { @@ -367,13 +359,13 @@ MIT dependencies: { '@objectstack/spec': objectstackDependencySpec(placement), '@objectstack/cli': objectstackDependencySpec(placement), - zod: '^4.3.6', + zod: SCAFFOLD_ZOD_RANGE, }, devDependencies: { - '@types/node': '^22.0.0', - tsx: '^4.21.0', - typescript: '^5.8.0', - vitest: '^4.0.0', + '@types/node': SCAFFOLD_TYPES_NODE_RANGE, + tsx: SCAFFOLD_TSX_RANGE, + typescript: SCAFFOLD_TYPESCRIPT_RANGE, + vitest: SCAFFOLD_VITEST_RANGE, }, }), 'objectstack.config.ts': (name: string) => { @@ -445,16 +437,10 @@ ${ }`, 'tsconfig.json': (name: string) => standalone - ? { - compilerOptions: { - ...STANDALONE_COMPILER_OPTIONS, - outDir: 'dist', - rootDir: '.', - declaration: true, - }, - include: ['*.ts', 'src/**/*'], - exclude: ['dist', 'node_modules'], - } + ? renderScaffoldTsconfig({ + rootDir: '.', + include: SCAFFOLD_TSCONFIG_INCLUDE_WITH_ROOT_CONFIG, + }) : { extends: rootTsconfigExtends(EXAMPLE_IN_REPO_DIR, name), compilerOptions: { diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index d0e83bb76e..5837dda90a 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -219,6 +219,145 @@ export const SCAFFOLD_ALLOWED_PEER_VERSIONS: Record = { */ export const SCAFFOLD_PNPM_RANGE = '>=10.15'; +// ─── Shared emission policy ────────────────────────────────────────── +// +// The third-party ranges and the `tsconfig.json` an emitted project carries, +// declared ONCE for both scaffolders in this package. +// +// ## Why these are constants and not literals in each template +// +// Restating them is the defect. Measured on the tree, the TypeScript range a +// scaffolded project installs was written in SIX places across three +// scaffolders and had split into THREE values: +// +// os init `^5.3.0` (three write points in this file) +// os create `^5.8.0` (two write points in create.ts) +// create-objectstack `^6.0.0` (its bundled template's package.json) +// +// The split is not recent and nobody caused it deliberately. Read off `git +// log -G` over the three files, as of 2026-09-05: +// +// 338e68d2564 2026-02-07 `^5.3.0` and `^5.8.0` written in the SAME commit +// — 210 days apart and counting +// dbb54e12f0c 2026-05-25 the bundled template lands, also at `^5.3.0` +// — so this is still a TWO-value tree +// eaff01425b7 2026-07-14 that template's line moves `^5.3.0` → `^6.0.0` +// — the third value, 53 days old +// +// ⚠️ And the move that made the third value recorded no reasoning about +// TypeScript at all. #2907's commit message documents the `@objectstack/*` +// version sync and nothing else, and in that same one-file diff the five +// `@objectstack/*` ranges move `^6.0.0` → `^14.0.0` while the `typescript` +// line moves ONTO the `^6.0.0` those lines are vacating. Whatever the intent +// was, no statement of it exists — which is the point: an unrecorded value is +// what a restatement decays into, and it decayed on the value that decides +// whether a new project type-checks at all. +// +// The control for that reading is in this same file: `SCAFFOLD_PNPM_RANGE` +// and `renderPnpmWorkspaceYaml()` are IMPORTED by the other scaffolder rather +// than restated, and they have not drifted across any of the five emissions. +// Same files, same authors, same window — the restated values split, the +// imported ones did not. That is the whole argument for this block. +// +// ## The surviving values, and why they are these +// +// `^5.3.0` over `^5.8.0`: **`TypeScript 5.3+` is already a recorded decision** +// on two live doc pages — `content/docs/getting-started/index.mdx` ("ObjectStack +// works with TypeScript 5.3+, but the project itself is built and tested +// against TypeScript 6.x") and `content/docs/deployment/troubleshooting.mdx` +// ("TypeScript 5.3.0 or later for full type inference support"). `^5.8.0` +// matches no statement anywhere. It is also the value three of the five +// emissions already carried, and it is measured rather than assumed: TypeScript +// 5.3.3 type-checks every shape these two commands emit with results identical +// to 6.0.3 (measured against this repo's own `@objectstack/spec` build, on the +// `skipLibCheck` configuration the scaffold actually emits). +// +// `^4.0.0` over `^4.0.18`: no recorded decision exists for either, both were +// written in the same 2026-02-07 commit, and `^4.0.18` claims a PATCH-level +// floor nothing justifies while being strictly the narrower of the two. +// +// ⚠️ Neither choice changes what a scaffolded project INSTALLS. Measured +// against the registry: `^5.3.0` and `^5.8.0` both resolve to typescript +// 5.9.3, and `^4.0.0` and `^4.0.18` both resolve to vitest 4.1.11. What +// changes is the floor each project DECLARES — and a floor is a support +// promise, so the one that survives is the one the docs already make. +// +// ⛔ `create-objectstack`'s `^6.0.0` is deliberately NOT unified here. That +// package cannot import from `@objectstack/cli`: the dependency edge already +// runs the other way (`create-objectstack` is a `workspace:*` dependency of +// this package, and this file imports its `created-summary` renderer), so a +// reverse import is a cycle — and it publishes as a two-dependency `npx` +// package that must not pull the CLI's ~50-package closure. Its emission is +// also a committed template file copied byte-for-byte, with no renderer to +// route through a constant. Unifying it would move a scaffolded project from +// TypeScript 6.0.3 to 5.9.3, which is a user-visible change and a support +// decision, not a refactor. + +/** The TypeScript range every scaffolded project declares. */ +export const SCAFFOLD_TYPESCRIPT_RANGE = '^5.3.0'; + +/** The vitest range a scaffolded project declares when its template tests. */ +export const SCAFFOLD_VITEST_RANGE = '^4.0.0'; + +/** The `@types/node` range a scaffolded project declares. */ +export const SCAFFOLD_TYPES_NODE_RANGE = '^22.0.0'; + +/** The `tsx` range a scaffolded project declares when its scripts need it. */ +export const SCAFFOLD_TSX_RANGE = '^4.21.0'; + +/** The zod range a scaffolded project declares when it authors schemas. */ +export const SCAFFOLD_ZOD_RANGE = '^4.3.6'; + +/** + * The compiler options every STANDALONE scaffold carries in full, because it + * extends nothing. + * + * `bundler` is what resolves the `exports` subpaths (`@objectstack/spec/data`, + * `/contracts`, `/kernel`) the templates import; two scaffolders that disagree + * about `moduleResolution` is a support question nobody can answer. The + * `--in-repo` placement of `os create` does NOT use these — it inherits its + * module semantics from the repo config it extends. + */ +export const SCAFFOLD_TSCONFIG_COMPILER_OPTIONS = { + target: 'ES2022', + module: 'ESNext', + moduleResolution: 'bundler', + strict: true, + esModuleInterop: true, + skipLibCheck: true, +} as const; + +/** + * Render the `tsconfig.json` a standalone scaffold receives. + * + * `rootDir` and `include` are the only things the emitted shapes differ on: + * `os create plugin` compiles `src/` alone, while `os init`'s three templates + * and `os create example` also compile the `objectstack.config.ts` at the + * project root. Measured before this renderer existed, four of the five + * emitted `tsconfig.json` files were already byte-identical and the fifth + * differed only in those two keys — so nothing here is a new decision. + */ +export function renderScaffoldTsconfig( + options: { rootDir: string; include: string[] }, +): Record { + return { + compilerOptions: { + ...SCAFFOLD_TSCONFIG_COMPILER_OPTIONS, + outDir: 'dist', + rootDir: options.rootDir, + declaration: true, + }, + include: options.include, + exclude: ['dist', 'node_modules'], + }; +} + +/** `include` for a scaffold whose root `objectstack.config.ts` is compiled too. */ +export const SCAFFOLD_TSCONFIG_INCLUDE_WITH_ROOT_CONFIG = ['*.ts', 'src/**/*']; + +/** `include` for a scaffold that compiles `src/` alone. */ +export const SCAFFOLD_TSCONFIG_INCLUDE_SRC_ONLY = ['src/**/*']; + /** * Render the `package.json` written into a freshly scaffolded project. * @@ -381,7 +520,7 @@ export const TEMPLATES: Record | undefined): Array<[string, string]> { + return Object.entries(deps ?? {}) + .filter(([name, range]) => !name.startsWith('@objectstack/') && typeof range === 'string') + .map(([name, range]) => [name, range as string]); +} + +/** + * Every `package.json` the two commands emit for the shape a reader of the docs + * actually gets — `os init`'s three templates and `os create`'s two, in its + * DEFAULT placement. `--in-repo` is excluded on purpose: it emits `workspace:*` + * and is documented as platform-work-only. + */ +function emittedManifests(): Array<{ id: string; manifest: Record }> { + const out: Array<{ id: string; manifest: Record }> = []; + for (const [key, template] of Object.entries(TEMPLATES)) { + out.push({ + id: `os init -t ${key}`, + manifest: renderScaffoldPackageJson(PROBE_NAME, template), + }); + } + for (const [key, template] of Object.entries(templates)) { + const render = template.filesFor(DEFAULT_PLACEMENT)['package.json']; + out.push({ + id: `os create ${key}`, + manifest: render(PROBE_NAME) as Record, + }); + } + return out; +} + +/** ` -> every range any emission declares for it`. */ +function declaredRanges(): Map> { + const byName = new Map>(); + for (const { id, manifest } of emittedManifests()) { + const deps = [ + ...thirdPartyOnly(manifest.dependencies as Record), + ...thirdPartyOnly(manifest.devDependencies as Record), + ]; + for (const [name, range] of deps) { + const ranges = byName.get(name) ?? new Map(); + ranges.set(range, [...(ranges.get(range) ?? []), id]); + byName.set(name, ranges); + } + } + return byName; +} + +describe('scaffold emission policy — one definition, five emissions', () => { + it('harvests a non-empty policy from all five emissions (control)', () => { + // Without this, every assertion below passes over an empty harvest — the + // vacuity that would make the whole file certify the defect it exists for. + const manifests = emittedManifests(); + expect(manifests.map((m) => m.id).sort()).toEqual([ + 'os create example', + 'os create plugin', + 'os init -t app', + 'os init -t empty', + 'os init -t plugin', + ]); + const names = [...declaredRanges().keys()]; + expect(names).toContain('typescript'); + expect(names).toContain('vitest'); + expect(names.length).toBeGreaterThanOrEqual(4); + }); + + it('declares exactly one range per third-party dependency', () => { + const disagreements: string[] = []; + for (const [name, ranges] of declaredRanges()) { + if (ranges.size === 1) continue; + const detail = [...ranges] + .map(([range, emissions]) => `${range} (${emissions.join(', ')})`) + .join(' vs '); + disagreements.push(`${name}: ${detail}`); + } + expect( + disagreements, + 'these dependency names are restated with different ranges by different ' + + 'scaffolders — declare the range once in init.ts and import it', + ).toEqual([]); + }); + + it('emits the exported constant rather than a literal, for every policy range', () => { + const ranges = declaredRanges(); + const expected: Array<[string, string]> = [ + ['typescript', SCAFFOLD_TYPESCRIPT_RANGE], + ['vitest', SCAFFOLD_VITEST_RANGE], + ['@types/node', SCAFFOLD_TYPES_NODE_RANGE], + ['tsx', SCAFFOLD_TSX_RANGE], + ['zod', SCAFFOLD_ZOD_RANGE], + ]; + for (const [name, constant] of expected) { + expect([...(ranges.get(name)?.keys() ?? [])], name).toEqual([constant]); + } + }); +}); + +describe('the surviving TypeScript range is the floor the docs already state', () => { + /** `TypeScript 5.3+` / `TypeScript 5.3.0 or later`, normalised to `major.minor`. */ + function statedFloors(file: string): string[] { + const text = readFileSync(file, 'utf8'); + const out: string[] = []; + const re = /TypeScript (\d+)\.(\d+)(?:\.\d+)?(?:\+| or later)/g; + for (const m of text.matchAll(re)) out.push(`${m[1]}.${m[2]}`); + return out; + } + + it('finds a stated floor on both pages (control)', () => { + // A regex that matched nothing would make the case below assert `[] === []`. + expect(statedFloors(GETTING_STARTED).length).toBeGreaterThan(0); + expect(statedFloors(TROUBLESHOOTING).length).toBeGreaterThan(0); + }); + + it('agrees with what the scaffolders emit', () => { + const floors = new Set([...statedFloors(GETTING_STARTED), ...statedFloors(TROUBLESHOOTING)]); + expect([...floors], 'the two pages state different TypeScript floors').toHaveLength(1); + const [floor] = [...floors]; + expect( + SCAFFOLD_TYPESCRIPT_RANGE, + 'the emitted range and the documented floor have to be the same promise — ' + + 'change both together, or neither', + ).toBe(`^${floor}.0`); + }); +}); + +describe('the emitted tsconfig.json comes from the shared renderer', () => { + function runCli(args: string[], cwd: string): Promise<{ code: number; stderr: string }> { + return new Promise((done) => { + execFile( + TSX, + [CLI, ...args], + { cwd, maxBuffer: 8 * 1024 * 1024, env: childEnv({ NO_COLOR: '1' }) }, + (err, _stdout, stderr) => { + done({ + code: err + ? typeof (err as { code?: unknown }).code === 'number' + ? (err as unknown as { code: number }).code + : 1 + : 0, + stderr: String(stderr), + }); + }, + ); + }); + } + + /** Everything but the two keys the emitted shapes legitimately differ on. */ + function base(tsconfig: Record): Record { + const options = { ...(tsconfig.compilerOptions as Record) }; + delete options.rootDir; + return options; + } + + it( + 'os init writes exactly what renderScaffoldTsconfig() returns, and os create shares its base', + { timeout: RUN_TIMEOUT_MS }, + async () => { + const sandbox = mkdtempSync(join(tmpdir(), 'emission-policy-')); + try { + const run = await runCli(['init', PROBE_NAME, '-t', 'app', '--no-install'], sandbox); + expect(run.code, run.stderr).toBe(0); + + // The emission really happened — an absent or empty directory would let + // every comparison below run over nothing. + const projectDir = join(sandbox, PROBE_NAME); + expect(readdirSync(projectDir).length).toBeGreaterThan(1); + + const emitted = readFileSync(join(projectDir, 'tsconfig.json'), 'utf8'); + const rendered = renderScaffoldTsconfig({ + rootDir: '.', + include: SCAFFOLD_TSCONFIG_INCLUDE_WITH_ROOT_CONFIG, + }); + expect(emitted).toBe(`${JSON.stringify(rendered, null, 2)}\n`); + + // `os create`'s standalone tsconfigs measured against the bytes `os + // init` actually wrote, not against a transcription of either. + const emittedOptions = base(JSON.parse(emitted) as Record); + for (const [key, template] of Object.entries(templates)) { + const render = template.filesFor(DEFAULT_PLACEMENT)['tsconfig.json']; + const created = render(PROBE_NAME) as Record; + expect(base(created), `os create ${key}`).toEqual(emittedOptions); + } + } finally { + rmSync(sandbox, { recursive: true, force: true }); + } + }, + ); +}); diff --git a/scripts/check-ci-filter-parity.mjs b/scripts/check-ci-filter-parity.mjs index 26563d289c..a9f229d976 100644 --- a/scripts/check-ci-filter-parity.mjs +++ b/scripts/check-ci-filter-parity.mjs @@ -680,14 +680,20 @@ export async function selfTest() { // @objectstack/cli: `os create`'s emitted plugin shape is stated nowhere but // its documentation, so the pin holding the template to it reads all three // pages, each covered only through the `content/**` root #10015 added. - // Ten plus one plus two plus one plus one plus one plus three: the rollback - // now uncovers nineteen. This pin is judged over the LIVE declaration table on - // purpose: a declaration added under a root the rollback keeps leaves the - // count alone, one under a new root moves it and is recorded here by name. + // Plus, since #15818, the two doc pages THAT card declared for the same + // package: the TypeScript floor both scaffolders emit was chosen because those + // two pages already promise it, so the pin holding the emitted range to that + // promise reads both -- and each is covered only through the same `content/**` + // root, exactly like #14824's three. + // Ten plus one plus two plus one plus one plus one plus three plus two: the + // rollback now uncovers twenty-one. This pin is judged over the LIVE + // declaration table on purpose: a declaration added under a root the rollback + // keeps leaves the count alone, one under a new root moves it and is recorded + // here by name. const preFix = judge(fixtureWorkflow({ core: real.filters?.core, crosspkg: ['scripts/**'] }), CROSS_PACKAGE_TEST_INPUTS); assert( - new Set(uncoveredGlobs(preFix)).size === 19, - `rolling \`crosspkg\` back to its pre-#10015 list uncovers the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one plus #14561's one plus #14824's three -- got ${new Set(uncoveredGlobs(preFix)).size}`, + new Set(uncoveredGlobs(preFix)).size === 21, + `rolling \`crosspkg\` back to its pre-#10015 list uncovers the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one plus #14561's one plus #14824's three plus #15818's two -- got ${new Set(uncoveredGlobs(preFix)).size}`, ); assert( uncoveredGlobs(preFix).includes('skills/**'), @@ -723,6 +729,15 @@ export async function selfTest() { `-- and #14824 added the \`os create plugin\` scaffold-listing page ${page}, by name`, ); } + for (const page of [ + 'content/docs/deployment/troubleshooting.mdx', + 'content/docs/getting-started/index.mdx', + ]) { + assert( + uncoveredGlobs(preFix).includes(page), + `-- and #15818 added the TypeScript-floor page ${page}, by name`, + ); + } // ── (7) WIRING: the gate and its self-test really run in CI ────────────── battery('(7) WIRING: the gate and its self-test really run in CI'); @@ -794,7 +809,7 @@ export async function selfTest() { `same-root-different-file case observed failing and then covered by naming the file, a glob covered by ` + `\`core\`, one covered only by \`crosspkg\` and one covered by neither judged separately in one table, the ` + `stale-entry direction, seven refusals over subjects that could not be read, the checked-in ci.yml, the ` + - `pre-#10015 rollback uncovering the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one plus #14561's one plus #14824's three, ` + + `pre-#10015 rollback uncovering the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one plus #14561's one plus #14824's three plus #15818's two, ` + `and the CI wiring read out of lint.yml.`, ); selfTestReachedVerdict = true; diff --git a/scripts/cross-package-test-inputs.mjs b/scripts/cross-package-test-inputs.mjs index 065e771576..5cd177ba21 100644 --- a/scripts/cross-package-test-inputs.mjs +++ b/scripts/cross-package-test-inputs.mjs @@ -291,6 +291,17 @@ export const CROSS_PACKAGE_TEST_INPUTS = { // merge queue would be the first signal -- the shape the three e2e pages // above were declared for. // + // The two pages added for #15818 are read by + // test/scaffold-emission-policy.e2e.test.ts, which holds the TypeScript range + // BOTH scaffolders emit equal to the floor those pages promise a reader + // ("ObjectStack works with TypeScript 5.3+", "TypeScript 5.3.0 or later"). + // That promise is what settled which of three restated ranges survived the + // extraction, so the pin is the only thing that keeps the emitted value and + // the documented one from parting again. Same both-ways coupling as the + // #14824 trio: a range moved in `init.ts` must redden the pages that still + // promise the old floor, and a page rewritten to a new floor must redden + // until the scaffolders follow. + // // `connector-mcp-plugin.ts` is read by test/serve-capability-identity.test.ts, // which pins that the connector still registers the name the #7652 repro uses // rather than importing the class. It surfaced with the three above and has the @@ -349,6 +360,8 @@ export const CROSS_PACKAGE_TEST_INPUTS = { 'examples/app-showcase/src/ui/pages/task-triage.page.ts', 'content/docs/deployment/cli.mdx', 'content/docs/deployment/index.mdx', + 'content/docs/deployment/troubleshooting.mdx', + 'content/docs/getting-started/index.mdx', 'content/docs/permissions/authentication.mdx', 'content/docs/plugins/index.mdx', 'content/docs/protocol/kernel/index.mdx', diff --git a/turbo.json b/turbo.json index a89f287450..277117613b 100644 --- a/turbo.json +++ b/turbo.json @@ -109,6 +109,8 @@ "$TURBO_ROOT$/examples/app-showcase/src/ui/pages/task-triage.page.ts", "$TURBO_ROOT$/content/docs/deployment/cli.mdx", "$TURBO_ROOT$/content/docs/deployment/index.mdx", + "$TURBO_ROOT$/content/docs/deployment/troubleshooting.mdx", + "$TURBO_ROOT$/content/docs/getting-started/index.mdx", "$TURBO_ROOT$/content/docs/permissions/authentication.mdx", "$TURBO_ROOT$/content/docs/plugins/index.mdx", "$TURBO_ROOT$/content/docs/protocol/kernel/index.mdx",