Uh oh!
There was an error while loading. Please reload this page.
fix(cli): os init scaffolds its starter object as <ns>_item.object.ts, the spelling the registry declares (#11598) - #12073
Conversation
`os init` wrote `src/objects/<namespace>_item.ts` while the registry declares the `object` type as `**/*.object.ts` — zero glob matches, against exactly one for the type-infixed name. Both `srcFiles` tables and the barrel specifier they emit now carry the infix, converging `os init` on the same spelling `os g object` writes after #11071, `create-objectstack`'s blank starter already ships, and the examples use. Measured, not assumed: a scaffolded project loads the file through the barrel's module specifier (the config declares `objects: Object.values(objects)`) and boots from the compiled artifact, so the old name was a naming inconsistency rather than breakage — an unbarrelled `*.object.ts` does not reach the artifact either, which is what puts the registry glob off this load path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
…ng (#11598) `init-template-comments-self-contained.test.ts` picked object sources with `/src\/objects\/.*_item\.ts$/`, so the rename turned its two vacuity guards red — the guard working exactly as designed, but a re-edit the property it defends never needed. It now selects anything under `src/objects/` that is not the barrel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
📓 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 — 23 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 02e7f93d6fda53772ee778068dcc6170df896bb0 && git checkout 02e7f93d6fda53772ee778068dcc6170df896bb0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7f6dc4ccf8b4f23eb2e0f224ddf5eff070d86a32 8a8aac728dfbf171b8c6ebf3c3ddcda222c8bec9 && git checkout -B drift-repro 7f6dc4ccf8b4f23eb2e0f224ddf5eff070d86a32 && git merge --no-ff 8a8aac728dfbf171b8c6ebf3c3ddcda222c8bec9
node scripts/docs-audit/affected-docs.mjs --json 7f6dc4ccf8b4f23eb2e0f224ddf5eff070d86a32
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11598
objectstack initscaffolded its starter object assrc/objects/<namespace>_item.tswhile
DEFAULT_METADATA_TYPE_REGISTRYdeclares theobjecttype as**/*.object.ts/.yml/.json. BothsrcFilestables and the barrelspecifier they emit now carry the type infix, so
os initwritessrc/objects/my_app_item.object.ts— the same spellingos g objectwritesafter #11071.
The premise measurement — the card's own caveat was the load-bearing part
The card asked, explicitly, not to infer breakage from the registry glob.
Measured end-to-end instead: three real scaffolds, each compiled with the real
os compile(tsx packages/cli/bin/run-dev.js compile), readingdist/objectstack.jsonback.compileexit["my_app_item"]src/objects/probe.object.ts["my_app_item"]["my_app_item"]Answer: this was a naming inconsistency, not user breakage. Row A rules out
the materially worse finding — the old-spelled file was loaded, so
os initwas never generating a file the project ignores, and this is not the #10359
silent-strip shape. Row B is the discriminator: a file spelled the way the
registry declares, sitting in the same directory but not re-exported from the
barrel, does not reach the artifact — so the registry glob is not on a
scaffolded project's load path at all. Row C shows the rename preserves the
behaviour.
The mechanism the measurement confirms: the scaffolded config declares
objects: Object.values(objects)overimport * as objects from './src/objects',so the object arrives through the barrel's module specifier;
os dev/os servethen boot from the compiled artifact, becausepackages/runtime/src/standalone-stack.tshandsMetadataPluginanartifactSource, which routes bootstrap to_loadFromLocalFileand never to_loadFromFileSystem(the glob pass). Its own comment says so: "Source-filescanner OFF — declarative metadata is loaded from the compiled artifact, not
from yaml/json files on disk."
So what this fixes is one CLI teaching two spellings for one metadata type. An
author who runs
os initthenos g object customergetsmy_app_item.tsbeside
customer.object.tsin one directory. The registry spelling is theauthority — the direction #11071 settled — and it is already what
create-objectstack's blank starter ships (note.object.ts, same code-declaredbarrel load path), what
examples/app-crm/src/objects/account.object.tsuses,and what
content/docs/getting-started/examples.mdxlists as the houseconvention two lines under the callout that described the old name.
Re-derived offsets — measured, not reconciled
The dispatch's
:424/:512held, at my branch basec804f0ca5and againat
origin/main3954fb7dfafter it moved mid-task:Measurement also found three sites the offsets did not name, all of which a
rename has to move with them:
:422/:510are the barrel specifiers — moving thesrcFileskey withoutthem produces a scaffold that does not compile.
Scope settled
Six files.
packages/cli/src/commands/init.ts(:422/:424/:510/:512plusthe
writeTemplateSrcFilesdoc comment at:636), the new pin, a changeset, andthree follow-on edits the rename forces:
packages/cli/test/init.test.ts— the round-trip pin asserts the object filepath and the barrel specifier by name.
packages/cli/test/init-template-comments-self-contained.test.ts— its twovacuity guards selected object sources with
/src\/objects\/.*_item\.ts$/and went red on the rename. That is the guard working exactly as designed; the
repair makes the selector structural (anything under
src/objects/that is notthe barrel) so the property it defends stops being coupled to a spelling.
content/docs/getting-started/examples.mdx— the callout stating whatos initemits becomes factually wrong otherwise, and
docs-drift-checkflags this pageon any
packages/**change anyway.Boundary scan: nothing here reaches the registry, the loader or
packages/spec— the direction the card said to stop and report on.
packages/cli/CHANGELOG.mdalso names the old spelling and was left alone (shipped history).
scaffold-next-steps-pm.test.ts(#11616's file) does not exist onmainand was not created;
detectPackageManager(also #11616's) andpackages/cli/src/commands/compile.ts(#11772's) are untouched. TheTEST_DEBT / type-check-debt ledger (#11788's) is untouched.
Clause ② — re-affirmed against what was actually built
Still no. The shipped diff changes what
os initgenerates; no door'saccept/reject set moves, no schema, no public type. Confirmed by the
measurement above rather than assumed: rows A and C compile identically and
produce the same artifact, and row B shows the registry glob was never
consulted, so no loader behaviour changed either. Nothing in the fix needed a
registry or loader change.
Anti-vacuity — the ablation
The pin asserts the generated file is discovered by the registry, via
matchesGlobagainstfilePatternsread out of the registry at test time, overprojects emitted by
init's ownwriteTemplateSrcFiles. Nothing in it comparesagainst a literal filename, and it never asserts that
init.tscontains astring.
Revert only
packages/cli/src/commands/init.tstoorigin/main, keep everypin, run. Mutation confirmed on disk with anchored greps in both directions
before the run (the editing tool's exit code is not evidence):
Red without the behaviour — 4 of the new pin's assertions plus the round-trip:
Green in both states, named as regression guards, not counted as red-before
evidence: the pin's two vacuity guards ("the registry still declares an
objecttype…", "at least one built-in template emits an object source atall"); its barrel-resolves assertion, which is green in both legs because
both states are internally consistent — it exists to catch a partial rename
that moves a
srcFileskey and leaves the barrel behind;init-scaffold-authoring-rules.test.ts(the scaffold still passes theauthor-time rules
devruns, which is where a broken barrel would surface); andinit-template-comments-self-contained.test.tsafter its selector repair.The ablation script carries
trap '<restore>' EXIT INT TERM, and the trap firedon exit (
TRAP: restored packages/cli/src/commands/init.ts) — a SIGTERMmid-mutation must never leave the tree measuring the wrong code.
On the
dist/trap (R35). Nodist/is on this pin's measured path, andthis is why rather than an assumption:
TEMPLATES/writeTemplateSrcFilesareimported as
../src/commands/init.js, an in-package relative specifier, sovitest loads the edited source; the pin spawns no child process. The one CLI
e2e that does spawn for scaffolds,
init-created-files-summary.e2e.test.ts,drives
bin/run-dev.js, whose own header says "The SOURCE entry point — sameCLI, run from
src/through tsx … so they do not depend onpackages/cli/disthaving been built." Consistent with that, both ablation legs moved, which is
the positive control: an unrebuilt
dist/would have come back green.Checks — each gate's own verdict line
Gate family re-derived from the actual change set, not from the dispatch
list:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(5 paths vs merge base
c804f0ca5; thecontent/docsedit pulled in the wholedocs family, which the dispatch list did not name). Union run at
git rev-parse --short HEAD=8a8aac728, the final commit.pnpm lint(full repo,eslint . --no-inline-config) — exit 0, nooutput. Run whole, not narrowed.
pnpm --filter @objectstack/cli typecheck(tsc --noEmit) — exit 0.packages/clitests — the whole package ran green oncepackages/cli/distexisted:Test Files 6 passed (6) · Tests 91 passed (91)over the pin,
init.test.ts,init-scaffold-authoring-rules.test.ts,init-template-comments-self-contained.test.ts,init-created-files-summary.e2e.test.ts,scaffold-workspace-consistency.test.ts.exit=0, each captured by redirecting firstand reading
$?after (never through a pipe):check:nul-bytes,check:changeset-gate-self-tests,check:cli-test-child-env,check:cross-package-test-inputs,check:doc-anchors,check:doc-authoring,check:docs-audit-scope,check:docs-redirects,check:objectui-changeset,check:published-files,check:published-readme-links,check:react-page-adapter-contract,check:role-word,check:slot-lookup,check:test-source-alias,check:type-check-coverage,check:type-source-resolution,check:query-options-erasure,check:engine-double-contract,check:where-matcher,check:i18n, the fivescripts/check-*.mjschangeset/docs gates,check-ci-filter-parity,check-plugin-teardown-shape,check-section-landing-index, bothscripts/docs-audit/check-*.mjs,release-rehearsal-clone --self-test, thetwo
@objectstack/lintdoc gates, and all four@objectstack/speclivenessgates.
check:type-check-coverage(the structural half) prints its own verdict:check-type-check-coverage: OK — 65/78 workspace packages type-checked (plus the root), 13 in the DEBT ledger …Two gates first REFUSED to measure — both now run, both green
refuse ≠ pass, so neither was left at its refusal. Both named an unbuiltworktree rather than this diff; the named closures were built and both re-run:
check:i18n-coverage— firstcheck-i18n-coverage: COULD NOT MEASURE — 1 of 12 config(s) failed to lint, cause@objectstack/connector-mcp"isinstalled but has no build output in this worktree" (then
@objectstack/connector-openapibehind it). After building the connectorclosure:
check-i18n-coverage: OK (12 config(s), 657 baselined untranslated string(s), none new).— exit 0.check:type-check-debt— firstError: --re-measure cannot run: 1 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk -- @objectstack/service-knowledge.After building that closure:check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured in 466.3s, 1898 raw tsc error(s) total, none above its recorded number.— exit 0.The second one is the gate that actually matters for this PR, which is why it
was chased rather than declared unmeasurable.
@objectstack/cliis aTEST_DEBT entry (146), and its
tsconfig.jsonisinclude: ["src"]with noexclude, sopackages/cli/test/"is read by nothing — notpnpm --filter @objectstack/cli typecheck, which exits 0 on this package today, not CI, onlythis ledger." The new pin lands in exactly that hidden layer, so the green
typecheck above does not cover it — the ratchet is the only thing that does,
and it reports the new file adds nothing above the recorded ceiling.
⛔ The ledger was not edited in either direction. The re-measure also prints a
standing
surplus: 1 raw error(s) across 1 entr(ies) sit BELOW their recorded ceilingwith the remedypnpm check:type-check-debt --lower— deliberately notrun: that is a ledger write, #11788 owns it this round, and it is a pre-existing
repo condition rather than anything this diff moved.
Cost note for the shared container, since the wrapper asked for it to be said on
the card rather than swallowed: the
--re-measurerun held the shared verifylock for 903s (15m03s), of which 466s is the re-measure itself. That is
inherent to this gate, not a one-off — it re-runs tsc across 32 ledger entries.
Also measured while chasing this:
serve-node-env-production-default.e2e.test.tsfailed in both ablation legs with
Error: command serve not found, and wentgreen (
Tests 3 passed) the momentpackages/cli/distwas built. An unbuiltworktree, not a defect — not filed.
Generated by Claude Code
Generated by Claude Code