refactor(cli): declare the scaffold emission policy once — os init and os create stop restating the tsconfig and the devtool ranges - #15974
Conversation
… module `os init` and `os create` each restated the third-party ranges and the `tsconfig.json` an emitted project carries. Measured on the tree, the TypeScript range was written in six places across three scaffolders and had split into three values (`^5.3.0` / `^5.8.0` / `^6.0.0`); vitest into two. Both CLI scaffolders now read one definition per value. Surviving values: `^5.3.0` for TypeScript (the floor two live doc pages already state) and `^4.0.0` for vitest (no recorded decision for either; `^4.0.18` claimed a patch-level floor nothing justifies). Neither changes what an emitted project installs — `^5.3.0` and `^5.8.0` both resolve to typescript 5.9.3, `^4.0.0` and `^4.0.18` both to vitest 4.1.11. `create-objectstack`'s `^6.0.0` is deliberately untouched: it cannot import from `@objectstack/cli` (the dependency edge runs the other way), and unifying it would change what a scaffolded project installs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…issions Both ends of every expectation are derived — from the renderers, from the other scaffolder, or from the doc pages that already state the TypeScript floor — so a transcription cannot go green on a half-edited tree. `os init` writes its `tsconfig.json` inside `run()`, so that half is measured by driving the real command into a throwaway directory and reading the bytes off disk; an exported renderer nobody calls would pass every in-process assertion. The two doc pages the floor case reads are declared in `scripts/cross-package-test-inputs.mjs` and mirrored into `turbo.json`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
📓 Docs Drift CheckThis PR changes 1 package(s): 9 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 5599a4e0b459292d81addf7695e0d4deda430712 && git checkout 5599a4e0b459292d81addf7695e0d4deda430712
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f50c394da55846da8d38f1e1efadbc24faa26ce2 f43dae79fa8e86c7181ca0a0efaddd49a8adadea && git checkout -B drift-repro f50c394da55846da8d38f1e1efadbc24faa26ce2 && git merge --no-ff f43dae79fa8e86c7181ca0a0efaddd49a8adadea
node scripts/docs-audit/affected-docs.mjs --json f50c394da55846da8d38f1e1efadbc24faa26ce2
|
os-litant
left a comment
There was a problem hiding this comment.
Contract review (clause ②) — PR #15974 at 65bafb464e4 — verdict: CHANGES REQUESTED
Implemented-by: claude/issue-15818-extract-scaffold-emission-policy
Reviewed-by: session_01D47qPfEWVPmhguWgBZCi5N
Posted as a COMMENT review on purpose: the clause-② carrier is the needs:contract-review label, which the protocol says never migrates into a PR review state (no Request Changes), and GitHub would refuse an APPROVE from the PR's own account in any case. Labels are the seat's stroke and are untouched here.
Tier reading. Taken from my own subagent transcript, subagents/agent-ae2a533a64a56f10d.jsonl (meta "Contract review PR 15974"; it carries this task's fingerprint 65bafb464e4), not the dispatching session's file. All 10 harness-stamped type:"assistant" records carry the tier that export const CONTRACT_REVIEW_TIER declares at scripts/pm/dispatch-gates.mjs:8659 (21 occurrences of the identifier, one declaration) — equal; a sibling agent file read as control stamps differently, so the read discriminates on file identity. Fuse holds.
The verdict in one paragraph. Every clause-② claim this card was fenced on holds at source — the Zone-1 fence, the argued surviving values, the structural exclusion of the third scaffolder, and the correction to the triage claim (all four measured below, independently). What blocks is outside the limbs: the delivered head reddens a CI gate through its own diff (Lint & Repo Gates → check-ci-filter-parity --self-test, red at the head and green on origin/main), and the shipped provenance narrative carries a false history claim about the third scaffolder that the seat's own ruling comment has already repeated. Both need a patch commit; since that moves the head, clearing the label now would be wasted. The clause-② substance is accepted as of 65bafb464e4, so the re-review of the patched head can be a delta review.
Blocking — what the patch round must carry
1. check-ci-filter-parity --self-test is red at the head, by this PR's hand. CI Lint & Repo Gates failed at 17:19:20Z: "1 of 45 assertion(s) failed — rolling crosspkg back to its pre-#10015 list uncovers the ten it fixed plus … #14824's three — got 21" (the pin is === 19). Reproduced locally at 65bafb464e4: exit 1, same line. Control on a second worktree at origin/main (aa6ba0623a9): exit 0, 45/45. The discriminating delta is exactly the two content/** declarations this PR adds for @objectstack/cli in scripts/cross-package-test-inputs.mjs (content/docs/deployment/troubleshooting.mdx, content/docs/getting-started/index.mdx): 0 hits on main, 2 at the head, and the rollback removes content/**, so both go uncovered and move the count 19 → 21. The pin's own comment states the rule and the precedent — "#14824's three: the three doc pages that card declared for @objectstack/cli … recorded here by name … a declaration added under a new root moves it and is recorded here by name." This card owes "#15818's two" the same way: in scripts/check-ci-filter-parity.mjs, the assertion (=== 19 → === 21, and the message at ~L690), its mirrored description (~L797), and a comment paragraph naming the two pages. The PR does not touch that file today.
Why the dev's 74/74 did not see it: the --self-test spelling is outside the 74-family runnable total. --commands line 7 is node scripts/check-ci-filter-parity.mjs (plain); the human rendering lists node scripts/check-ci-filter-parity.mjs --self-test under "Artifact rosters — 37 famil(ies) whose silent verdict is a fact about a LIST", with the tool's own "⛔ never read their silence as a clearance — run them, or read them." The harvest was faithful; the block the tool says to run separately is where the red gate lives. (A note for the seat, not a re-litigation.)
2. A false history claim in shipped comments and the test header. init.ts:239-241, create.ts:68, scaffold-emission-policy.e2e.test.ts:16 and the PR body say the third value "arrived with the bundled template on 2026-05-25" and that its typescript line "has never been touched since" — 102 days. Measured: dbb54e12f0c (2026-05-25) added templates/blank/package.json with "typescript": "^5.3.0"; eaff01425b7 (PR #2907, 2026-07-14) changed it to "^6.0.0" (git log -L on the line shows exactly those two commits). So the three-value split is 53 days old at 2026-09-05, and from 2026-05-25 to 2026-07-14 the third scaffolder agreed with os init. #2907's message reasons only about the @objectstack/* ranges ("pinned ^6.0.0 while the registry publishes 14.x"); nothing in that diff reasons about a TypeScript version, so the template's ^6.0.0 is a ride-along value, not a recorded floor decision. That strengthens the surviving-value argument — but the narrative that ships must say the true history. The same commit also created the syncObjectStackDeps fixture lines 677/683 (item 4 below).
3. One number, please. The changeset (L7) and the test header (L15) say 211 days; the PR body says 210. 2026-02-07 → 2026-09-05 is 210. The changeset is CHANGELOG-bound.
Clause ②, per limb
- Mechanical: NO — holds.
@objectstack/cli'sexportsmap is.,./console,./hook-body,./package.json;src/index.ts:14re-exports only theInitCommanddefault fromcommands/init.js, so the newSCAFFOLD_*/renderScaffoldTsconfigexports are module-internal. Control after a real build:dist/index.d.ts,dist/index.js,dist/hook-body.js,dist/utils/console.jscarry 0 occurrences of either name. No new key on an emitted payload: all five emissions driven at the head, every emittedtsconfig.jsonequals the renderer output byte-for-byte; only threepackage.jsonvalues move. - Conformance: YES — correctly graded under the not-clear doctrine, and the content is accepted. The declared floor on three emitted
package.jsonfiles widens (^5.8.0→^5.3.0×2,^4.0.18→^4.0.0×1). Checklist ① derived judgments: that is the whole accept-set change; nothing installed changes (fence below). ② semver:@objectstack/clipatch — consistent. ③ boundary flags: the third-scaffolder question was ruled A by the seat on the card; the two handed-back findings (#15976,sanitizeNamespace's second definition) are not re-opened here.
The Zone-1 fence — HOLDS (resolution read 2026-09-05T17:09:44Z, https://registry.npmjs.org/)
typescript@^5.3.0 → 5.9.3; typescript@^5.8.0 → 5.9.3; vitest@^4.0.18 → 4.1.11; vitest@^4.0.0 → 4.1.11. Controls in the same run, discriminating on the range rather than replaying a cache: typescript@'^5.3.0 <5.9' → 5.8.3 and vitest@'^4.0.0 <4.1' → 4.0.18 (different answers to different ranges); dist-tags read alongside: typescript latest 7.0.2 (so ^5 caps at 5.9.x), vitest V4 = 4.1.11. Registry state can move; this is the reading at that timestamp. On disk at the head, the five driven emissions declare typescript ^5.3.0 ×5 and vitest ^4.0.0 ×2.
The declared floor is also honest, which #2907's history makes load-bearing: TypeScript 5.3.3 (installed from the registry) against the repo's 6.0.3, on all five freshly emitted projects with the workspace packages linked — init empty 0/0 identical; init app 1 error under each, identical text (the #15976 Data.Object error); init plugin the same; create plugin 0/0 identical; create example 0/0 identical. Control: an injected const ctrl: number = "…" is TS2322-red under both compilers.
Surviving-value decision — both citations hold at source
content/docs/getting-started/index.mdx:183 reads verbatim "ObjectStack works with TypeScript 5.3+, but the project itself is built and tested against TypeScript 6.x"; content/docs/deployment/troubleshooting.mdx:359 reads "TypeScript 5.3.0 or later for full type inference support." Both predate the PR (it touches no content/docs): written 2026-02-12 (5635a41b539, 304351db56a), the first re-affirmed by the 2026-06-15 docs audit (#1866, 7d94d2f1d8a). ^5.8.0 is unrecorded: a repo-wide sweep hits only the audit table docs/audits/2026-09-os-create-os-init-convergence-measurement.md:220 (a measurement row) and this PR's own comments. The test's docs regex, run by me: 1 match per page, both → 5.3; control: the "TypeScript 6.x" sentence on the same page does not match. vitest: both values born in 338e68d2564; ^4.0.18 is the narrower — confirmed. "Three of five already carried ^5.3.0": init.ts:384/474/548 at merge-base — confirmed.
create-objectstack exclusion — HOLDS, structural
packages/cli/package.json dependencies carry "create-objectstack": "workspace:*"; packages/cli/src/commands/init.ts:22 is import { summarizeTree, describeEntry } from 'create-objectstack/created-summary' — the edge runs cli → create-objectstack, so a reverse import is a cycle. packages/create-objectstack/package.json dependencies are exactly chalk, commander; no @objectstack/cli import anywhere in its src (every hit is a comment or the template's own package.json); copyDir() at src/template-copy.ts:35, called from src/index.ts:134; sync-template-versions.mjs rewrites @objectstack/* ranges only. @objectstack/cli has 60 direct prod dependencies — the "~50-package closure" is if anything understated.
template-consistency.test.ts:677/683 — the dev is right
The lines sit inside describe('syncObjectStackDeps') → it('rewrites @objectstack/* ranges in deps and devDeps'); the fixture's typescript: '^6.0.0' is asserted unchanged after the sync — a leaves-alone assertion, not a pin on the emitted template. The file is untouched by this PR. Control sweep: no other test under packages/create-objectstack or packages/cli asserts the blank template's typescript value. And those fixture lines were created by #2907, the same commit that moved the template to ^6.0.0.
Drift figures and the import list — confirmed
338e68d2564 (2026-02-07) wrote ^5.8.0/^4.0.0 into create.ts and ^5.3.0/^4.0.18 into init.ts in one commit — 210 days to today. Six write points at merge-base (create.ts:260/261/375/376, init.ts:384/474/475/548, blank/package.json:26). create.ts's import from ./init.js carries six pre-existing symbols including NPM_PACKAGE_NAME_MAX_LENGTH and validateProjectName (cee39617591, #15893).
Verification record
- Gate union: the script's own line reads
Reconciliation — 74 famil(ies)(63 by path + 6 by kind + 7 declared whole-tree, 2 reached both ways);--commandsprinted exactly 74 lines; all 74 executed in a dedicated worktree at the head, exit codes captured before any pipe: 72 exit 0; 2 exit 3 —check:dual-build-cjs-loadsandcheck:i18n-coverage, bothPREREQUISITE NOT MET(packages outside the cli closure have nodist/: connector-slack, embedder-openai, knowledge-memory, connector-mcp, …). After a fullpnpm build(72 tasks, the four named packages'dist/present as control) both re-ran exit 0 —check:dual-build-cjs-loads(103 published require entry points across 66 packages load) andcheck:i18n-coverage(13 configs, none new). 74/74 green, the same account the dev gave. - The pin: baseline 6/6 green. My own ablation, predictions written before mutation, each leg proved on disk by an injected-marker count and restored from a
git hash-object -wblob: M1 — restatetypescriptas a literal at bothcreate.tssites (2 replacements, 2 markers): 2 failed / 4 passed, exactly the two named cases (declares exactly one range per third-party dependency,emits the exported constant rather than a literal). M3 — moveSCAFFOLD_TYPESCRIPT_RANGEto^5.8.0(1 replacement, 1 marker): 1 failed / 5 passed, the docs case (agrees with what the scaffolders emit). After each leg:git diff HEADon both files is empty and both blob hashes equal HEAD's (d86240fc89f…,5e8581d2255…). No leg was VOID. pnpm --filter @objectstack/cli typecheckgreen includingcheck:test-typecheck; the new file is in the test program (--listFiles: 1 hit).git merge-tree --write-tree origin/main 65bafb464e4(main 13 commits ahead ataa6ba0623a9): clean.- CI, re-read at 17:34Z: 47 check runs —
Lint & Repo Gatesfailure (above);Test Core (1/6)finished green at 17:29:07Z; every other check success or skipped. One red check, and it is this PR's.
NOT MEASURED (by name): node scripts/pm/check-clause2-carriers.mjs --pair 15974 — exit 3, PREREQUISITE NOT MET (this sandbox's token is refused, HTTP 403); not a clean reading. The six value-bearing families the tool places outside the 74 (check-cross-package-test-inputs.mjs --union-into …, check-shard-attestation.mjs …, and their siblings) — not runnable locally by the tool's own statement. The cli integration tier as a whole — not run here (the dev declared the same narrowing).
For the seat (not re-litigated here)
The card's ruling comment repeats "its typescript line has never been touched since" and the 102-day figure; the record should carry the correction above, and the fact that the third scaffolder's ^6.0.0 is not a recorded floor decision is material to the separate ruling on unifying it. No code written, no file changed on any branch; the ablation's two mutated files were restored to their HEAD blobs (verified by hash) before the review worktrees were removed.
Generated by Claude Code
…w declarations in the CI filter-parity pin Two review findings from PR #15974. 1. The head reddened `check-ci-filter-parity --self-test`: the pre-#10015 rollback now uncovers 21 globs, not 19. Measured, the delta is exactly the two `content/docs` pages this branch declared for `@objectstack/cli` (troubleshooting.mdx, getting-started/index.mdx) — an origin/main control yields 19 with an empty symmetric difference otherwise. Both are recorded by name, the way the pin's own comment prescribes for #14824's three, rather than the count merely being bumped. 2. The shipped history claim was wrong. `dbb54e12f0c` (2026-05-25) added the bundled template at `^5.3.0`, not `^6.0.0`; `eaff01425b7` (#2907, 2026-07-14) moved it to `^6.0.0` and recorded no reasoning about TypeScript — in that same one-file diff the five `@objectstack/*` ranges move `^6.0.0` to `^14.0.0` while the `typescript` line moves onto the `^6.0.0` they are vacating. So the three-value split is 53 days old, not 102, and the two-value split is 210 (the changeset said 211). Corrected in init.ts, create.ts, the pin's header and the changeset, which now agree. A dated provenance claim written into source comments is exactly the restated fact nothing checks that this card exists to close; shipping a wrong one inside the fix would have been self-refuting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
os-litant
left a comment
There was a problem hiding this comment.
Contract re-review (clause ②, scoped) — PR #15974 at 78fd6b2ade0 — verdict: CLEARED
Implemented-by: claude/issue-15818-extract-scaffold-emission-policy
Reviewed-by: session_01D47qPfEWVPmhguWgBZCi5N
Posted as a COMMENT review on purpose: the clause-② carrier is the needs:contract-review label, which never migrates into a PR review state, and GitHub refuses an APPROVE from the PR's own account in any case. Labels are the seat's stroke and are untouched here.
Scope. A delta review of the two items the review at 65bafb464e4 blocked on (#15974 (review)). Everything that review verified — both clause-② limbs, the Zone-1 no-user-visible-change fence, the two doc citations, the create-objectstack cycle, the template-consistency.test.ts adjudication — is taken as settled and not reopened. The delta 65bafb464e4..78fd6b2ade0 is five files: scripts/check-ci-filter-parity.mjs (three hunks), and comment/prose-only changes to init.ts, create.ts, the test header and the changeset. Nothing fenced moved: at the head SCAFFOLD_TYPESCRIPT_RANGE = '^5.3.0' (init.ts:297) and SCAFFOLD_VITEST_RANGE = '^4.0.0' (init.ts:300); content/** untouched; nothing under packages/create-objectstack touched; #15976, #15530, #15817 and the third-scaffolder unification still out.
Item 1 — the parity pin: RESOLVED
Recorded by name, not a bare bump. The diff to scripts/check-ci-filter-parity.mjs is: the pin === 19 → === 21 with its message extended by plus #15818's two; a new for loop over content/docs/deployment/troubleshooting.mdx and content/docs/getting-started/index.mdx asserting each is in uncoveredGlobs(preFix) (-- and #15818 added the TypeScript-floor page ${page}, by name), sitting inside battery (6) the real tree (opened at L651, the next battery opens at L743); the verdict description at ~L812 mirrored; and a comment paragraph stating why both pages are declared, in the shape the #14824 paragraph uses.
The two names are this branch's declarations. scripts/cross-package-test-inputs.mjs adds exactly those two paths under @objectstack/cli, mirrored into turbo.json. I drove the pin's own rollback fixture (fixtureWorkflow({ core: real.filters.core, crosspkg: ['scripts/**'] })) outside the self-test against both trees' declaration tables, from a scratch script importing judge from each tree: head table 21, origin/main table 19; only-at-head = exactly ['content/docs/deployment/troubleshooting.mdx', 'content/docs/getting-started/index.mdx']; only-at-main = []. The sizes are script-independent — head's judge and main's judge agree on both tables — so the delta is the declaration table and nothing else.
Floor, not equality — read at source, not from the claim. scripts/check-ci-filter-parity.mjs L151-153: "The counts are a FLOOR, not an equality — adding cases is ordinary work and must not red." The consumer at L784 is if (count >= SELF_TEST_BATTERIES[name]) continue; — >=. The only other pin on that ledger is SELF_TEST_BATTERY_FLOOR = 7 (~L166), checked as declaredBatteries.length < SELF_TEST_BATTERY_FLOOR — also a floor, on roster size. The printed total (${checked} assertions, L808) is asserted against nothing. The diff has zero changed lines mentioning SELF_TEST_BATTERIES. No ratchet moved; 45 → 47 needed no ledger edit, and that is now a source reading.
Both verdict lines, re-run by me, exit codes captured before any pipe (dedicated worktree at 78fd6b2ade0, after pnpm install --frozen-lockfile — without it a fresh worktree answers exit 3 PREREQUISITE NOT MET — the dependency yaml is not installed, which is NOT MEASURED, not red):
node scripts/check-ci-filter-parity.mjs --self-test→ EXIT 0 ·✓ check-ci-filter-parity --self-test: 47 assertions — … the pre-#10015 rollback uncovering the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one plus #14561's one plus #14824's three plus #15818's two, and the CI wiring read out of lint.yml.node scripts/check-ci-filter-parity.mjs→ EXIT 0 ·OK: all 151 declared cross-package glob(s) (104 unique) are covered by \core` or `crosspkg`, every `crosspkg` entry still covers one, and the `test` job's `if:` still names both filters.`- Control, a second worktree at
origin/main(f7db8f4fd26), same run:--self-test→ EXIT 0, 45 assertions, message ends at#14824's three; bare → EXIT 0, 149 glob(s) (102 unique). 151−149 = 2 and 104−102 = 2: the control discriminates on the axis under test (the declaration table) and on freshness (main's own script and table, not a replayed answer). - CI agrees at the head: step 125
CI filter parity (cross-package Layer C)ofLint & Repo Gates→ success at 18:03:55Z.
Item 2 — the history claim: RESOLVED
Three commits, confirmed with -G and -L, not -S.
338e68d2564(2026-02-07, "feat(cli): restructure commands and add new functionalities"): writestypescript: '^5.8.0'/vitest: '^4.0.0'intocreate.tsand'typescript': '^5.3.0'/'vitest': '^4.0.18'intoinit.tsin one commit. 2026-02-07 → 2026-09-05 = 210 days.dbb54e12f0c(2026-05-25): addspackages/create-objectstack/src/templates/blank/package.jsonwith"typescript": "^5.3.0"— the tree is still two-valued.eaff01425b7(#2907, 2026-07-14):"typescript": "^5.3.0"→"^6.0.0"in that file. 2026-07-14 → 2026-09-05 = 53 days.git log -Lon that line lists exactly these two commits.- The blind spot reproduces:
git log -S'"typescript"'over the same paths listsdbb54e12f0c(andd39e1a9dfb3) and noteaff01425b7;-Glists it. (For the record, 2026-05-25 → today is 103 days, so the old "102" was wrong even on its own terms.)
Five places, one story. init.ts ~L237-253 (the three commits by hash, 210 / 53, and the ride-along paragraph), create.ts:68-69 ("the two CLI values 210 days apart, the third 53 and recorded nowhere"), the test header L15-19 (hashes, 210, 53), the changeset L7 ("stayed apart for 210 days, and the third value is 53 days old … a commit that records no reasoning about TypeScript"), and the PR body's dated table. git grep at 65bafb464e4 finds 211 (changeset, test header), 102 days (create.ts, test header) and "arrived with the bundled template on 2026-05-25" (init.ts); at 78fd6b2ade0 none of those strings survive. The changeset and the PR body agree — 210, 53, the same three commits, the same no-reasoning clause — and 211 → 210 landed in the changeset (CHANGELOG-bound).
The ride-along, refined — a correction to "swept along by a find-replace". The template file's whole diff in eaff01425b7 is exactly six changed lines: five @objectstack/* ranges ^6.0.0 → ^14.0.0, plus typescript ^5.3.0 → ^6.0.0. The commit message, its changeset (.changeset/create-objectstack-current-major.md: "had ^6.0.0 ranges frozen in while the registry was publishing 14.x") and the PR #2907 body all reason only about the @objectstack/* majors and the template's API drift (Field.longText, api.rest, sharingModel); TypeScript is named in none of them. But it is not a find-replace: a literal ^6.0.0 → ^14.0.0 substitution cannot produce ^5.3.0 → ^6.0.0, and the same commit's new syncObjectStackDeps fixture writes typescript: '^6.0.0' and asserts it survives the sync — so someone typed that value and recorded no reason. The nearest thing to a mechanism in the diff is the package's own devDependency "typescript": "^6.0.3", which only gained a trailing comma in that commit — an alignment by eye, not a floor decision. So: an undocumented ride-along inside a dependency-refresh commit, not a tool sweep and not a recorded decision. That is a different thing from a tool changing it unattended, and it is what the card's surviving-value argument needs.
Landing pre-checks — totals confirmed, runs partial
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsat the head prints exactly 77 lines; the human rendering's own lines readReconciliation — 77 famil(ies)(64 by path + 8 by kind + 7 declared whole-tree, 2 reached both ways) andArtifact rosters — 36 famil(ies). The families that entered because the diff touchesscripts/check-ci-filter-parity.mjsare in the harvest: lines 7-8 (check-ci-filter-parity, bare and--self-test), 40 (bare-root-worklist --self-test), 64 (check:pm-dispatch-gates).- Driven in the head worktree, exit codes captured before any pipe, per-command output kept: 63 of 77 union families measured — 58 exit 0; the other five are refusals, not reds:
check:dts-closure,check:dual-build-cjs-loads,check:i18n("the workspace CLI is not built"),check:i18n-coverageat exit 3PREREQUISITE NOT MET, andcheck:i18n-walk-parityat exit 1 whose own verdict line isPREREQUISITE NOT MET: the workspace spec package is not built. No workspace build was made here; none of the five reads a file the delta touches; the implementer reports all green after a build.bare-root-worklist --self-test→ exit 0. pnpm check:merge-driver→ exit 0 at the head and atorigin/main(✓ check-regen-pending self-test passed.) — see the next section.git merge-tree --write-tree origin/main 78fd6b2ade0(main 15 commits ahead): clean.
Main-wide CI red at Merge-driver wiring gate — not this diff's, and not part of the verdict
Lint & Repo Gates is the only red check on the head (37 check runs: Test Core 1-6, every type-check job, Dogfood, changeset, size, both claim guards and the Part-of guard are success). Its failing step is 141 Merge-driver wiring gate (pnpm check:merge-driver → node scripts/check-regen-pending.mjs --self-test, "✗ self-test failed -- 1 failure(s) (cases and floor)"; the failing cases are the "a gate that cannot LOAD is PREREQUISITE NOT MET" / "a gate whose RUNNER is not installed" group); steps 142-148 were skipped behind it. Evidence it is main-wide:
- Main's own push run of the same workflow at
f7db8f4fd26— run 33981169123 (Lint & Type Check, 17:30-17:51Z), which is exactly this review's control commit — fails at the same step 141, with the parity step 125 success. The run before it on main (aa6ba0623a9, run 33979882868) also concludes failure. - The PR's run is 33981886973 (job 101348455384, 17:44-18:05Z): same step, same failure text.
- This PR touches neither
scripts/check-regen-pending.mjsnorscripts/git-merge-regen.mjs(git diff --stat origin/main...78fd6b2ade0on both: empty).check-regen-pending.mjslast moved on main at 08:15Z today (fc20f7bda4e, #15785). - Locally the same command exits 0 at both trees, so the failure is runner-environmental on top of main, not a property of either tree as read here.
This does not touch the clause-② verdict; nothing was re-run speculatively or repaired for it. It is named because a main-wide Lint & Repo Gates failure is a candidate explanation for a PR leaving the merge queue without merging.
Tier reading
Taken from my own subagent transcript, subagents/agent-a670f1f20bdb38df3.jsonl (meta description "Re-review PR 15974 patch round"; it carries this task's fingerprint — the scoped re-review brief and 78fd6b2ade0 — and no other file in the directory does), not the dispatching session's file. All 32 harness-stamped type:"assistant" records carry the literal that export const CONTRACT_REVIEW_TIER declares at scripts/pm/dispatch-gates.mjs:8659 — equal. Controls in the same read: the previous reviewer's file agent-ae2a533a64a56f10d.jsonl stamps the same literal but lacks the fingerprint (discriminates on file identity); two os-dev transcripts stamp a different literal on every record (discriminates on stamp value). Fuse holds.
NOT MEASURED (by name)
- 14 union families the local run had not reached when this was posted:
check:pm-dispatch-gates(CI step 31PM dispatch-gates self-test→ success at the head),check:pnpm-filter-targets,check:published-files,check:query-options-erasure,check:ratchet-remedy-authority,check:refd-timer-probe,check:slot-lookup,check:test-source-alias,check:turbo-task-graph,check:type-check-coverage,check:type-check-debt,check:type-source-resolution,check:watch-hint-literal,check:where-matcher. None reads a file the delta touches. - The five build-dependent families above (exit 3, or an own
PREREQUISITE NOT METline). - The 36-family artifact-roster block — not reached locally. CI's own wired runs at the head cover the three the implementer flagged:
Part-of PR must not also close its card→ success at 18:27:19-34Z, after the final body edit at 18:27:12Z — socheck-partof-closing-keywordwas measured on this body text by CI, not by me (api.github.comanswers this sandbox 403 with and without the token, so no body fetch);No other open PR may claim the same single-writer path→ success (thecheck-single-claim-pathsgate, NOT WIRED locally);check:react-declaration-parity— not run here, and its trigger (.objectui-sha) is untouched. node scripts/pm/check-clause2-carriers.mjs --pair 15974— not run (token refused in this sandbox, as the previous review recorded).- The cli integration tier as a whole; the workspace build.
Verdict: CLEARED. Both blocking items are resolved at 78fd6b2ade0; clause ② may be stripped by the seat. No code written, no file changed on any branch; two review worktrees under the scratchpad, touched only by pnpm install.
Generated by Claude Code
|
Addendum to the re-review at Union, 77/77 measured — 70 exit 0, 0 red. The seven non-zero exits, each by its own verdict line (exit codes captured before any pipe):
Artifact-roster block, 36/36 measured — 32 exit 0, 0 red. Four NOT MEASURED, each by its own line: Nothing changed on any branch; the review worktrees were touched only by Generated by Claude Code |
Clause ② carriers cleared — contract review PASSContract review returned CLEARED at head Per the maintainer ruling of 2026-08-31, a PASS verdict is discharged by the ruling seat stripping
Both blocking items from the first round are resolved at this head:
Generated by Claude Code |
Part of #15818 — item (1) of option B, the half with no user-visible change.
os initandos createeach wrote thetsconfig.jsonand the third-party dependency ranges a new project receives, in their own words. This makes that one declaration, in the module the second scaffolder already imports from.Not closing the card: the third scaffolder's
^6.0.0is deliberately left alone and the reasoning is below, so the PM re-triages rather than the merge deciding it silently.Patch round (contract review). Two items, both fixed at
78fd6b2ade0: this branch reddenedcheck-ci-filter-parity --self-testby its own diff, and the dated provenance claim it shipped was wrong. Both are written up in place below rather than appended, so the body says one thing. The surviving-value decisions are unchanged.The measurement this rests on, re-derived on today's tree
The card's claim reproduces, and the numbers are worse than it wrote:
os init—init.ts:384/474/548^5.3.0(3 write points)^4.0.18(1)os create—create.ts:260/375^5.8.0(2 write points)^4.0.0(2)create-objectstack—templates/blank/package.json:26^6.0.0(1)Six write points, three values, spanning one TypeScript major. Dated off
git log -Gover the three files, as of 2026-09-05:338e68d2564^5.3.0and^5.8.0in the same commit — the two-value split, 210 days olddbb54e12f0c^5.3.0, so the tree is still two-valuedeaff01425b7^5.3.0→^6.0.0— the third value, 53 days old^6.0.0on 2026-05-25 and that its typescript line had never been touched since; both are false, and the card's "102 days" measures from the template's arrival rather than from the value's. The error came from a pickaxe misuse —git log -S'"typescript"'counts occurrences of that string, and a value change does not change the count, so the 2026-07-14 commit was invisible to it.git log -Gshows both commits.⭐ And the move that made the third value recorded no reasoning about TypeScript at all. #2907's commit message documents the
@objectstack/*version sync and nothing else. Its whole one-file diff:The five
@objectstack/*ranges move off^6.0.0; thetypescriptline moves onto it. Read the intent as you like — what is not in dispute is that no statement of it exists anywhere. That makes this card's argument stronger, not weaker: the third value was never a decision.The control is in the same file as the defect.
SCAFFOLD_PNPM_RANGE,getCliVersion(),renderPnpmWorkspaceYaml(),sanitizeNamespace()and — since PR #15893 landed —NPM_PACKAGE_NAME_MAX_LENGTHandvalidateProjectName()are imported bycreate.tsrather than restated. That is six symbols today, not the four the card names. Each has exactly one definition point inpackages/cli, and none has drifted across any of the five emissions. Same files, same authors, same window: the restated values split into three, the imported ones did not move at all.The decision: which value survives
The card fences this and requires it argued rather than picked. Two ranges had to survive a merge; both are argued from measurement, and neither changes what a scaffolded project installs.
typescript
^5.3.0. It is already a recorded decision on two live doc pages:content/docs/getting-started/index.mdx:183— "ObjectStack works with TypeScript 5.3+, but the project itself is built and tested against TypeScript 6.x."content/docs/deployment/troubleshooting.mdx:359— "TypeScript 5.3.0 or later for full type inference support."^5.8.0matches no statement anywhere in the repo.^5.3.0was also what three of the five emissions already carried, and it is measured rather than assumed: TypeScript 5.3.3 type-checks every shape these two commands emit with results identical to 6.0.3 — driven against this repo's own@objectstack/specbuild, on theskipLibCheckconfiguration the scaffold actually emits (5.3.3 / 5.8.3 / 6.0.3, five emitted projects each: identical exit codes, identical error counts, identical error text). Choosing^5.8.0instead would have contradicted two published pages, and Zone 1 of this card forbids doc edits — so^5.3.0was the only choice that did not require moving something the card fences.vitest
^4.0.0. Neither value is a recorded decision; both were written in the same 2026-02-07 commit.^4.0.18claims a patch-level floor nothing justifies and is strictly the narrower of the two.Neither choice moves an installed byte. Read off the registry today:
^5.3.0and^5.8.0both resolve to typescript 5.9.3;^4.0.18and^4.0.0both resolve to vitest 4.1.11. What changes is the floor each project declares — a support promise — so the surviving one is the promise the docs already make.Can
create-objectstackparticipate at all? No, and here is the measurementIt cannot import from
@objectstack/cli, in either of the two ways that would matter:packages/cli/package.jsondeclares"create-objectstack": "workspace:*", andinit.tsimportscreate-objectstack/created-summary. A reverse import is a cycle.npxpackage (chalk,commander). Adding@objectstack/cliwould drag the CLI's ~50-package workspace closure intonpx create-objectstack.copyDir()intemplate-copy.ts. There is no renderer to route through a constant;scripts/sync-template-versions.mjsrewrites only@objectstack/*ranges and deliberately leaves third-party ones alone.So this is a two-of-three extraction that says why. Unifying the third would move a scaffolded project from typescript 6.0.3 to 5.9.3 — a change to what an emitted project installs, which is user-visible and a support decision rather than a refactor.
template-consistency.test.ts:677/683does not pin the blank template's^6.0.0. Those lines are asyncObjectStackDepsunit fixture whosetypescript: '^6.0.0'exists to assert the sync leaves non-@objectstack/*deps alone. Nothing in the tree pins the emitted value.What actually changed on disk, driven and hashed
All five CLI emissions were driven for real —
bin/run-dev.jsunder tsx, into throwaway directories, one process per shape — and every emitted file hashed before and after.os init -t appd11d5dcada628ecd, 7/7 files)os init -t empty824e2dfd56010376, 5/5 files)os init -t pluginpackage.jsononlyos create pluginpackage.jsononlyos create examplepackage.jsononlyThree one-line diffs, and nothing else in 29 emitted files:
Every one of the five
tsconfig.jsonfiles is byte-identical before and after — the shared renderer reproduces both inline objects exactly. (Four of the five were already byte-identical to each other onmain; the fifth,os create plugin, differed only inrootDirandinclude, which are the renderer's two parameters.)Anti-vacuity control, non-zero and re-validated in the same run: the harness's own earlier run — before
create-objectstack/distexisted — reportedexit=2, files=0, tree=e3b0c44298fc1c14(the sha256 of the empty string) for all five. Every reading above isexit=0with 5-7 files. The predicted per-file result was written down before the "after" run and matched it exactly, file for file.The pin, and the ablation that shows it can fail
packages/cli/test/scaffold-emission-policy.e2e.test.ts(6 cases). No expected value is transcribed: every expectation comes from the renderers, from the other scaffolder, or from the doc page that already states the answer.os initwrites itstsconfig.jsoninsiderun(), so that half is measured by driving the real command and reading the bytes off disk.Three mutations, each discriminating a different axis. Direction predicted in writing first, including which cases must stay GREEN and why; every mutation proved on disk by a removed-text count and an injected-marker count; baseline captured as a blob with
git hash-object -w, restored withgit cat-file blobunder anEXIT INT TERMtrap, and confirmed bygit diff HEADempty plus a blob hash equal to HEAD's after each leg.typescriptas a literal increate.ts(2 sites)init.tsrun()SCAFFOLD_TYPESCRIPT_RANGEoff the documented floorMutation 2 is also the positive proof of source-vs-
distresolution for the spawned half: it touchespackages/cli/srconly,packages/cli/distwas not rebuilt between the legs, and the spawned command's emitted bytes still changed — sotsx bin/run-dev.jsis loading source.Illegal division by zeroon a/inside the replacement text, so nothing landed. The guard caught it — injected-marker count 0, reported VOID, no reading taken — and both were re-run through a python replacement. The numbers above are from the second run; the first produced no result at all rather than a green one.The docs case has a limitation worth naming: it measures the exported constant against the pages, so mutation 1 (a literal restated in a template) leaves it GREEN. That is why case 2 of the first group exists — the two together cover both directions.
Verification
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands— exactly 77 lines on stdout — and asserted against the script's ownReconciliation — 77 famil(ies)line. The three families the first round did not owe are exactly the onesscripts/check-ci-filter-parity.mjspulled in:check-ci-filter-parity --self-test,bare-root-worklist --self-test,check:pm-dispatch-gates. Exit codes captured before any pipe.check:dts-closure,check:dual-build-cjs-loads,check:i18n,check:i18n-coverage,check:type-check-debtat exit 3 (PREREQUISITE NOT MET), andcheck:i18n-walk-parityat exit 1 whose own verdict line readsPREREQUISITE NOT MET: the workspace CLI is not built. All six are green after the workspace build.check-ci-filter-parity --self-testwent unrun while it was still in that block. Run now, 33 of 36 exit 0. The other three are NOT MEASURED, each by its own verdict line, and none is implicated by this diff:check-partof-closing-keywordexit 2NOT WIRED(noPR_BODY/PR_NUMBERoutside its workflow — driven separately below with this body's real text),check-single-claim-pathsexit 2NOT WIRED(needsPR_NUMBERand a token), and@objectstack/spec check:react-declaration-parityexit 1 sayingMANIFEST is not set … This gate did NOT run— an on-demand gate whose trigger is the.objectui-shapin moving, which this diff does not touch.check-ci-filter-parity --self-testfailed at the previous head: the pre-ci.yml's core filter misses ten more declared cross-package test inputs — content/ (7), docs/, skills/, .github/workflows/ #10015 rollback now uncovers 21 globs against a pin of=== 19. ⛔ Not bumped. The delta was measured first — the same rollback fixture run outside the self-test yields 19 atorigin/mainand 21 here, with the symmetric difference being exactlycontent/docs/deployment/troubleshooting.mdxandcontent/docs/getting-started/index.mdx, this branch's two declarations and nothing else. Both are now recorded by name, the way the pin's own comment prescribes for [finding] Two scaffolders, one of which emits output that cannot install outside this monorepo:os createvsos init#14824's three, and the count moved with them. Verdicts, exit codes captured before any pipe:node scripts/check-ci-filter-parity.mjs --self-test→ exit 0,✓ check-ci-filter-parity --self-test: 47 assertions — … the pre-#10015 rollback uncovering the ten it fixed plus #10848's one plus #10178's two plus #12201's one plus #12924's one plus #14561's one plus #14824's three plus #15818's two, and the CI wiring read out of lint.yml.node scripts/check-ci-filter-parity.mjs→ exit 0,OK: all 151 declared cross-package glob(s) (104 unique) are covered by \core` or `crosspkg`, every `crosspkg` entry still covers one, and the `test` job's `if:` still names both filters.`SELF_TEST_BATTERIESare a floor, not an equality, so the two added cases (45 → 47) need no ledger edit.check-partof-closing-keyword, driven with this body's real text —PR_BODY="$(cat …)" node scripts/check-partof-closing-keyword.mjs→ exit 0,✓ check:partof-closing-keyword: this PR carries no Part-of/closing-keyword contradiction.pnpm --filter @objectstack/cli typecheck— green, includingcheck:test-typecheck. The new test file is genuinely in that program:tsc -p tsconfig.test.json --listFilesnames it (1 hit, not 0).pnpm --filter @objectstack/cli exec vitest run --project unit— 177 files, 2380 passed, 0 failed (6 expected-fail).init,create,scaffold-workspace-consistency,scaffold-manifest-schema,create-plugin-docs-parity,create-refuses-invalid-project-name, the threeinit-scaffold*/init-template*,init-created-files-summary,generate-scaffold-validates,commands, and the new pin — 195 passed, 0 failed.exit 124) and is NOT MEASURED here — not green, not red. The 13 files above include every scaffold-touching member of that tier and were run individually. CI runs the tier in full.pnpm lint(eslint . --no-inline-config, repo-wide, the only style authority here) — exit 0, no findings. No narrowing needed.check:nul-bytesgreen, plus a directgrep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over every touched file — no hits.All measurements above were taken at
78fd6b2ade0.origin/mainhas advanced 12 commits since this branch's based4c2cb196;git merge-tree --write-treereports a clean merge with no conflicts, so nothing was merged in.Clause 2, declared per limb
package.jsonandtsconfig.jsonkey sets are unchanged, and only three values move. Nothing underpackages/spec/src/**is touched.needs:contract-reviewis on this PR and on the card.Deliberately untouched
os create pluginnames the scaffolded package@objectstack/plugin-NAME— a scope the developer it is scaffolded for cannot publish to #15530 (emitted scope) and theos create exampleretirement question are with the maintainer; neither is pre-empted here.pluginnames two different artifacts across the scaffolders —os init -t pluginemits a metadata plugin,os create pluginemits a kernel code plugin, andcli.mdxpoints readers at the wrong one #15817 (thepluginword naming two artifacts) and [finding]os createaccepts a project name npm refuses and writes it —os create pluginwithMy Appemits./plugin-My App/andname: "@objectstack/plugin-My App";os initrefuses the same input #15816 (name validation, landed as PR fix(cli): refuse a project name npm rejects inos create, before any write #15893) are independent defects from the same measurement and are not folded in.os init -t appand-t pluginemit a project that fails its ownpnpm typecheck— the generated object file annotatesData.Object, which@objectstack/spec/datadoes not export #15976 —os init'sappandplugintemplates emit a project that fails its ownpnpm typecheck, because they annotateData.Objectand@objectstack/spec/dataexports no such member. Found while measuring the TypeScript floor here (it reproduces against the published@objectstack/spec@17.3.0, at 5.3.3, 5.8.3 and 6.0.3 alike, withos create exampleclean on the same tarball as the control). Severe, separate, and deliberately not repaired in this PR.init.tsis not in@objectstack/cli'sexportsmap), no command-surface change..gitignoreis emitted only byos initandREADME.mdonly byos createis left as it stands — turning those two into recorded decisions is an emitted-scope question, which isos create pluginnames the scaffolded package@objectstack/plugin-NAME— a scope the developer it is scaffolded for cannot publish to #15530's, not this card's.