Skip to content

fix(cli): os init scaffolds its starter object as <ns>_item.object.ts, the spelling the registry declares (#11598) - #12073

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-11598-init-scaffold-object-spelling
Aug 25, 2026
Merged

fix(cli): os init scaffolds its starter object as <ns>_item.object.ts, the spelling the registry declares (#11598)#12073
os-zhuang merged 2 commits into
mainfrom
claude/issue-11598-init-scaffold-object-spelling

Conversation

@claude

@claudeclaudeBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Fixes#11598

objectstack init scaffolded its starter object as src/objects/<namespace>_item.ts
while DEFAULT_METADATA_TYPE_REGISTRY declares the object type as
**/*.object.ts / .yml / .json. Both srcFiles tables and the barrel
specifier they emit now carry the type infix, so os init writes
src/objects/my_app_item.object.ts — the same spelling os g object writes
after #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), reading
dist/objectstack.json back.

scaffoldcompile exitobjects in the artifact
A — old spelling, exactly as shipped0["my_app_item"]
B — old spelling plus an unbarrelled src/objects/probe.object.ts0["my_app_item"]
C — new spelling (this PR)0["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 init
was 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) over import * as objects from './src/objects',
so the object arrives through the barrel's module specifier; os dev /
os serve then boot from the compiled artifact, because
packages/runtime/src/standalone-stack.ts hands MetadataPlugin an
artifactSource, which routes bootstrap to _loadFromLocalFile and never to
_loadFromFileSystem (the glob pass). Its own comment says so: "Source-file
scanner 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 init then os g object customer gets my_app_item.ts
beside customer.object.ts in one directory. The registry spelling is the
authority — the direction #11071 settled — and it is already what
create-objectstack's blank starter ships (note.object.ts, same code-declared
barrel load path), what examples/app-crm/src/objects/account.object.ts uses,
and what content/docs/getting-started/examples.mdx lists as the house
convention two lines under the callout that described the old name.

Re-derived offsets — measured, not reconciled

The dispatch's :424 / :512held, at my branch base c804f0ca5 and again
at origin/main3954fb7df after it moved mid-task:

$ git show origin/main:packages/cli/src/commands/init.ts | grep -n "src/objects/__name___item.ts"
424: 'src/objects/__name___item.ts': …
512: 'src/objects/__name___item.ts': …

Measurement also found three sites the offsets did not name, all of which a
rename has to move with them:

$ git show origin/main:packages/cli/src/commands/init.ts | grep -n "from './\${namespace}_item'"
422: 'src/objects/index.ts': … from './${namespace}_item';
510: 'src/objects/index.ts': … from './${namespace}_item';
$ git show origin/main:packages/cli/src/commands/init.ts | grep -n "my_app_item.ts"
636: * contains hyphens (`my-app` → namespace `my_app` → `src/objects/my_app_item.ts`).

:422 / :510 are the barrel specifiers — moving the srcFiles key without
them produces a scaffold that does not compile.

Scope settled

Six files. packages/cli/src/commands/init.ts (:422/:424/:510/:512 plus
the writeTemplateSrcFiles doc comment at :636), the new pin, a changeset, and
three follow-on edits the rename forces:

  • packages/cli/test/init.test.ts — the round-trip pin asserts the object file
    path and the barrel specifier by name.
  • packages/cli/test/init-template-comments-self-contained.test.ts — its two
    vacuity 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 not
    the barrel) so the property it defends stops being coupled to a spelling.
  • content/docs/getting-started/examples.mdx — the callout stating what os init
    emits becomes factually wrong otherwise, and docs-drift-check flags this page
    on 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.md
also names the old spelling and was left alone (shipped history).
scaffold-next-steps-pm.test.ts (#11616's file) does not exist on main
and was not created; detectPackageManager (also #11616's) and
packages/cli/src/commands/compile.ts (#11772's) are untouched. The
TEST_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's
accept/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
matchesGlob against filePatterns read out of the registry at test time, over
projects emitted by init's own writeTemplateSrcFiles. Nothing in it compares
against a literal filename, and it never asserts that init.ts contains a
string.

Revert onlypackages/cli/src/commands/init.ts to origin/main, keep every
pin, run. Mutation confirmed on disk with anchored greps in both directions
before the run (the editing tool's exit code is not evidence):

LEG 1 — MUTATED
removed text '__name___item.object.ts' count: 0 (expect 0)
restored text '__name___item.ts' count: 2 (expect 2)
removed text "_item.object'" count: 0 (expect 0)
restored text "_item'" count: 2 (expect 2)
git diff --name-only HEAD: packages/cli/src/commands/init.ts
→ Test Files 3 failed | 2 passed (5) Tests 6 failed | 81 passed (87)
LEG 2 — RESTORED
restored text '__name___item.object.ts' count: 2 (expect 2)
removed text '__name___item.ts' count: 0 (expect 0)
restored text "_item.object'" count: 2 (expect 2)
removed text "_item'" count: 0 (expect 0)
git diff --name-only HEAD: '' (empty)
→ Test Files 1 failed | 4 passed (5) Tests 2 failed | 85 passed (87)

Red without the behaviour — 4 of the new pin's assertions plus the round-trip:

init-scaffold-file-name-registry-parity > template "app" writes every object under a name the `object` patterns match
init-scaffold-file-name-registry-parity > template "plugin" writes every object under a name the `object` patterns match
init-scaffold-file-name-registry-parity > template "app" writes the same filename `os g object` would, for the same stem
init-scaffold-file-name-registry-parity > template "plugin" writes the same filename `os g object` would, for the same stem
init.test.ts > scaffold rendering — round-trip > renders kebab project name into snake_case file paths and identifiers

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
object type…"
, "at least one built-in template emits an object source at
all"
); 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 srcFiles key and leaves the barrel behind;
init-scaffold-authoring-rules.test.ts (the scaffold still passes the
author-time rules dev runs, which is where a broken barrel would surface); and
init-template-comments-self-contained.test.ts after its selector repair.

The ablation script carries trap '<restore>' EXIT INT TERM, and the trap fired
on exit (TRAP: restored packages/cli/src/commands/init.ts) — a SIGTERM
mid-mutation must never leave the tree measuring the wrong code.

On the dist/ trap (R35). No dist/ is on this pin's measured path, and
this is why rather than an assumption: TEMPLATES / writeTemplateSrcFiles are
imported as ../src/commands/init.js, an in-package relative specifier, so
vitest 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 — same
CLI, run from src/ through tsx … so they do not depend on packages/cli/dist
having 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; the content/docs edit pulled in the whole
docs family, which the dispatch list did not name). Union run at
git rev-parse --short HEAD = 8a8aac728, the final commit.

os-verify-lock: VERDICT command-exit 0 · held the lock 82s ← cli build + typecheck + tests
os-verify-lock: VERDICT command-exit 0 · held the lock 68s ← ablation, both legs
os-verify-lock: VERDICT command-exit 0 · held the lock 294s ← i18n / type-check-debt / full lint
  • pnpm lint (full repo, eslint . --no-inline-config) — exit 0, no
    output. Run whole, not narrowed.
  • pnpm --filter @objectstack/cli typecheck (tsc --noEmit) — exit 0.
  • packages/cli tests — the whole package ran green once
    packages/cli/dist existed: 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.
  • 38 derived gates, every one exit=0, each captured by redirecting first
    and 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 five
    scripts/check-*.mjs changeset/docs gates, check-ci-filter-parity,
    check-plugin-teardown-shape, check-section-landing-index, both
    scripts/docs-audit/check-*.mjs, release-rehearsal-clone --self-test, the
    two @objectstack/lint doc gates, and all four @objectstack/spec liveness
    gates.
  • 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 unbuilt
worktree rather than this diff; the named closures were built and both re-run:

  • check:i18n-coverage — first check-i18n-coverage: COULD NOT MEASURE — 1 of 12 config(s) failed to lint, cause @objectstack/connector-mcp"is
    installed but has no build output in this worktree"
    (then
    @objectstack/connector-openapi behind it). After building the connector
    closure: check-i18n-coverage: OK (12 config(s), 657 baselined untranslated string(s), none new). — exit 0.
  • check:type-check-debt — first Error: --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 a
TEST_DEBT entry (146), and its tsconfig.json is include: ["src"] with no
exclude, so packages/cli/test/"is read by nothing — not pnpm --filter @objectstack/cli typecheck, which exits 0 on this package today, not CI, only
this 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 ceiling with the remedy pnpm check:type-check-debt --lower — deliberately not
run: 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-measure run held the shared verify
lock 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.ts
failed in both ablation legs with Error: command serve not found, and went
green (Tests 3 passed) the moment packages/cli/dist was built. An unbuilt
worktree, not a defect — not filed.


Generated by Claude Code


Generated by Claude Code

`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
@github-actions

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))
  • content/docs/getting-started/examples.mdx(via os init (command))
  • content/docs/getting-started/your-first-project.mdx(via os init (command))

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

  • content/docs/releases/v17.mdx(via os init (command))

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 — 23 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 7f6dc4ccf8b4f23eb2e0f224ddf5eff070d86a32packageMentionDocs.

Which tree this was computed on

This run read content/docs from 02e7f93d6fda53772ee778068dcc6170df896bb0 — the merge of head 8a8aac728dfbf171b8c6ebf3c3ddcda222c8bec9 into base 7f6dc4ccf8b4f23eb2e0f224ddf5eff070d86a32, 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 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

⚠️ 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 7f6dc4ccf8b4f23eb2e0f224ddf5eff070d86a32 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os init scaffolds NAME_item.ts while the registry declares *.object.ts — after #11071 the same project gets both spellings

2 participants

@os-zhuang@claude