fix(cli): os init writes a lint script into all three scaffold templates, and a pin holds the two scaffolders equal - #16888
Conversation
`packages/create-objectstack`'s blank template declares `lint` and ships a CI workflow that runs `pnpm lint`; the three script maps in `os init` declared `validate` and no `lint`, so the two scaffolders emitted different projects and only one of them could run that workflow. Adds `lint: 'objectstack lint'` after `validate` in all three maps, and a pin that derives the required script set from the workflow the on-ramp template ships, so the next divergence reddens instead of being discovered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 34c540c23c9dce49ea2477d6d266e3e20f817148 && git checkout 34c540c23c9dce49ea2477d6d266e3e20f817148
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 613bfbd3db7eb9b311ed435eb33e30d81d96641c 9bb46c764ca124ab9357a39d4ea45a136ec14bfd && git checkout -B drift-repro 613bfbd3db7eb9b311ed435eb33e30d81d96641c && git merge --no-ff 9bb46c764ca124ab9357a39d4ea45a136ec14bfd
node scripts/docs-audit/affected-docs.mjs --json 613bfbd3db7eb9b311ed435eb33e30d81d96641c
|
Fixes #16350
packages/create-objectstack/src/templates/blank/package.jsondeclares alintscript and the CI workflow that template ships runspnpm lint(both added by #16330). The three script maps inpackages/cli/src/commands/init.tseach declaredvalidateand nolint, sonpm create objectstackandos create/os initproduced projects with different script sets, and only the first could run that workflow. This adds the missing line to all three maps and pins the two scaffolders together on the scripts that workflow runs.Clause-②: no
1. All three maps, located by symbol
The maps were found by the string
validate: 'objectstack validate',— not by the card's or triage's line numbers, which rot. On this branch that string occurs exactly three times ininit.ts, once in each of theapp,pluginandemptytemplates, andlint: 'objectstack lint',is inserted directly after each one, matching the template's order.The zero this rests on was re-taken here with its control rather than inherited: before the change
objectstack lintoccurred zero times ininit.tswhilevalidateoccurred 14 times in the same file, so the probe demonstrably reaches those maps. After the change,lint: 'objectstack lint',occurs three times andvalidate: 'objectstack validate',still occurs three times.The card's
lint!=validateargument was also re-run here rather than relayed, with the criterion it came with.git grep hook-body-lowering -- packagesreturns exactly two hits — the one import inpackages/cli/src/commands/lint.tsand the rule's own test atpackages/cli/src/lint/hook-body-lowering.test.ts— which is the criterion the card stated. Firing control on the same channel:git grep -c runAuthoringRules -- packages/cli/src/commandsreturns three files (compile.ts,lint.ts,validate.ts), so the grep channel is live.checkHookBodyLoweringhas exactly one call site,src/commands/lint.ts:457, sohook-body/not-lowerableis unreachable fromos validate.2. Ratchet outcome: PIN LANDED
packages/cli/test/scaffold-ci-script-parity.test.ts(22 assertions, unit tier).The pin does not compare the whole maps, because the two sides differ on purpose in places:
init.ts'sappmap spellsstartasobjectstack compile && objectstack serve(with the reasoning in a comment beside it) where the template saysobjectstack start; itsbuildrunsobjectstack compilewhere the template names theobjectstack buildalias; and theplugin/emptytemplates scaffold a metadata package with no server to run at all. Whole-map equality would have needed an exemption ledger and an adjudication of which spelling wins — the design exercise the escape hatch exists to avoid.Instead the required set is derived from the workflow the on-ramp template ships: the project scripts its
pnpmsteps run. That is the subset on which the two scaffolders make the same promise to the same user, it is exactly the harm the card names ("a project scaffolded throughinit.tsthat later copies the template's workflow gets a red build on a step whose script it does not have"), and on it there is currently no accepted exception — the pin carries no ledger at all, on names or on values. Nothing is transcribed: the script names come from the workflow, the expected values come from the template's ownpackage.json. Today the derivation resolves tovalidate,lintandtypecheck.Two guards against a vacuous green: the harvest is asserted non-empty before it is used (a parser or regex that stopped matching would otherwise read exactly like parity), and the template is asserted to declare each harvested script before its value is compared (
undefined === undefinedwould otherwise pass).The other half of the loop already exists in
create-objectstack, holding that workflow against the template's ownpackage.json; it is package-local by construction and cannot seeinit.ts. Its failure text says so in words. This file closes the sentence from the other side.Because the pin reads a second package,
packages/create-objectstack/src/templates/blank/.github/workflows/ci.ymlis declared as a cross-package test input for@objectstack/cliand mirrored intoturbo.json;pnpm check:cross-package-test-inputsis green (OK: 28 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob). The blank template'spackage.jsonwas already declared.3. Ablation — the pin fails when the two sides disagree
Driven from the committed state, one map mutated on disk, restored with
git checkout HEAD --, with an absolute-pathtrap ... EXIT INT TERMon the restore.Not read as a bare non-zero exit: the red leg ran 22 tests and failed 2, both naming the mutated map —
and the green leg ran the same 22 tests, 22 passed. Equal counts on both legs, so neither leg was an empty collection. The pin is unit tier by the behaviour predicate in
packages/cli/vitest-tiers.ts(it neither spawns nor boots a kernel), so it guards a PR and a merge-queue entry; it is not a nightly-tier file.The dist preflight does not apply here: the pin imports
../src/commands/init.jsrelatively, so the subject resolves through source, never through a packageexportsentry intodist/.4. Verification
Branch merged with
origin/mainata5d4e286b6before this run; all readings below are from9bb46c764c.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RANFILEreconciles:77 derived famil(ies) accounted for - 77 run, 0 NOT-MEASURED. Derived and re-derived after the merge; the family list is byte-identical to the pre-merge derivation and theSTALE TREEbanner is gone.PREREQUISITE NOT METand are reported as NOT MEASURED, not as passes:check:dual-build-cjs-loads("this gate reads built output, and some package has no dist/ ... nothing was measured"),check:i18n-coverage("Nothing was compared: 12 config(s) did lint, but a partial round cannot judge"),check:type-check-debt("PREREQUISITE NOT MET ... nothing was measured"). Each needs a full-workspace build; CI runs them over the built tree.check:dts-closure,check:sourcemap-no-sources-content,check:i18nandcheck:i18n-walk-paritywere re-run after the build and are green.pnpm --filter "@objectstack/cli^..." buildexit 0, thenpnpm --filter @objectstack/cli buildexit 0, thenpnpm --filter @objectstack/spec check:generatedexit 0 (spec moved onmain's side of the merge).pnpm --filter @objectstack/cli typecheckexit 0 (that script istsc --noEmit && pnpm check:test-typecheck). The new test really is inside the checked program:tsc -p tsconfig.test.json --listFilesnames it, 1 hit, withtest/init.test.tsas a positive control at 1 hit. The package's plaintsconfig.jsoncovers neither, which is what the test-layer project exists for.packages/cliunit tier,vitest run --project unit:2524 passed | 6 expected fail | 11 skipped. Five suites failed on the shared box under concurrent load and all five pass when re-run: fourdoctor-*suites died onCannot find module .../packages/spec/dist/security/index.mjswhile a sibling build was rewriting thatdist(the file is present, and the four are green on re-run), andsrc/commands/datasource/envelope-unwrap.test.tshitHook timed out in 10000msinside oclif'sConfig.loadand passes alone, 11/11. None of the five is reachable from this diff. Theintegrationtier is declared to CI: this diff touches no integration-tier file, no spawn entry point and no driver or kernel boot path.pnpm lint(eslint . --no-inline-config) exit 0, at9bb46c764c. Not narrowed, so no narrowing needed a proof.pnpm check:nul-bytesgreen, plus a direct control-character sweep over the five changed paths with no hit.A
patchchangeset for@objectstack/cliis included: the maps are program output of a published package, so this publishes.5. Out of scope, by the card's own words
The template half (#16330), any lint-rule change, and any change to
os validate's rule set are all untouched. This diff adds one string property to three object literals and one test file, plus the two declaration rows that test needs.objectstack lintaccepts and refuses exactly what it did before.6. Docs drift advisory
Re-derived on a clean checkout, and the bot's list reproduces exactly. Worktree created detached at
refs/pull/16888/merge,git status --porcelainempty before the run and still empty after.node scripts/docs-audit/affected-docs.mjs --json 613bfbd3db7eb9b311ed435eb33e30d81d96641creports its own provenance as:dirty: false, same head and same diff base as the advisory. 6 docs, 5 hand-written + 1 release-owned, one anchor (os init, command, read offpackages/cli/src/commands/init.ts),anchorlessChanges: []— identical to the bot's rows, so on this PR its dirty-checkout provenance defect did not move the number in either direction.What each hand-written page actually claims
content/docs/deployment/cli.mdx— two passages. Theos initsection (its table, itsos init …examples and its options list) describes templates, placement and flags, and never enumerates the emitted scripts, so it is untouched. The passage that does enumerate is the Callout at lines 570-575: "os validate,os buildandos lintshare one rule registry … In a scaffolded project these are wired asnpm run validateandnpm run build". That names three commands and then maps them onto two scripts. After this PR everyos inittemplate also wires the third, so the mapping is under-inclusive.content/docs/getting-started/your-first-project.mdx— walksnpm create objectstack, notos init; itsos initmention is a routing Callout. Its "What was generated" tree listspackage.jsonas a filename and never opens it, and its steps invokenpm run dev/npm run validate/npm run buildone at a time rather than as a closed set. Its dependency table describes@objectstack/clias "Theos/objectstackCLI:dev,validate,build,start" — a list of CLI commands, already silent onlintbefore this PR and equally silent after; nothing this diff does makes it false.content/docs/getting-started/examples.mdx— itsos initCallout is strictly about emitted FILES ("objectstack.config.tsplus a single object … with a matching barrel"). No script claim at all.content/docs/plugins/index.mdx— namesos init -t pluginonly to route the reader away from it; the scaffold tree andpnpm buildlines shown belong toos create plugin, a scaffolder this diff does not touch.content/docs/protocol/kernel/index.mdx— same shape: one routing sentence aboutos init -t plugin, thenos create plugin's generated tree. No script claim.content/docs/releases/v17.mdx(release-owned, read only)Read, not edited. It mentions
os initfour times and every one is a historical record of a past change:driver-memorydropped from scaffolded dependencies,engines: { protocol: '^17' }stamping, thesharingModelfix, and template descriptions no longer advertising kinds they never emit. None of them states anything about the emitted script set, so nothing on that page is falsified by this diff — there is no fact here to hand back, and no docs-only PR or issue is owed for it.Answering the advisory's own blind spot by hand
The tool anchors on identifiers; a page that states a rule by its inputs shares none with the emitter. Swept all of
content/for the short tokens, every zero paired with a control from the same vocabulary:objectstack lint"lint":pnpm lintpnpm buildnpm run lintobjectstack validate"validate":"build":"typecheck":"build":Every control fires, so the three zeros are readings. The non-zero hits are all unrelated:
automation/hook-bodies.mdxdiscusses which command lowers a handler,releases/v15.mdxis historical, and the only"lint":/"build":/npm run linthits sit inprotocol/kernel/plugin-spec.mdx, in a kernel code plugin'spackage.jsonexample whose lint script iseslint src/—os create plugin's world, notos init's. No page anywhere undercontent/prints anos init-emittedpackage.jsonscripts block, which is why nothing shows a scripts list this PR could contradict.The
os lintsection ofcli.mdx: incomplete before, incomplete after, not wrongRe-measured rather than inherited:
packages/cli/src/commands/lint.tsdeclaresjson,fix,strict,score,eval,generator,eval-min,skip-i18n,include-platform,i18n-strictanddefault-locale, while the page documents four (--score,--fix,--strict,--json). That under-documentation reproduces, is pre-existing, and is not touched by this diff. Separately, nothing in that section describes what scripts a scaffolded project declares, and this PR changes nothingos lintaccepts or refuses — so this diff makes nothing on that page wrong. The two findings are different and only neither is mine to fix here.Verdict
One page carries a sentence this PR moves —
content/docs/deployment/cli.mdx:570-575. By the repo's own criterion it is incomplete, not wrong: a reader who copiesnpm run validateandnpm run buildsucceeds, and the omission is a missing member of an enumeration rather than an instruction that now fails. It is therefore recorded below rather than filed, and ⛔ not fixed here — a docs edit is not this card's surface, and it pairs naturally with the already-knownos lintflag gap on the same page in one docs-only follow-up.验收备注
Observations from the neighbourhood, deliberately not filed and not fixed here:
init.tsspells thebuildscriptobjectstack compilewhere the blank template spells itobjectstack build.packages/cli/src/commands/build.tsisclass Build extends Compilewith only a description override, so the two are the same command and the difference is a spelling, not a behaviour. Picking one spelling would be a decision about which name the docs teach; 承接者: whoever next revisits the scaffolders' emission policy.init.ts'sappmap spellsstartasobjectstack compile && objectstack servewhere the template saysobjectstack start;init.tscarries a comment explaining the choice, so this is a recorded difference rather than drift. 承接者: 无.content/docs/deployment/cli.mdxlines 570-575 say "os validate,os buildandos lintshare one rule registry ... In a scaffolded project these are wired asnpm run validateandnpm run build" — three commands mapped onto two scripts. Everyos inittemplate now wires the third as well, and the blank template has since scaffold:create-objectstackcreates.github/but ships no workflow, so every scaffolded app starts with zero CI — the gates exist as npm scripts nothing ever runs #16330, so the mapping is under-inclusive for both scaffolders. Incomplete, not wrong (copying either named script still works), so it is not filed; it belongs in a docs-only follow-up together with the pre-existingos lintflag gap on the same page. 承接者: whoever takes that docs-only PR.content/docs/deployment/cli.mdxdocumentsos lint's flags as--score/--fix/--strict/--json, whilepackages/cli/src/commands/lint.tsdeclares eleven including--eval,--generator,--eval-min,--skip-i18n,--include-platform,--i18n-strictand--default-locale. Re-measured here rather than inherited; pre-existing and untouched by this diff. 承接者: the same docs-only PR.src/commands/datasource/envelope-unwrap.test.tshas a 10-secondbeforeAllbudget around oclif'sConfig.load, which is enough alone and not enough on a loaded shared box. It is a timing budget, not a defect this branch can see reproduced deterministically. 承接者: 无.Generated by Claude Code