fix(create-objectstack): generate the on-ramp's version pins from the shared emission policy - #16654
Conversation
… shared emission policy `npx create-objectstack` declared `typescript: ^6.0.0` while `os init` and `os create` emitted `^5.3.0` from the shared `SCAFFOLD_*` constants, so two projects created the same day got different TypeScript majors depending on which documented entry point the reader followed. The structural cause is that `create-objectstack` cannot import from `@objectstack/cli` — the dependency edge runs the other way and the npx package must not pull the CLI's closure — so its bundled template restated the policy and the restatement decayed. Editing the values into agreement would leave them free to diverge again for the same reason, so they are generated instead: - `scripts/sync-scaffold-emission-policy.mjs` reads the constants out of `packages/cli/src/commands/init.ts` and stamps them into every bundled template's `package.json`; `create-objectstack`'s `build` runs it, and `pnpm check:scaffold-emission-policy` reds on drift (both legs in lint.yml). - `test/scaffold-emission-policy.e2e.test.ts` now drives all THREE scaffolders and compares the manifests they emit, rather than restating the constants. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
… disposition Three gate findings from the first pass, all in the wiring rather than the mechanism: - `check:cross-package-test-inputs` — the emission-policy pin names `scripts/sync-scaffold-emission-policy.mjs`, so `@objectstack/cli` must declare it (with the on-ramp's `bin/` entry and bundled template) or a change to it would replay a cached green over the divergence the pin exists to catch. - `check:merge-driver` — every `gen:` name owes a recorded merge disposition. The bundled `package.json` is MIXED: the generator rewrites two values and reproduces none of the rest, so it is `NOT_DRIVER_MANAGED` with the reason. - The same gate counts generator NAMES, so `create-objectstack`'s build calls the script directly rather than declaring a second `gen:` alias for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
📓 Docs Drift CheckThis PR changes 1 package(s): 3 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 9785d6401c2ca769eb24abc23b99f70131cdeb48 && git checkout 9785d6401c2ca769eb24abc23b99f70131cdeb48
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bc0ac1d1f5775659e72ad35452c463443a71f1be cb9f71bded69434e9e5098e84bfdd87ca0745910 && git checkout -B drift-repro bc0ac1d1f5775659e72ad35452c463443a71f1be && git merge --no-ff cb9f71bded69434e9e5098e84bfdd87ca0745910
node scripts/docs-audit/affected-docs.mjs --json bc0ac1d1f5775659e72ad35452c463443a71f1be
|
…16483) Contract review on PR #16665, findings F1-F4. F1 — the "no alias, no deprecation window" ruling could not be located by the review: not in #15531's comments, not in ledger #12708 (whose earliest comment postdates the attributed date), not in the tree. The removal itself IS verified by the #15531 batch #66 entry, so create.ts and the e2e header now cite that and record the alias/window terms as recorded on card #16483, pending maintainer confirmation. The runtime message is deliberately unchanged: it describes what the code does, which is true whoever ruled it. F2 — `os init` does NOT write "the same objectstack.config.ts". The audit this card cites measures tsconfig.json byte-identical and the two manifests DIFFERENT (both ManifestSchema-valid). Runtime message and changeset now say "the same tsconfig.json and an equivalent objectstack.config.ts", and the changeset states the difference. F3 — `-t empty` writes five files and runs the install, so "objectstack.config.ts only" was wrong in the runtime message, the changeset and the doc callout this PR added. All three now read "config only, no src/objects". F4 — the changeset said the e2e file holds the doc pages (the docs pin is the separate queue-tier file) and that `--in-repo` is unchanged (its examples/ placement goes with the template). Both corrected. F5 is deliberately NOT acted on here: SCAFFOLD_TSX_RANGE stays, init.ts is held by #16654. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
Brings in #16654 (scripts/sync-scaffold-emission-policy.mjs and its `check:scaffold-emission-policy` gate), which landed while this PR was in contract review. Merged rather than left to the queue because that gate's declared inputs include packages/cli/src/commands/init.ts and the bundled templates, and this PR touches init.ts (comment-only) and removes a template — close enough that it should be measured on the PR instead of first discovered in a dequeue. ⛔ No code change for the `allSixManifests()` drift this merge creates (#16654's helper is named for five CLI emissions plus the on-ramp, and this PR takes the CLI side to four). It is main's code now, a card of its own, and repairing it here would widen this PR past its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
…16483) (#16665) * feat(cli)!: retire `os create example`; the refusal names `os init` (#16483) The `example` template emitted a subset of what `os init` writes plus one README, the only template-level duplication #15531 found between the two scaffolder families. Removed under the #15531 ruling (batch #66, option B) with no alias and no deprecation window. The template is not merely deleted: `os create example` still answers, exits 1 and names `os init`, rather than falling through to `Unknown type:` and printing only the surviving roster. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ * docs(cli): make the retirement's citations match what is verifiable (#16483) Contract review on PR #16665, findings F1-F4. F1 — the "no alias, no deprecation window" ruling could not be located by the review: not in #15531's comments, not in ledger #12708 (whose earliest comment postdates the attributed date), not in the tree. The removal itself IS verified by the #15531 batch #66 entry, so create.ts and the e2e header now cite that and record the alias/window terms as recorded on card #16483, pending maintainer confirmation. The runtime message is deliberately unchanged: it describes what the code does, which is true whoever ruled it. F2 — `os init` does NOT write "the same objectstack.config.ts". The audit this card cites measures tsconfig.json byte-identical and the two manifests DIFFERENT (both ManifestSchema-valid). Runtime message and changeset now say "the same tsconfig.json and an equivalent objectstack.config.ts", and the changeset states the difference. F3 — `-t empty` writes five files and runs the install, so "objectstack.config.ts only" was wrong in the runtime message, the changeset and the doc callout this PR added. All three now read "config only, no src/objects". F4 — the changeset said the e2e file holds the doc pages (the docs pin is the separate queue-tier file) and that `--in-repo` is unchanged (its examples/ placement goes with the template). Both corrected. F5 is deliberately NOT acted on here: SCAFFOLD_TSX_RANGE stays, init.ts is held by #16654. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ --------- Co-authored-by: os-sales <sales@objectstack.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Fixes #16485
Item 4 of #15531, under the director seat's ruling (decision batch #66, 2026-09-07, option B).
npx create-objectstackdeclaredtypescript: ^6.0.0for a new project whileos initandos createdeclared^5.3.0from the shared emission policy, so two projects created the same day got different TypeScript majors depending on which documented entry point the reader followed.The range decision, measured rather than assumed
The card asked for the surviving range to be recorded, and the PM seat recorded
^5.3.0with one exception to look for: if the repo itself had moved to TypeScript 6, then^6.0.0would be the right value and the shared policy the stale one. Measured on this tree:typescriptdevDependency^6.0.3, in the root manifest and every workspace packageengines{ "node": ">=22.0.0" }— notypescriptkeycontent/docs/getting-started/index.mdxcontent/docs/deployment/troubleshooting.mdxNeither value is stale. They answer different questions, and the getting-started sentence is the one place the repo says so:
^6.0.3is what this monorepo builds with,5.3+is the floor a scaffolded project DECLARES to its own user — a support promise.create-objectstack's^6.0.0matched neither: it is not the repo's6.0.3pin either, andinit.tsalready records that it arrived in a commit whose message documents an@objectstack/*version sync and says nothing about TypeScript. So the shared policy's^5.3.0wins, as recorded, and no shared-policy bump is owed.What landed
scripts/sync-scaffold-emission-policy.mjsreadsSCAFFOLD_TYPESCRIPT_RANGEandSCAFFOLD_PNPM_RANGEout ofpackages/cli/src/commands/init.tsand stamps them into every bundled template'spackage.json.create-objectstack'sbuildruns it first, the waypackages/spec's runsgen:schema. The template set is DISCOVERED by walkingsrc/templates/, reusingsync-template-versions.mjs's walk rather than restating it.pnpm check:scaffold-emission-policy(--self-test+--check), wired as its own step inlint.yml. It reds when an inlined value disagrees with the policy source, and its failure text refuses the hand edit by name.packages/cli/test/scaffold-emission-policy.e2e.test.tsnow measures the on-ramp by driving it — spawningbin/create-objectstack.jsinto a throwaway directory and reading the emittedpackage.json— and compares its third-party ranges andengines.pnpmagainst the five manifests the two CLI commands render. It deliberately does not read the committed template as the expectation: a pin that read the generator's own output would be reading the same source it guards.create-objectstack(minor), stating what moves for a scaffolded project and why the value is generated.Why the generator and not the one-line edit: editing
^6.0.0to^5.3.0by hand ticks the acceptance box and leaves the defect intact — the two values would agree today and diverge again on the next policy move, silently, for the same structural reason (create-objectstackcannot import from@objectstack/cli; the dependency edge runs the other way and the npx package must not pull the CLI's closure).The check can go RED — two ablations, both proven on disk
Neither reads an exit code as evidence of the mutation: each counts the removed and the injected text on disk, records
git hash-object, restores under anEXIT/INT/TERMtrap and proves byte-identity against the HEAD blob before believing the restored green.A — perturb the INLINED value (
typescriptback to^6.0.0in the bundled template):The mutant blob
125997e869is byte-for-byte the pre-fix file this PR replaced, so the RED leg is the real defect and not a synthetic one.B — move the POLICY SOURCE and leave the on-ramp where it is, which is the future recurrence the card is about (
SCAFFOLD_PNPM_RANGE>=10.15to>=10.16; the on-ramp's built template is deliberately untouched):Exactly ONE of the ten cases reddened — the new pin — so the ablation is surgical and the pin is what caught it.
Before the fix,
--checkon unmodifiedmainexits 1 naming the live defect; that is the third red-direction reading and the one that motivated the shape.Verification
pnpm --filter create-objectstack test— 16 files, 203 passed;typecheckclean.pnpm --filter @objectstack/cli exec vitest run --project unit— 182 files, 2493 passed (6 expected-fail).pnpm --filter @objectstack/cli typecheck— clean, including the test layer (check:test-typecheck, so the new test file compiles).OS_TEST_TIERS=nightly ... --project integration test/scaffold-emission-policy.e2e.test.ts— 10 passed. This file carries the.e2ename, so it is a nightly-tier run; the per-PR guard against this defect class ischeck:scaffold-emission-policyinlint.yml.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran— 107 derived, 107 run, 0 NOT-MEASURED, 0 UNRUN. Five gates first read a mid-build tree (check:dts-closure,check:dual-build-cjs-loads,check:i18n,check:i18n-coverage,check:i18n-walk-parity, all exit 1/3 = NOT MEASURED); all five are green re-run after the dependency closure and@objectstack/cliwere built.eslint --no-inline-config --format jsonover the 9 changed source-shaped paths reports 9 entries, of which 4 are JSON files it declines ("File ignored because no matching configuration was supplied"), so 5 files were really linted — 0 errors, 0 rule warnings. The narrowing excludes nothing:eslint.config.mjsstates, with its own positive-control measurement, that this repo "runs oneeslint.config.mjs, which never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file" — verified by grep on this tree — so no edit in this diff can move the verdict on a file it does not touch. Measured atcb9f71bded.Wiring the gates asked for
check:cross-package-test-inputs—@objectstack/clinow declares the on-ramp'sbin/entry, its bundledpackage.jsonand the generator, mirrored intoturbo.json. Without it a template-only diff would leave@objectstack/cli#testhashing the same and replaying a cached green over exactly the divergence the pin exists to catch.check:merge-driver— everygen:name owes a recorded merge disposition. The bundledpackage.jsonis MIXED (the generator rewrites two values and reproduces none of the rest, and refuses a template that does not already declare them), so it isNOT_DRIVER_MANAGEDwith that reason rather than routed. The same gate counts generator NAMES, socreate-objectstack's build calls the script directly instead of declaring a secondgen:alias.Not governed, and clause 2 re-derived
The diff touches none of
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md. Clause 2 stays no, re-derived from the delivered diff: nothing underpackages/spec/src/**, no publishedskills/**change, and no acceptance set widened — the new exported symbols live in a repo build script that ships in no package, and a scaffolded project's dependency range is generated output rather than a published contract face.Docs-drift verdict — three pages listed, zero falsified
The drift bot anchored on
os init, i.e. on this diff touchingpackages/cli/src/commands/init.ts. That file's change is 100% comment lines —git difffiltered to non-comment, non-blank added/removed lines returns nothing (25 insertions / 10 deletions, all prose). No emitted value ininit.tsmoved: the paragraph that saidcreate-objectstack's^6.0.0"is deliberately NOT unified here" became false when it was unified, so it now records the generation mechanism and the range measurement instead.Listed, checked, and left alone:
content/docs/getting-started/your-first-project.mdxengines: { protocol: '^17' }typescript; the protocol stamp belongs tosync-template-versions.mjsand this diff does not move itcontent/docs/getting-started/examples.mdxtypescripthit is a fenced-block language tagcontent/docs/deployment/cli.mdxengines: { protocol: '^17' }content/docs/releases/v17.mdx(listed; not touched)engines.nodemoving>=18to>=22and theengines.protocolstamp; notypescriptrange and no scaffoldengines.pnpmHand sweep past the bot's blind spot (a page that states the rule by its inputs names no symbol this diff touched), over
content/**,skills/**and the package READMEs, fortypescript@, atypescriptrange literal, "TypeScript N.x" prose and scaffoldengines:content/docs/getting-started/index.mdx— "ObjectStack works with TypeScript 5.3+, but the project itself is built and tested against TypeScript 6.x". This diff moves the on-ramp INTO agreement with that sentence rather than away from it; it is also the page the existing pin already tiesSCAFFOLD_TYPESCRIPT_RANGEto. No edit.content/docs/deployment/troubleshooting.mdx— "TypeScript 5.3.0 or later". Same, agrees. No edit.验收备注
Filed:
objectstack-platformskill still teachestypescript: ^6.0.0— the one carrier of the retired scaffold range left after #16485 #16655 —skills/objectstack-platform/SKILL.mdline 613 declares"typescript": "^6.0.0"in the "Complete Working Example"package.jsonan AI agent following the skill writes. After this PR that is the only^6.0.0left in the tree, and it is the same defect class in a carrier cli:create-objectstackemits TypeScript^6.0.0whileos init/os createemit^5.3.0— generate the on-ramp's version policy from the same constants at build time (item 4 of #15531) #16485 did not enumerate: the@objectstack/*ranges in the same block are at^17.0.0(maintained), so thetypescriptline is the one value in it that is not. ⛔ Not fixed here —skills/**is a governed surface and a rider would convert this PR's landing path; it needs its own maintainer-merged PR.Noted, not filed:
content/docs/protocol/kernel/plugin-spec.mdxline 708 shows an illustrative pluginpackage.jsonwith"typescript": "^5.0.0"beside"@objectstack/core": "^2.0.0"— fifteen majors stale on the framework range. Not produced by any scaffolder (os create pluginemits the shared constant), not falsified by this diff, and its real problem is the@objectstack/*line rather than the TypeScript one. Documentation drift for a docs-accuracy sweep, not a defect this card creates.content/docs/getting-started/index.mdxline 185 answers "TypeScript version mismatch" withpnpm add -D typescript@^6, which a scaffolded project's^5.3.0does not admit. It is scoped one section above by the callout "pnpm and TypeScript versions only matter for the framework monorepo", it read the same way before this PR for the two CLI scaffolders that already emitted^5.3.0, and this diff neither created nor widened it. Prose scoping, deliberately left to the maintainer.packages/cli/test/scaffold-emission-policy.e2e.test.tscarries the.e2ename, so underscripts/nightly-tiers.mjsit runs nightly rather than in the merge queue. That is the cadence its five sibling assertions already had, and the per-PR guard against this defect class ischeck:scaffold-emission-policyinlint.yml.pnpm --filter @objectstack/cli typecheckreported TS7016 against@objectstack/rest's declarations whilepackages/rest/dist/index.d.tswas present on disk; the immediate re-run on the same tree was clean. A single unreproduced reading on a shared container is not a bug report.Generated by Claude Code