Skip to content

fix(create-objectstack): generate the on-ramp's version pins from the shared emission policy - #16654

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-16485-create-objectstack-version-policy
Sep 7, 2026
Merged

fix(create-objectstack): generate the on-ramp's version pins from the shared emission policy#16654
os-sales merged 2 commits into
mainfrom
claude/issue-16485-create-objectstack-version-policy

Conversation

@claude

@claude claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #16485

Item 4 of #15531, under the director seat's ruling (decision batch #66, 2026-09-07, option B).

npx create-objectstack declared typescript: ^6.0.0 for a new project while os init and os create declared ^5.3.0 from 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.0 with one exception to look for: if the repo itself had moved to TypeScript 6, then ^6.0.0 would be the right value and the shared policy the stale one. Measured on this tree:

reading value verdict
repo's own typescript devDependency ^6.0.3, in the root manifest and every workspace package the toolchain this monorepo builds ITSELF with
root engines { "node": ">=22.0.0" } — no typescript key says nothing about either range
content/docs/getting-started/index.mdx "ObjectStack works with TypeScript 5.3+, but the project itself is built and tested against TypeScript 6.x" states BOTH halves in one sentence, deliberately
content/docs/deployment/troubleshooting.mdx "TypeScript 5.3.0 or later for full type inference support" the same floor, stated again

Neither value is stale. They answer different questions, and the getting-started sentence is the one place the repo says so: ^6.0.3 is 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.0 matched neither: it is not the repo's 6.0.3 pin either, and init.ts already 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.0 wins, as recorded, and no shared-policy bump is owed.

What landed

  1. A build-time generator. scripts/sync-scaffold-emission-policy.mjs reads SCAFFOLD_TYPESCRIPT_RANGE and SCAFFOLD_PNPM_RANGE out of packages/cli/src/commands/init.ts and stamps them into every bundled template's package.json. create-objectstack's build runs it first, the way packages/spec's runs gen:schema. The template set is DISCOVERED by walking src/templates/, reusing sync-template-versions.mjs's walk rather than restating it.
  2. A drift check. pnpm check:scaffold-emission-policy (--self-test + --check), wired as its own step in lint.yml. It reds when an inlined value disagrees with the policy source, and its failure text refuses the hand edit by name.
  3. A three-scaffolder pin. packages/cli/test/scaffold-emission-policy.e2e.test.ts now measures the on-ramp by driving it — spawning bin/create-objectstack.js into a throwaway directory and reading the emitted package.json — and compares its third-party ranges and engines.pnpm against 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.
  4. A changeset for 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.0 to ^5.3.0 by 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-objectstack cannot 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 an EXIT/INT/TERM trap and proves byte-identity against the HEAD blob before believing the restored green.

A — perturb the INLINED value (typescript back to ^6.0.0 in the bundled template):

HEAD_BLOB=48dca9cd5c23c51dda404713c647b26c3881f8a5
BEFORE  policy-text=1  mutant-text=0  hash=48dca9cd5c23c51dda404713c647b26c3881f8a5
AFTER   policy-text=0  mutant-text=1  hash=125997e8695e824a02cc48613846b43e3eb9a0d2
MUTANT_GATE_EXIT=1
  ...blank/package.json: devDependencies.typescript is "^6.0.0" but the policy declares "^5.3.0"
RESTORED hash=48dca9cd5c23c51dda404713c647b26c3881f8a5  identical=yes   GIT_DIFF_HEAD_EMPTY=yes
RESTORED_GATE_EXIT=0

The mutant blob 125997e869 is 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.15 to >=10.16; the on-ramp's built template is deliberately untouched):

BEFORE  orig-text=1  mutant-text=0  hash=fb2602d9db2ee98054468f8461c41737b54b9e05
AFTER   orig-text=0  mutant-text=1  hash=9a7d2ad539387a92a780d6e91fd24c1cd687aba7
ON_RAMP_DIST_UNTOUCHED=1
MUTANT_GATE_EXIT=1   ...blank/package.json: engines.pnpm is ">=10.15" but the policy declares ">=10.16"
MUTANT_PIN_EXIT=1    x emits the exported TypeScript and pnpm constants, not a restatement of them
                     Tests  1 failed | 9 passed (10)
RESTORED hash=fb2602d9db2ee98054468f8461c41737b54b9e05  identical=yes   GIT_DIFF_HEAD_EMPTY=yes
RESTORED_GATE_EXIT=0   RESTORED_PIN_EXIT=0   Tests  10 passed (10)

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, --check on unmodified main exits 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; typecheck clean.
  • 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).
  • The emission pin, driven: OS_TEST_TIERS=nightly ... --project integration test/scaffold-emission-policy.e2e.test.ts10 passed. This file carries the .e2e name, so it is a nightly-tier run; the per-PR guard against this defect class is check:scaffold-emission-policy in lint.yml.
  • Gate union, reconciled as an external artefact: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran107 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/cli were built.
  • Lint, narrowed and the narrowing measured rather than asserted: eslint --no-inline-config --format json over 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.mjs states, with its own positive-control measurement, that this repo "runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) 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 at cb9f71bded.

Wiring the gates asked for

  • check:cross-package-test-inputs@objectstack/cli now declares the on-ramp's bin/ entry, its bundled package.json and the generator, mirrored into turbo.json. Without it a template-only diff would leave @objectstack/cli#test hashing the same and replaying a cached green over exactly 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, and refuses a template that does not already declare them), so it is NOT_DRIVER_MANAGED with that reason rather than routed. The same gate counts generator NAMES, so create-objectstack's build calls the script directly instead of declaring a second gen: 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 under packages/spec/src/**, no published skills/** 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 touching packages/cli/src/commands/init.ts. That file's change is 100% comment linesgit diff filtered to non-comment, non-blank added/removed lines returns nothing (25 insertions / 10 deletions, all prose). No emitted value in init.ts moved: the paragraph that said create-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:

page what it says about versions verdict
content/docs/getting-started/your-first-project.mdx "A standalone project does not need pnpm, TypeScript pre-installed... TypeScript comes in as a dev dependency"; engines: { protocol: '^17' } accurate and version-free about typescript; the protocol stamp belongs to sync-template-versions.mjs and this diff does not move it
content/docs/getting-started/examples.mdx no version statement at all — every typescript hit is a fenced-block language tag nothing to falsify
content/docs/deployment/cli.mdx "TypeScript setup", "TypeScript availability", engines: { protocol: '^17' } prose about the language, not a range
content/docs/releases/v17.mdx (listed; not touched) states engines.node moving >=18 to >=22 and the engines.protocol stamp; no typescript range and no scaffold engines.pnpm nothing this diff falsifies — no docs-only follow-up owed

Hand 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, for typescript@, a typescript range literal, "TypeScript N.x" prose and scaffold engines:

  • 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 ties SCAFFOLD_TYPESCRIPT_RANGE to. No edit.
  • content/docs/deployment/troubleshooting.mdx — "TypeScript 5.3.0 or later". Same, agrees. No edit.
  • Two pre-existing items neither created nor falsified by this diff, recorded below rather than ridden on.

验收备注

Filed:

Noted, not filed:

  • content/docs/protocol/kernel/plugin-spec.mdx line 708 shows an illustrative plugin package.json with "typescript": "^5.0.0" beside "@objectstack/core": "^2.0.0" — fifteen majors stale on the framework range. Not produced by any scaffolder (os create plugin emits 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.mdx line 185 answers "TypeScript version mismatch" with pnpm add -D typescript@^6, which a scaffolded project's ^5.3.0 does 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.ts carries the .e2e name, so under scripts/nightly-tiers.mjs it 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 is check:scaffold-emission-policy in lint.yml.
  • One transient during verification: a first pnpm --filter @objectstack/cli typecheck reported TS7016 against @objectstack/rest's declarations while packages/rest/dist/index.d.ts was 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

… 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
@github-actions github-actions Bot added size/l ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 1 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx (via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/examples.mdx (via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/your-first-project.mdx (via os init (command, read off packages/cli/src/commands/init.ts))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via os init (command, read off packages/cli/src/commands/init.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bc0ac1d1f5775659e72ad35452c463443a71f1bepackageMentionDocs.

Which tree this was computed on

This run read content/docs from 9785d6401c2ca769eb24abc23b99f70131cdeb48 — the merge of head cb9f71bded69434e9e5098e84bfdd87ca0745910 into base bc0ac1d1f5775659e72ad35452c463443a71f1be, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs bc0ac1d1f5775659e72ad35452c463443a71f1be → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-sales
os-sales marked this pull request as ready for review September 7, 2026 16:02
@os-sales
os-sales enabled auto-merge September 7, 2026 16:02
@os-sales
os-sales added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit 813d6c5 Sep 7, 2026
42 checks passed
@os-sales
os-sales deleted the claude/issue-16485-create-objectstack-version-policy branch September 7, 2026 16:28
os-sales added a commit that referenced this pull request Sep 7, 2026
…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
os-sales added a commit that referenced this pull request Sep 7, 2026
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
github-merge-queue Bot pushed a commit that referenced this pull request Sep 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants