Skip to content

Commit 2d3d1c9

Browse files
claude[bot]claude
andauthored
test(cli): hold content/docs/deployment/cli.mdx's two enumerations to their declarations (#18206)
Fixes #17723 Clause-②: no No `packages/spec` contract, no published schema and no declared API shape moves here; the diff is one new test file under `packages/cli/test/`, and both declarations it reads are imported rather than changed. ## What the card asked for, and what this is not The page is **correct today** — PR #17722 documented all eleven `os lint` flags, all three scripts and the `--manifest-id` condition accurately. This card is not "fix the page"; it is that **the next drift has nothing to stop it**. So this PR changes no prose and adds an instrument. `content/docs/deployment/cli.mdx` states two enumerations whose sources live in code, and nothing read either one. Three measured drifts, all under-inclusive, all repaired by hand: #8965, #16892 item ① (the scaffolded-scripts sentence said "these" of three commands and named two npm scripts), and #16892 item ② (`os lint` declares eleven flags; the page documented four — including `--include-platform`, which the command names in its own hint). ## The two sources the page is now bound to, and that both are machine-enumerable | Page enumeration | Source it is held to | Machine-enumerable? | |---|---|---| | The `**Options.**` table under `#### os lint` (eleven flags + one positional) | `Lint.flags` / `Lint.args` in `packages/cli/src/commands/lint.ts`, **imported** | Yes — oclif object literals, read as `Object.keys` of the imported class | | The scaffolded-wiring callout (three `os init` templates, four scaffolders, three npm scripts) | `TEMPLATES` in `packages/cli/src/commands/init.ts` plus `packages/create-objectstack/src/templates/blank/package.json`, **imported / read** | Yes — a map whose keys are the template names, and four script maps | Neither source is transcribed and neither is parsed out of source text. The script list is **derived**, not listed: it is the set of scripts that every scaffolder declares **and** whose command line runs the ObjectStack CLI. Today that is exactly `build`, `validate`, `lint` — `typecheck` (`tsc --noEmit`) is declared by all four and is not CLI-invoking; `dev`, `start` and `test` are CLI-invoking in some scaffolders and not all. A fourth such script reddens the sentence that omits it. ## Why a pin and not a generator The standing repair order puts a check last, so generation (the card's route A) was measured against what the table carries before being set aside. The page's rows are **not** the declarations' `description` strings: `--json` is declared "Output as JSON" and documented "Output as JSON, for CI. The verdict fields are described below"; the `--include-platform` row names the hint that sends a reader there; `config` links to the auto-detection section of the same page. Generation reaches the table's **membership** and none of that text, and pushing the text into the declarations would put doc-site cross-references into `--help`. Membership is also exactly what drifted all three times — not one of the three was a wrong description. So the residue generation cannot reach is empty, and the membership it can reach is held here instead. ## Why a vitest pin and not a root `scripts/check-*.mjs` Every sibling `check:` gate is invoked by its own step in `.github/workflows/lint.yml`, and `.github/workflows/**` is out of scope for this seat. The in-surface route is the one this repo already built for exactly this coupling: `packages/cli` declares `content/docs/deployment/cli.mdx` and `packages/create-objectstack/src/templates/blank/package.json` as cross-package test inputs (`scripts/cross-package-test-inputs.mjs`, mirrored into `turbo.json`), `ci.yml`'s `crosspkg` filter carries `content/**`, and the `test` job's `--union-into` step pulls the declaring package back into the affected set. So a **docs-only** PR runs this pin, which is the failure mode it exists for — and no workflow diff is needed. Both paths were already declared; this PR adds none. ## What it does not cover, stated so the gate's own reason stays true - The `--manifest-id` **condition** (#16892 item ③) and every statement of the form "X applies when Y". A condition is not an enumeration; there is no set to compare it with. - The eight-line `os lint` example fence above the table — a sample, not a claim of totality. - Row **text**. Membership only, for the reason above. - Any other page. It also does not duplicate `packages/cli/test/scaffold-ci-script-parity.test.ts`, which holds the scaffolders to each other and to the workflow the on-ramp ships — the source side, green through all three drifts, and unable to see a sentence. Nor does it extend `scripts/check-cli-examples-parity.mjs`: `os lint` declares no `examples`, and a flag need not appear in any example, so that gate is structurally blind to a missing flag. ## Firing controls and the red/green ablation Every comparison here can pass by reading nothing, so each parser is asserted non-empty before it is asserted equal, the governed-command roster carries a floor (`lint`), and an `**Options.**` table for a command the file does not bind is itself a failure — a new table cannot arrive unheld. Four mutations of `content/docs/deployment/cli.mdx`, each reproducing a shape the card measured. Every mutation was proven on disk by a `grep -o | wc -l` delta **and** a blob-hash change before its reading was taken; every restore was proven by `git hash-object` equal to the `HEAD` blob plus an empty `git diff HEAD`, never by an exit code. `HEAD` blob for the page: `9a34d83634a7a401b96c1260812ba3cd91febf66` | Ablation | On-disk proof | Pin exit | Case that fired | |---|---|---|---| | (green, unmutated) | — | **0** — 13 passed | — | | A · delete the `--include-platform` row (#16892 ② shape) | `--include-platform` 3 to 1; blob to `8e857878` | **1** — 1 failed, 12 passed | `os lint` documents exactly the flags and positionals it declares | | B · drop `npm run lint` from the callout (#16892 ① shape) | `npm run lint` 1 to 0; blob to `597e12c9` | **1** — 1 failed, 12 passed | names exactly the CLI scripts every scaffolder declares | | C · drop the `empty` template from the list (#16350 shape) | backticked `empty` 3 to 2; blob to `ef8ffca2` | **1** — 1 failed, 12 passed | names exactly the `os init` templates that exist | | D · add an `**Options.**` table under `os info`, unbound | `**Options.**` 1 to 2; blob to `977644ae` | **1** — 1 failed, 12 passed | no command documents an options table this pin does not bind | | (green again, restored) | blob back to `9a34d83634a7a401b96c1260812ba3cd91febf66` | **0** — 13 passed | — | The mutations are a one-time proof; no mutated bytes and no ablation harness are in this diff. ## Acceptance notes - `--generator`'s documented row and its declared `description` both say "Requires `--eval`", and the runtime enforces it (#15550). Noted while reading, unchanged, and out of scope here. - The `**Options.**` shape exists on exactly one section of this 2109-line page today. The pin's roster refusal is what makes the second one arrive held rather than unheld. ## Second commit: the declaration the pin's own header forced `check:cross-package-test-inputs` reads a test's source for quoted paths without parsing, so the pin's header naming `scripts/check-cli-examples-parity.mjs` made that path an undeclared cross-package input for `@objectstack/cli` — the gate went red on it. Settled the way the four `check-nul-bytes.mjs`-shaped entries above it in the roster are settled, and for the reason that roster records: declaring one rarely touched file is cheaper than rewording prose to dodge a scanner, and over-collection can only widen a re-run radius, never narrow one. Mirrored into `turbo.json`'s `@objectstack/cli#test` inputs, as every declared glob must be. The alternative — rewording the header so the scanner cannot see the path — would have made the gate's own stated reason vaguer to satisfy a scanner, which is the direction this repo refuses. ## Verification All readings at `3492199df`, working tree clean. Exit codes captured to disk before any pipe. **Gate sweep.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived **68** commands (up from 50 before the `turbo.json` / roster edits — a new declaration widens the derived set, which is correct). All 68 exit **0**. Reconciled: `dispatch-gates --ran` reports *"68 derived famil(ies) accounted for — 68 run, 0 NOT-MEASURED (a DERIVED zero — all 68 recorded an exit code and none of them is 3)"*. Two were red on the first sweep and both were fixed, not excused: - `check:cross-package-test-inputs` exit **1** — the undeclared path above. Green after the second commit. - `check:dual-build-cjs-loads` exit **3** — `PREREQUISITE NOT MET`, no `dist/` on 12 packages. Not a failure and not a pass: `pnpm build` (73 tasks, 0 failed), then the **whole** sweep re-run. **Build.** `pnpm --filter '@objectstack/cli^...' build` exit 0 (dependency closure), then `pnpm build --concurrency=2` exit 0 — 73 successful, 73 total. **Tests.** `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2` — **207 files / 2971 tests passed**. The `integration` tier is declared to CI: nothing in this diff touches a spawn entry, `test/helpers/serve-process.ts` or a driver/kernel boot path, and the new file carries none of the tier predicate's signals, so it lands in `unit` (`test/vitest-tiers-partition.test.ts` is inside that green run and would red if it landed in both tiers or neither). **Typecheck.** `pnpm --filter @objectstack/cli typecheck` exit 0 — `tsc --noEmit` plus `check:test-typecheck` (*"@objectstack/cli's test layer compiles under packages/cli/tsconfig.test.json"*, debt ledger unmoved at 3 files / 28 errors / 6 pinned signatures). **Lint.** `eslint . --no-inline-config --format json` was run over the **whole repo**, not narrowed: **6754 files linted, 0 errors, 0 warnings**, exit 0. The file count is read from eslint's own JSON output, and the population is eslint's own config resolution rather than a path list of mine. **changeset: `skip-changeset`**, measured rather than asserted. `packages/cli`'s publish surface is `files: ["dist", "README.md", "CHANGELOG.md"]`. After `pnpm --filter @objectstack/cli build`, grepping those three paths: `SCAFFOLDED_EVERYWHERE` 0 hits, `docs-cli-enumeration-parity` 0 hits, `GOVERNED_FLOOR` 0 hits — against a positive control from the same paths, `include-platform` 33 hits and `eval-min` 6 hits. `scripts/` and `turbo.json` are root files in no package's `files[]`. Nothing published moves. Label applied and read back: `size/m`, `tests`, `skip-changeset`. --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ea0b24a commit 2d3d1c9

3 files changed

Lines changed: 347 additions & 0 deletions

File tree

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* PIN — `content/docs/deployment/cli.mdx`'s two ENUMERATIONS against the
5+
* declarations they are about (#17723).
6+
*
7+
* ## The record this exists for, and what it is NOT
8+
*
9+
* The page is correct today. This pin is not the repair of a wrong page; it is
10+
* what keeps a right page right. Three times an enumeration on it fell behind
11+
* the code it enumerates, always in the same direction — UNDER-INCLUSIVE — and
12+
* every one of the three was repaired by hand:
13+
*
14+
* #8965 an earlier under-inclusive drift on this page.
15+
* #16892 ① the scaffolded-scripts sentence said "these" of THREE commands
16+
* and named TWO npm scripts. Under-inclusive from #16330, and
17+
* for all four scaffolders from #16350 / PR #16888.
18+
* #16892 ② `os lint` declares ELEVEN flags; the page documented FOUR.
19+
* One of the seven missing was `--include-platform`, which the
20+
* command NAMES IN ITS OWN HINT — so a reader following that
21+
* hint arrived at a page that did not mention the flag.
22+
*
23+
* Hand repair is what produces `4 of 11` in the first place. Nothing read
24+
* either enumeration, so nothing could fail.
25+
*
26+
* ## Why a pin and not a generator
27+
*
28+
* The standing repair order puts a check LAST: delete the construct that lets
29+
* the error happen, make the correct form the only spelling, and only then add
30+
* a check. Generating the options table would be that second step — and it was
31+
* MEASURED against what the table carries, row by row, before being set aside:
32+
* the page's rows are not the declarations' `description` strings. `--json` is
33+
* declared as "Output as JSON" and documented as "Output as JSON, for CI. The
34+
* verdict fields are described below"; the `--include-platform` row names the
35+
* hint that sends a reader there; `config` links to the auto-detection section
36+
* of the same page. Generation reaches the MEMBERSHIP of the table and none of
37+
* that text, and pushing the text into the declarations would put doc-site
38+
* cross-references into `--help`. Membership is also exactly what drifted all
39+
* three times — not one of the three was a wrong description. So the residue
40+
* generation cannot reach is empty, and what it CAN reach is held here instead.
41+
*
42+
* ## The two bindings, and that both sources are machine-enumerable
43+
*
44+
* 1. The `**Options.**` table under `#### \`os lint\`` is held to
45+
* `Lint.flags` and `Lint.args` — the oclif declarations, IMPORTED, not
46+
* transcribed and not parsed out of the source text. A flag that is added,
47+
* removed or renamed moves this pin with it, and a declaration that is
48+
* MOVED is a module-resolution failure rather than an empty derived set.
49+
* 2. The scaffolded-wiring callout is held to `TEMPLATES` (the `os init`
50+
* template maps) and to the `create-objectstack` blank template's own
51+
* `package.json`. The documented script list is compared with the scripts
52+
* that EVERY scaffolder declares AND that invoke the ObjectStack CLI —
53+
* derived, so a fourth such script reddens the sentence that omits it.
54+
* `typecheck` (`tsc --noEmit`) is declared by all four and is not one;
55+
* `dev`, `start` and `test` invoke the CLI in some scaffolders and not
56+
* all. Today that derivation is exactly `build`, `validate`, `lint`.
57+
*
58+
* ## The half this does NOT duplicate, stated so the reason stays true
59+
*
60+
* `packages/cli/test/scaffold-ci-script-parity.test.ts` holds the SCAFFOLDERS
61+
* to each other and to the workflow the on-ramp ships. That is the source side,
62+
* and it was already green through all three drifts — it cannot see a sentence.
63+
* This file holds the PAGE to that same source. The gap #17723 records is
64+
* one-sided, and this is the side of it.
65+
*
66+
* `scripts/check-cli-examples-parity.mjs` holds this page's `os package
67+
* publish` EXAMPLE block to the examples the command declares. An example set
68+
* and a flag set are different populations — `os lint` declares no `examples`
69+
* at all, and a flag need not appear in any example — so that gate is
70+
* structurally blind to a missing flag and extending it would not have been
71+
* the cheap win it looks like.
72+
*
73+
* ## What this pin does NOT cover
74+
*
75+
* - The `--manifest-id` CONDITION (#16892 item ③) and every other statement
76+
* of the form "X applies when Y". A condition is not an enumeration; there
77+
* is no set to compare it with, and claiming otherwise would make this
78+
* file's stated reason false.
79+
* - The eight-line `os lint` example fence above the table. It is a sample,
80+
* not a claim of totality, and holding it to the declaration would force
81+
* every flag into an example.
82+
* - Row TEXT. See "Why a pin and not a generator" — membership only.
83+
* - Any other page. This file is about `content/docs/deployment/cli.mdx`.
84+
*
85+
* ## Firing controls
86+
*
87+
* Every comparison here can pass by reading nothing: an anchor that stops
88+
* matching yields an empty documented set, an empty derived set compares clean
89+
* with it, and the run is green. So each parser is asserted NON-EMPTY before it
90+
* is asserted EQUAL, the governed-command roster carries a floor, and an
91+
* `**Options.**` table for a command this file does not bind is itself a
92+
* failure — a new table cannot arrive unheld.
93+
*
94+
* ⛔ Never satisfy a red here by deleting an enumeration from the page, or by
95+
* dropping a command from the roster. The enumeration is the promise.
96+
*/
97+
98+
import { describe, it, expect } from 'vitest';
99+
import { readFileSync } from 'node:fs';
100+
import { resolve } from 'node:path';
101+
import { fileURLToPath } from 'node:url';
102+
import Lint from '../src/commands/lint.js';
103+
import { TEMPLATES } from '../src/commands/init.js';
104+
105+
const HERE = resolve(fileURLToPath(import.meta.url), '..');
106+
107+
// One `resolve(HERE, …)` call per line and nothing split across lines:
108+
// `check:cross-package-test-inputs` reconstructs these reads by SOURCE SCAN,
109+
// and a spelling it cannot parse leaves the glob declared and held by nothing.
110+
// Both are already declared for `@objectstack/cli` in
111+
// scripts/cross-package-test-inputs.mjs and mirrored into turbo.json.
112+
const CLI_PAGE = resolve(HERE, '../../..', 'content/docs/deployment/cli.mdx');
113+
const BLANK_TEMPLATE_PKG = resolve(HERE, '../../..', 'packages/create-objectstack/src/templates/blank/package.json');
114+
115+
const PAGE = readFileSync(CLI_PAGE, 'utf8');
116+
const PAGE_ID = 'content/docs/deployment/cli.mdx';
117+
118+
// ─── The derived sets ───────────────────────────────────────────────
119+
120+
/**
121+
* The commands whose `**Options.**` table this file binds. A table under a
122+
* command that is not here is a finding, so the roster cannot be narrowed to
123+
* make a red go away.
124+
*/
125+
const GOVERNED = {
126+
lint: { flags: Object.keys(Lint.flags), args: Object.keys(Lint.args) },
127+
} satisfies Record<string, { flags: string[]; args: string[] }>;
128+
129+
/** The roster's floor: the command #16892 ② measured at 4 of 11. */
130+
const GOVERNED_FLOOR = ['lint'];
131+
132+
/** Every scaffolder that writes a new project's `package.json`, by its documented name. */
133+
const SCAFFOLDER_SCRIPTS: Record<string, Record<string, string>> = {
134+
...Object.fromEntries(Object.entries(TEMPLATES).map(([name, t]) => [name, t.scripts])),
135+
'create-objectstack blank': JSON.parse(readFileSync(BLANK_TEMPLATE_PKG, 'utf8')).scripts,
136+
};
137+
138+
/** The script names in one map whose command line runs the ObjectStack CLI. */
139+
function cliScripts(scripts: Record<string, string>): Set<string> {
140+
return new Set(Object.entries(scripts).filter(([, body]) => /\bobjectstack\b/.test(body)).map(([name]) => name));
141+
}
142+
143+
/** The CLI-invoking scripts EVERY scaffolder declares — what the callout claims. */
144+
const SCAFFOLDED_EVERYWHERE: string[] = (() => {
145+
const maps = Object.values(SCAFFOLDER_SCRIPTS).map(cliScripts);
146+
const [first, ...rest] = maps;
147+
return [...first].filter((name) => rest.every((m) => m.has(name))).sort();
148+
})();
149+
150+
// ─── The page readers ───────────────────────────────────────────────
151+
152+
/** `#### \`os <name>\`` down to the next one — the section a table belongs to. */
153+
function commandSections(mdx: string): { command: string; body: string }[] {
154+
const hits = [...mdx.matchAll(/^#### `os ([^`\n]+)`[^\n]*$/gm)];
155+
return hits.map((hit, i) => ({
156+
command: hit[1].trim(),
157+
body: mdx.slice(hit.index + hit[0].length, i + 1 < hits.length ? hits[i + 1].index : mdx.length),
158+
}));
159+
}
160+
161+
/** The `**Options.**` lead-in and its table rows, or null when the section has none. */
162+
function optionsTable(body: string): { lead: string; rows: string[] } | null {
163+
const m = /^\*\*Options\.\*\*([^\n]*)\n\n\|[^\n]*\|\n\|[-:| ]+\|\n((?:\|[^\n]*\|\n)+)/m.exec(body);
164+
return m ? { lead: m[1], rows: m[2].trimEnd().split('\n') } : null;
165+
}
166+
167+
/** The name each table row is ABOUT: the first word of its first backticked cell. */
168+
function rowNames(rows: string[]): { flags: string[]; positionals: string[] } {
169+
const flags: string[] = [];
170+
const positionals: string[] = [];
171+
for (const row of rows) {
172+
const cell = row.replace(/^\|/, '').split('|')[0] ?? '';
173+
const code = /`([^`]+)`/.exec(cell);
174+
if (!code) continue;
175+
const name = code[1].trim().split(/\s+/)[0];
176+
(name.startsWith('--') ? flags : positionals).push(name);
177+
}
178+
return { flags: flags.sort(), positionals: positionals.sort() };
179+
}
180+
181+
/** Number words this page spells out. A word outside the table is a refusal, not a zero. */
182+
const NUMBER_WORDS: Record<string, number> = { one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8 };
183+
184+
function spelledNumber(word: string, where: string): number {
185+
const n = NUMBER_WORDS[word.toLowerCase()];
186+
expect(n, `${PAGE_ID}: "${word}" in ${where} is not a number word this pin can read — add it to NUMBER_WORDS`).toBeDefined();
187+
return n;
188+
}
189+
190+
/** The scaffolded-wiring callout, read off the page with its line wraps flattened. */
191+
const CALLOUT = (() => {
192+
const flat = PAGE.replace(/\s+/g, ' ');
193+
const wired = /all (\w+) are wired as ((?:`npm run [\w:-]+`(?:, | and )?)+)/.exec(flat);
194+
const templates = /the (\w+) `os init` templates \(([^)]*)\)/.exec(flat);
195+
const eachDeclares = /each declare all (\w+) scripts/.exec(flat);
196+
return {
197+
flat,
198+
wiredCount: wired?.[1],
199+
wiredScripts: wired ? [...wired[2].matchAll(/`npm run ([\w:-]+)`/g)].map((m) => m[1]).sort() : null,
200+
templateCount: templates?.[1],
201+
templateNames: templates ? [...templates[2].matchAll(/`([^`]+)`/g)].map((m) => m[1]).sort() : null,
202+
eachDeclaresCount: eachDeclares?.[1],
203+
};
204+
})();
205+
206+
// ─── 1. The derivations are real before anything is compared ────────
207+
208+
describe('#17723 · the derived sets are non-empty, so nothing here can pass by reading nothing', () => {
209+
it('`os lint` declares flags and a positional', () => {
210+
expect(GOVERNED.lint.flags.length).toBeGreaterThan(0);
211+
expect(GOVERNED.lint.args.length).toBeGreaterThan(0);
212+
});
213+
214+
it('the governed roster holds its floor', () => {
215+
for (const command of GOVERNED_FLOOR) {
216+
expect(
217+
Object.keys(GOVERNED),
218+
`\`os ${command}\` left the roster — #16892 measured its table at 4 of 11 flags, and the roster is what holds it`,
219+
).toContain(command);
220+
}
221+
});
222+
223+
it('every scaffolder declares CLI-invoking scripts, and some are common to all of them', () => {
224+
expect(Object.keys(SCAFFOLDER_SCRIPTS).length).toBeGreaterThan(1);
225+
for (const [name, scripts] of Object.entries(SCAFFOLDER_SCRIPTS)) {
226+
expect(cliScripts(scripts).size, `scaffolder \`${name}\` declares no script that runs the CLI`).toBeGreaterThan(0);
227+
}
228+
expect(SCAFFOLDED_EVERYWHERE.length).toBeGreaterThan(0);
229+
});
230+
231+
it('the page still contains the two sections this pin reads', () => {
232+
expect(commandSections(PAGE).map((s) => s.command)).toContain('lint');
233+
expect(CALLOUT.wiredScripts, `${PAGE_ID}: the "all N are wired as \`npm run …\`" sentence is gone`).not.toBeNull();
234+
expect(CALLOUT.templateNames, `${PAGE_ID}: the "the N \`os init\` templates (…)" list is gone`).not.toBeNull();
235+
});
236+
});
237+
238+
// ─── 2. Every documented options table is bound to a declaration ────
239+
240+
describe('#17723 · the `**Options.**` tables are held to the oclif declarations', () => {
241+
it('no command documents an options table this pin does not bind', () => {
242+
const unheld = commandSections(PAGE)
243+
.filter((s) => optionsTable(s.body) !== null)
244+
.map((s) => s.command)
245+
.filter((c) => !(c in GOVERNED));
246+
expect(
247+
unheld,
248+
`${PAGE_ID}: these sections promise "every flag the command declares" and nothing reads them: ${unheld.join(', ')}.\n`
249+
+ ' Add the command to GOVERNED in this file — an enumeration nobody reads is how #16892 ② happened.',
250+
).toEqual([]);
251+
});
252+
253+
it.each(Object.keys(GOVERNED))('`os %s` documents an options table at all', (command) => {
254+
const section = commandSections(PAGE).find((s) => s.command === command);
255+
expect(section, `${PAGE_ID}: the \`#### \`os ${command}\`\` section is gone`).toBeDefined();
256+
const table = optionsTable(section!.body);
257+
expect(table, `${PAGE_ID}: \`os ${command}\` no longer carries an \`**Options.**\` table — the promise this pin holds is gone`).not.toBeNull();
258+
expect(rowNames(table!.rows).flags.length, `${PAGE_ID}: \`os ${command}\`'s options table names no flags`).toBeGreaterThan(0);
259+
});
260+
261+
it.each(Object.keys(GOVERNED))('`os %s` documents exactly the flags and positionals it declares', (command) => {
262+
const declared = GOVERNED[command as keyof typeof GOVERNED];
263+
const table = optionsTable(commandSections(PAGE).find((s) => s.command === command)!.body)!;
264+
const documented = rowNames(table.rows);
265+
expect(
266+
documented,
267+
`${PAGE_ID}: \`os ${command}\`'s options table disagrees with its declaration in `
268+
+ `packages/cli/src/commands/${command}.ts:\n`
269+
+ ` declared but NOT documented: ${declared.flags.map((f) => `--${f}`).filter((f) => !documented.flags.includes(f)).join(', ') || '(none)'}\n`
270+
+ ` documented but NOT declared: ${documented.flags.filter((f) => !declared.flags.map((d) => `--${d}`).includes(f)).join(', ') || '(none)'}\n`
271+
+ ` positionals declared: ${declared.args.join(', ') || '(none)'} · documented: ${documented.positionals.join(', ') || '(none)'}`,
272+
).toEqual({
273+
flags: declared.flags.map((f) => `--${f}`).sort(),
274+
positionals: [...declared.args].sort(),
275+
});
276+
});
277+
278+
it.each(Object.keys(GOVERNED))('`os %s`\'s lead-in counts the positionals it really has', (command) => {
279+
const declared = GOVERNED[command as keyof typeof GOVERNED];
280+
const table = optionsTable(commandSections(PAGE).find((s) => s.command === command)!.body)!;
281+
const said = /its (\w+) positional/.exec(table.lead);
282+
expect(said, `${PAGE_ID}: \`os ${command}\`'s \`**Options.**\` lead-in no longer counts its positionals`).not.toBeNull();
283+
expect(
284+
spelledNumber(said![1], `\`os ${command}\`'s options lead-in`),
285+
`${PAGE_ID}: \`os ${command}\` says "${said![1]} positional" and declares ${declared.args.length}`,
286+
).toBe(declared.args.length);
287+
});
288+
});
289+
290+
// ─── 3. The scaffolded-wiring callout is held to the scaffolders ────
291+
292+
describe('#17723 · the scaffolded-wiring callout is held to the scaffolders', () => {
293+
it('names exactly the `os init` templates that exist', () => {
294+
expect(
295+
CALLOUT.templateNames,
296+
`${PAGE_ID}: the callout's \`os init\` template list disagrees with TEMPLATES in `
297+
+ 'packages/cli/src/commands/init.ts — a template added there must be named here (#16350)',
298+
).toEqual(Object.keys(TEMPLATES).sort());
299+
});
300+
301+
it('counts those templates correctly', () => {
302+
expect(
303+
spelledNumber(CALLOUT.templateCount!, "the callout's `os init` template count"),
304+
`${PAGE_ID}: the callout says "${CALLOUT.templateCount}" \`os init\` templates and there are ${Object.keys(TEMPLATES).length}`,
305+
).toBe(Object.keys(TEMPLATES).length);
306+
});
307+
308+
it('still names the `create-objectstack` blank template as a scaffolder', () => {
309+
expect(
310+
CALLOUT.flat,
311+
`${PAGE_ID}: the callout stopped naming the \`create-objectstack\` blank template — it is the fourth scaffolder (#16350)`,
312+
).toContain('`create-objectstack` blank template');
313+
});
314+
315+
it('names exactly the CLI scripts every scaffolder declares', () => {
316+
expect(
317+
CALLOUT.wiredScripts,
318+
`${PAGE_ID}: the callout's \`npm run …\` list disagrees with what the scaffolders declare:\n`
319+
+ ` declared by every scaffolder but NOT named: ${SCAFFOLDED_EVERYWHERE.filter((s) => !CALLOUT.wiredScripts!.includes(s)).join(', ') || '(none)'}\n`
320+
+ ` named but NOT declared by every scaffolder: ${CALLOUT.wiredScripts!.filter((s) => !SCAFFOLDED_EVERYWHERE.includes(s)).join(', ') || '(none)'}\n`
321+
+ ' Sources: TEMPLATES in packages/cli/src/commands/init.ts and packages/create-objectstack/src/templates/blank/package.json',
322+
).toEqual(SCAFFOLDED_EVERYWHERE);
323+
});
324+
325+
it('counts those scripts correctly in both sentences', () => {
326+
const n = SCAFFOLDED_EVERYWHERE.length;
327+
expect(
328+
spelledNumber(CALLOUT.wiredCount!, 'the callout\'s "all N are wired" sentence'),
329+
`${PAGE_ID}: the callout says "all ${CALLOUT.wiredCount} are wired" and ${n} scripts are`,
330+
).toBe(n);
331+
expect(
332+
spelledNumber(CALLOUT.eachDeclaresCount!, 'the callout\'s "each declare all N scripts" sentence'),
333+
`${PAGE_ID}: the callout says "each declare all ${CALLOUT.eachDeclaresCount} scripts" and ${n} are declared`,
334+
).toBe(n);
335+
});
336+
});

scripts/cross-package-test-inputs.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,16 @@ export const CROSS_PACKAGE_TEST_INPUTS = {
504504
// declaration, and declaring one rarely-touched file is cheaper than
505505
// rewording prose to dodge a scanner.
506506
'scripts/cross-package-test-inputs.mjs',
507+
// `check-cli-examples-parity.mjs` is the mention shape again, from
508+
// test/docs-cli-enumeration-parity.test.ts (#17723): that pin's header
509+
// states WHY it does not extend this sibling gate --- `os lint` declares no
510+
// `examples`, and an example set and a flag set are different populations,
511+
// so an examples-parity check is structurally blind to a missing flag. The
512+
// sentence is the gate's own stated reason, and it has to name what it is
513+
// about. Settled the way `check-nul-bytes.mjs` above is: declaring one
514+
// rarely touched file is cheaper than rewording prose to dodge a scanner,
515+
// and over-collection can only widen a radius, never narrow one.
516+
'scripts/check-cli-examples-parity.mjs',
507517
// `nightly-tiers.mjs` is the mention shape a FIFTH time, and the one entry
508518
// that was already HALF declared: turbo.json has carried it (and its
509519
// `.d.mts`) in this task's `inputs` all along, because `vitest-tiers.ts`

turbo.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
"$TURBO_ROOT$/scripts/cli-unbuilt-workspace-lead.d.mts",
135135
"$TURBO_ROOT$/scripts/cli-build-prerequisite.mjs",
136136
"$TURBO_ROOT$/scripts/cross-package-test-inputs.mjs",
137+
"$TURBO_ROOT$/scripts/check-cli-examples-parity.mjs",
137138
"$TURBO_ROOT$/packages/spec/src/system/translation.zod.ts",
138139
"$TURBO_ROOT$/scripts/check-cross-package-test-inputs.mjs",
139140
"$TURBO_ROOT$/packages/create-objectstack/src/templates/blank/pnpm-workspace.yaml",

0 commit comments

Comments
 (0)