11// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22
33/**
4- * DOCS PARITY (#16483) — no public page still offers `os create example` to run.
4+ * DOCS PARITY (#16483, #16690) — no public page and no SHIPPED file still
5+ * offers `os create example` to run.
56 *
67 * ## The half no process can check
78 *
89 * `create-example-retired.e2e.test.ts` drives the real CLI and proves the
910 * refusal: non-zero, and the message names `os init`. That says nothing about
10- * the four public pages which present `os create` as a user-facing command. A
11+ * the public pages which present `os create` as a user-facing command. A
1112 * reader who follows a page rather than a terminal is exactly the reader the
1213 * #15531 ruling is protecting, and a page that still prints
1314 * `os create example my-app` in a copyable block hands them a command that now
1819 * That one is `*.e2e.test.*`, a NAME-decided tier (`scripts/nightly-tiers.mjs`,
1920 * #16455): those files are excluded from the per-PR and merge-queue population
2021 * and run in the nightly job. That is the right lane for four cold `tsx` spawns
21- * and the wrong lane for four `readFileSync` calls — a docs-only PR that
22- * reintroduced the retired command would otherwise be caught the following
22+ * and the wrong lane for a handful of `readFileSync` calls — a docs-only PR
23+ * that reintroduced the retired command would otherwise be caught the following
2324 * night instead of on the PR. This file spawns nothing, so it stays queue-tier
2425 * and reddens where the edit is made.
2526 *
2930 * a reader arriving from a search engine or an older tutorial needs. What the
3031 * ruling fixed is two narrower things, and those are what is asserted:
3132 *
32- * a. no FENCED CODE BLOCK on any public page carries it, so there is nothing
33- * left to copy into a terminal;
33+ * a. nothing a reader can COPY carries it, so there is nothing left to paste
34+ * into a terminal;
3435 * b. a page that mentions it in prose also names `os init`, so the mention is
3536 * a signpost rather than a leftover.
3637 *
3738 * ⛔ Never satisfy a red here by deleting a page's `os create` section. The
38- * positive control below fails when a page stops showing `os create plugin` in
39- * a fence , which is what makes the two assertions capable of failing at all.
39+ * positive controls below fail when a carrier stops showing a SURVIVING
40+ * `os create` , which is what makes the assertions capable of failing at all.
4041 *
4142 * All four pages are already declared as cross-package inputs of
4243 * `@objectstack/cli` (`scripts/cross-package-test-inputs.mjs`, mirrored into
4344 * `turbo.json`), so a docs-only edit reaches this suite instead of replaying a
44- * cached green.
45+ * cached green. The shipped carriers below live inside this package, so
46+ * `$TURBO_DEFAULT$` already hashes them and they need no declaration.
47+ *
48+ * ## The carrier the docs population could not see (#16690)
49+ *
50+ * The four pages above are the WEBSITE. `packages/cli/README.md` is not one of
51+ * them, and it is the carrier that actually ships: this package's own `files`
52+ * names it — `["dist", "README.md", "CHANGELOG.md"]` — so it goes out in every
53+ * tarball as the npm front page. Of the five carriers of the retired command,
54+ * the four that were guarded were documentation pages and the one that was not
55+ * is the one in the tarball.
56+ *
57+ * ## Widening the population without widening the READER is worse than the gap
58+ *
59+ * Adding the README to `DOC_SITES` and changing nothing else was MEASURED, and
60+ * it does not work. On a clean tree that naive addition fails exactly one test
61+ * — the `os create plugin` FENCE control, because the README shows that
62+ * command in prose and in a table, never in a fence. With `os create example`
63+ * written into the README's command roster TABLE ROW it fails THAT SAME ONE
64+ * TEST AND NO OTHER: both retirement assertions passed over a README that was
65+ * carrying the retired command. The two failure signatures are identical, so
66+ * the red says nothing about the retirement — an author would silence it by
67+ * adding a fenced `os create plugin` to the README and land a fully green
68+ * suite that had just been shown a retired roster row.
69+ *
70+ * ⇒ A roster row is not a fence and the fence reader cannot see it. So the
71+ * reader is widened here too, and the same widening is applied to the four
72+ * pages: `content/docs/deployment/cli.mdx` was measured to carry four
73+ * `os create` TABLE ROWS of its own, so fence-only reading left the same
74+ * blind spot on a page the suite already claimed to guard.
75+ *
76+ * ## What counts as an OFFER, and why it stops short of prose
77+ *
78+ * `offeredLines` reads two shapes, applied to both populations:
79+ *
80+ * - lines inside a fenced code block — what a reader copies to a terminal;
81+ * - markdown table rows — a command roster IS a list of runnable commands,
82+ * and a table is the shape both the README and `cli.mdx` present them in.
83+ *
84+ * ⛔ It deliberately does NOT read inline code in running prose, and that
85+ * boundary is measured rather than assumed: `packages/cli/CHANGELOG.md` ships
86+ * too, and it carries `os create example` in two historical entries (it has no
87+ * fences at all, and none of its 348 table rows names `os create`). A
88+ * changelog's job is to RECORD that a command existed and was retired;
89+ * reddening on it would demand rewriting history to keep a gate green. The
90+ * boundary test below pins that, so the next author who widens the reader sees
91+ * which line they are about to cross.
92+ *
93+ * ⚠️ NOT COVERED, stated plainly so this suite is not read as claiming it: an
94+ * inline `os create example` written into README PROSE is invisible here.
95+ * Closing that needs a rule separating an offer from a mention inside running
96+ * text, and the CHANGELOG is the proof that the rule is not "it is in
97+ * backticks".
98+ *
99+ * ## The shipped population is DERIVED, never restated
100+ *
101+ * `SHIPPED_MARKDOWN` is read out of `packages/cli/package.json`'s `files`. A
102+ * second hand-written shipping list is the drift this card exists to fix, so
103+ * the only hand-written thing is a TRIPWIRE on the entries that are NOT
104+ * markdown — `dist` today, measured to ship 0 `.md` files out of 500. A new
105+ * entry there may be a prose carrier this suite cannot see, and it must fail
106+ * until someone re-derives the population rather than extending a list.
45107 */
46108
47109import { describe , it , expect } from 'vitest' ;
@@ -59,6 +121,8 @@ const CLI_DOCS = resolve(HERE, '../../..', 'content/docs/deployment/cli.mdx');
59121const PLUGINS_INDEX = resolve ( HERE , '../../..' , 'content/docs/plugins/index.mdx' ) ;
60122const KERNEL_INDEX = resolve ( HERE , '../../..' , 'content/docs/protocol/kernel/index.mdx' ) ;
61123const PLUGIN_SPEC = resolve ( HERE , '../../..' , 'content/docs/protocol/kernel/plugin-spec.mdx' ) ;
124+ const CLI_PACKAGE_ROOT = resolve ( HERE , '..' ) ;
125+ const CLI_PACKAGE_JSON = resolve ( HERE , '..' , 'package.json' ) ;
62126
63127/** The four public pages that present `os create` as a user-facing command. */
64128const DOC_SITES : Record < string , string > = {
@@ -68,14 +132,47 @@ const DOC_SITES: Record<string, string> = {
68132 'content/docs/protocol/kernel/plugin-spec.mdx' : PLUGIN_SPEC ,
69133} ;
70134
135+ /**
136+ * This package's OWN shipping declaration. ⛔ Never restate it here: `files` is
137+ * the one list that cannot drift out of step with what `npm pack` emits, and
138+ * `packages/cli/package.json` is read for it and never written by this suite.
139+ */
140+ const SHIPPED : readonly string [ ] = (
141+ JSON . parse ( readFileSync ( CLI_PACKAGE_JSON , 'utf8' ) ) as { files : string [ ] }
142+ ) . files ;
143+
144+ /** The shipped carriers that can hold prose, derived from the line above. */
145+ const SHIPPED_MARKDOWN = SHIPPED . filter ( ( entry ) => entry . endsWith ( '.md' ) ) ;
146+
147+ const shippedText = ( rel : string ) : string => readFileSync ( resolve ( CLI_PACKAGE_ROOT , rel ) , 'utf8' ) ;
148+
71149/** Every fenced code block on a page — what a reader copies into a terminal. */
72- function fences ( mdx : string ) : string [ ] {
73- return [ ...mdx . matchAll ( / ^ ` ` ` [ ^ \n ] * \n ( [ \s \S ] * ?) ^ ` ` ` / gm) ] . map ( ( m ) => m [ 1 ] ) ;
150+ function fences ( md : string ) : string [ ] {
151+ return [ ...md . matchAll ( / ^ ` ` ` [ ^ \n ] * \n ( [ \s \S ] * ?) ^ ` ` ` / gm) ] . map ( ( m ) => m [ 1 ] ) ;
152+ }
153+
154+ /** Every markdown table row — the shape a COMMAND ROSTER is written in. */
155+ function tableRows ( md : string ) : string [ ] {
156+ return md . split ( '\n' ) . filter ( ( line ) => / ^ \s * \| / . test ( line ) ) ;
157+ }
158+
159+ /**
160+ * What a carrier OFFERS a reader to run. Fences and roster rows both present a
161+ * command as runnable; running prose does not, and the CHANGELOG boundary test
162+ * below is what holds that line in place.
163+ */
164+ function offeredLines ( md : string ) : string [ ] {
165+ return [ ...fences ( md ) . flatMap ( ( block ) => block . split ( '\n' ) ) , ...tableRows ( md ) ] ;
74166}
75167
76168/** The retired spelling, DERIVED from the registry rather than written twice. */
77169const RETIRED = Object . keys ( RETIRED_TEMPLATES ) . map ( ( key ) => `os create ${ key } ` ) ;
78170
171+ const offersRetired = ( md : string ) : string [ ] =>
172+ offeredLines ( md )
173+ . filter ( ( line ) => RETIRED . some ( ( cmd ) => line . includes ( cmd ) ) )
174+ . map ( ( line ) => line . trim ( ) ) ;
175+
79176describe ( '[#16483] the docs and the retirement registry agree' , ( ) => {
80177 it ( 'has something retired to look for, and a survivor to control against' , ( ) => {
81178 // Both halves, so neither loop below can pass over an empty population.
@@ -94,11 +191,8 @@ describe('[#16483] the docs and the retirement registry agree', () => {
94191 } ) ;
95192
96193 it . each ( Object . keys ( DOC_SITES ) ) ( '%s offers nothing to COPY that now refuses' , ( site ) => {
97- const copyable = fences ( readFileSync ( DOC_SITES [ site ] , 'utf8' ) )
98- . flatMap ( ( f ) => f . split ( '\n' ) )
99- . filter ( ( line ) => RETIRED . some ( ( cmd ) => line . includes ( cmd ) ) ) ;
100194 expect (
101- copyable . map ( ( line ) => line . trim ( ) ) ,
195+ offersRetired ( readFileSync ( DOC_SITES [ site ] , 'utf8' ) ) ,
102196 `${ site } still shows a retired command as runnable — it exits non-zero now` ,
103197 ) . toEqual ( [ ] ) ;
104198 } ) ;
@@ -112,3 +206,71 @@ describe('[#16483] the docs and the retirement registry agree', () => {
112206 ) . toContain ( 'os init' ) ;
113207 } ) ;
114208} ) ;
209+
210+ describe ( '[#16690] what SHIPS carries the retirement too' , ( ) => {
211+ it ( 'derives a non-empty carrier population from the package `files` array' , ( ) => {
212+ expect (
213+ SHIPPED_MARKDOWN . length ,
214+ '`files` names no markdown, so every assertion below passes over an empty '
215+ + 'population — re-derive the population, do not delete these tests' ,
216+ ) . toBeGreaterThan ( 0 ) ;
217+ for ( const rel of SHIPPED_MARKDOWN ) {
218+ expect ( ( ) => shippedText ( rel ) , `\`files\` names ${ rel } , which is not on disk` ) . not . toThrow ( ) ;
219+ }
220+ } ) ;
221+
222+ it ( 'has no shipped entry that could carry prose unread (tripwire)' , ( ) => {
223+ // ⛔ NOT a second shipping list — the population above is derived. This is a
224+ // tripwire: `dist` was measured to ship 0 `.md` files out of 500, so it
225+ // carries no prose. A new entry here may, and must fail until someone
226+ // re-derives the population rather than widening this line.
227+ expect (
228+ SHIPPED . filter ( ( entry ) => ! entry . endsWith ( '.md' ) ) ,
229+ 'a new non-markdown entry ships in the tarball — if it can carry a runnable '
230+ + 'command, it belongs in the suite population before this line changes' ,
231+ ) . toEqual ( [ 'dist' ] ) ;
232+ } ) ;
233+
234+ it . each ( SHIPPED_MARKDOWN ) ( '%s offers nothing to COPY that now refuses' , ( rel ) => {
235+ expect (
236+ offersRetired ( shippedText ( rel ) ) ,
237+ `${ rel } ships in the npm tarball and still offers a retired command as `
238+ + 'runnable — it exits non-zero now' ,
239+ ) . toEqual ( [ ] ) ;
240+ } ) ;
241+
242+ it ( 'reads a roster TABLE ROW, not only a fence (control)' , ( ) => {
243+ // ⭐ The control that makes the assertion above capable of failing at all.
244+ // The README presents `os create` in a table cell and never in a fence, so
245+ // a fence-only reader finds nothing here and every assertion goes vacuous.
246+ const rosterRows = SHIPPED_MARKDOWN . flatMap ( ( rel ) => tableRows ( shippedText ( rel ) ) ) . filter (
247+ ( line ) => line . includes ( 'os create' ) ,
248+ ) ;
249+ expect (
250+ rosterRows . length ,
251+ 'no shipped markdown shows `os create` in a table row — the reader has '
252+ + 'nothing to prove it can see a roster cell, so the retirement '
253+ + 'assertion above can no longer fail' ,
254+ ) . toBeGreaterThan ( 0 ) ;
255+ } ) ;
256+
257+ it ( 'records a retired command outside an offer position without reddening (boundary)' , ( ) => {
258+ // The CHANGELOG names `os create example` in historical entries. That is a
259+ // RECORD, not an offer, and it must stay green: a reader widened to all
260+ // inline code would demand rewriting shipped history to keep a gate green.
261+ const recorded = SHIPPED_MARKDOWN . flatMap ( ( rel ) => {
262+ const text = shippedText ( rel ) ;
263+ const offered = new Set ( offeredLines ( text ) ) ;
264+ return text
265+ . split ( '\n' )
266+ . filter ( ( line ) => RETIRED . some ( ( cmd ) => line . includes ( cmd ) ) && ! offered . has ( line ) ) ;
267+ } ) ;
268+ expect (
269+ recorded . length ,
270+ 'no shipped file mentions a retired command outside an offer position, so '
271+ + 'the offer/record boundary this reader depends on is no longer '
272+ + 'exercised — re-establish it or retire this control deliberately' ,
273+ ) . toBeGreaterThan ( 0 ) ;
274+ expect ( offersRetired ( recorded . join ( '\n' ) ) ) . toEqual ( [ ] ) ;
275+ } ) ;
276+ } ) ;
0 commit comments