fix(cli): os init and os generate object declare the scaffolded object with ObjectSchema.create - #20195
Conversation
…t with ObjectSchema.create Both doors that write a *.object.ts now emit the one authorised shape (ruling 5644350230, decision batch #122 item 1) instead of a Data.ServiceObject-annotated literal: a value import of ObjectSchema from @objectstack/spec/data, const X = ObjectSchema.create({ ... }), and the unchanged default export the barrels re-export. Repoints the pins that asserted the refused spelling, adds a parity pin over both emitters, updates the cli docs page, and carries the @objectstack/cli changeset with the one mechanical user rewrite. Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP Co-authored-by: Claude <noreply@anthropic.com>
…actory Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 25 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 5575e5a3039849e34164ca61d00cb483ca9d6e63 && git checkout 5575e5a3039849e34164ca61d00cb483ca9d6e63
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d7c024133e77f69aa0f26af359391c6a4b0142e4 468000c4d33c114821ce729f204f2286965f4673 && git checkout -B drift-repro d7c024133e77f69aa0f26af359391c6a4b0142e4 && git merge --no-ff 468000c4d33c114821ce729f204f2286965f4673
node scripts/docs-audit/affected-docs.mjs --json d7c024133e77f69aa0f26af359391c6a4b0142e4
|
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: Independence: INDEPENDENT AGENT (fed the card, the ruling and the PR only; not the dispatch order or the seat's conclusions) VERDICT: FAIL
Generated by Claude Code |
…actory Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP Co-authored-by: Claude <noreply@anthropic.com>
… type
The item-1 sentence claimed every non-object type is written as a typed
literal; `os g skill` writes `defineSkill({ ... })`. The clause now names
the skill exception and lists the five typed-literal types exactly.
Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP
Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Delta of: ① Derived judgments
② Semver levelUnchanged by the delta: ③ Boundary flags
Implemented-by: Independence: INDEPENDENT AGENT (fed the card, the prior review, and the PR only; not the dispatch order or the seat's conclusions) VERDICT: PASS |
…the ObjectSchema.create shape (#20270) `generate-agent-retired.e2e.test.ts` asserted the pre-#20195 object template line `import * as Data from '@objectstack/spec/data'`, which the template stopped emitting in 0bd1126, so the nightly e2e tier was red on main. The control now asserts the template's current shape: `ObjectSchema` among the named imports from `@objectstack/spec/data`, and the `ObjectSchema.create({` call, not one exact import line. The case is neither skipped nor quarantined. Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19722
Clause-②: no
Ruling
5644350230(director seat, decision batch #122 item 1, maintainer 「同意」 2026-09-12), item 1: 「packages/cli/src/commands/init.tsTEMPLATESemit the factory shape;content/docs/deployment/cli.mdx:1323describes it.」 Item 3: 「the changeset states how a user converts theirs (one mechanical rewrite: wrap the literal).」 This is thedomain:clihalf; the spec/scripts half landed as PR #19720 (42339e2f). #17418 remains open (it carriesBlocked-byon this card and is the spec lane's to move). #19098 remains open (the othergenerate.tscard, serial behind this one).What changed
Both doors that write a
*.object.tsnow emit the one authorised shape,ObjectSchema.create({ … }):os init -t app/-t plugin(TEMPLATES[…].srcFiles)import * as Data …+const myAppItem: Data.ServiceObject = { … };import { ObjectSchema } …+const myAppItem = ObjectSchema.create({ … });os generate object(GENERATORS.object)const orderLine: Data.ServiceObject = { … };const orderLine = ObjectSchema.create({ … });ObjectSchemais a value import:import typeis erased at compile time and the module would throw on first evaluation.os init'ssrc/objects/index.tsand the lineos generateappends for all seven generators) re-exportdefault, so no barrel spelling moves and no user barrel needs touching.};became}););init-template-comments-self-contained.test.tsis green.generate.ts's docblock states that the init/generate parity now covers the declaration shape as well as thesharingModelvalue, and names the pin that holds it.Premise check (on
origin/main, sites located by symbol)TEMPLATES(both object-bearing entries) andGENERATORS.object.generateemitted the annotated literal: confirmed.create-objectstack's bundledblank/src/objects/note.object.tsis alreadyexport const Note = ObjectSchema.create({ … }): confirmed.scripts/sync-scaffold-emission-policy.mjssyncs the pnpm/TypeScript ranges only and reads no declaration shape;pnpm check:scaffold-emission-policywas run (read-only--check) and is green.cli.mdx:1323anchor has drifted with later edits. The page's only description of the scaffolded object shape was theos generate"What it does" line (it namedData.ServiceObject); that line now describes the factory (and namesdefineSkill({ … })forskill, the one non-object type that is not a typed literal), and theos initsection gains a short paragraph naming the shape and the one mechanical rewrite for older projects.Measured: does the #19720 gate reach a scaffold? Before and after
Built
@objectstack/cliat the base and at this branch, ranos init my-app -t appandos init my-plugin -t plugin(--no-install, underpackages/cli/node_modulesso@objectstack/specis found by the upward walk), thenos g object my_app_order_linein each, then the project's own gates. The repo gate was driven through its exportedsweep()over a tree holding the four scaffolded object files (plus the driver file it reads its text family from).3bd28e2b)os validate/os compile/tsc --noEmit, init onlyos g objectcheck-keyed-text-boundssweep()over the 4 scaffolded files… is declared as a plain object literal — use ObjectSchema.create)dist/objectstack.json3981f1ab…(app),e6d2c61d…(plugin)cmpequal)So the platform's own shape gate refused every scaffold before this change, but only as a repo script: a user project carries no
scripts/, andos validate/os compilenever judged the shape. After it, the gate parses all four. The compiled artifact is byte-identical, which is the measured basis forClause-②: no(no published payload changes).Pins
packages/cli/test/scaffold-object-declaration-shape.test.tsreads every emitter's bytes with the TypeScript parser (roster derived fromTEMPLATESandGENERATOR_SCAFFOLD_TARGETS) and asserts: value import ofObjectSchemafrom@objectstack/spec/data; exactly one top-level declaration, initialised byObjectSchema.create({…}), with no annotation; the default export is that binding; and one signature acrossos initandos generate object, which is the parity the docblock claims. Two controls prove the reader can refuse each half (the pre-ruling annotated literal; a type-only factory import).domain:servicespointer5788276757):generate-emission-parses.test.ts(:148and theclassdiscriminator, which assertedconst class:),generate-refuses-unparseable-name.test.ts:255, and the worked examples inemitted-source-parses.ts,generate-emission-parses.test.tsand thegenerate.tsrefusal comment. Docblock-only:scaffold-emission-typechecks.test.ts(why the pin still stands after the annotation is gone) andgenerate-refuses-name-outside-charset.test.ts(const class:→const class =).scaffold-emission-typechecks.test.ts(tsc over every emitted scaffold),generate-scaffold-validates.test.tsandinit-scaffold-authoring-rules.test.ts(runtime loads, which now execute the factory),init.test.ts(its assertions are name and barrel, not shape).Ablation (the new pin can fail)
Committed first, then
node scripts/ablation-replace.mjsswapped theos generate objectemitter'simport { ObjectSchema }forimport type { ObjectSchema }and ran the pin: 2 failed / 5 passed. The failures were'os generate object order_line'(is not value-imported … (type-only)) andone signature across every door(the generate door's signature diverged). Restore proven by the tool: blob03b8006959dc== HEAD andgit diff HEADempty. The direction observed was red, as expected.Verification (head
3082b024, after mergingorigin/main836aad2a; round 1 at468000c4below)mainmoved under this branch with PR #20164 (same package), so the suite was re-run after the merge:@objectstack/cliunit tier,vitest run --project unit --maxWorkers=2 --shard=N/4× 4: 226 files / 3199 tests passed (949 + 780 + 723 + 747).@objectstack/cliintegration tier, run locally because the diff touches two integration-tier files:generate-refuses-unparseable-name+generate-refuses-name-outside-charset, 2 files / 25 tests passed. The rest of the integration tier is declared to CI.pnpm --filter @objectstack/cli typecheck(tsc +check:test-typecheck): exit 0; the new test file is in the test program (tsc -p tsconfig.test.json --listFilesOnlycounts it).pnpm lint(full,eslint . --no-inline-config): exit 0.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(94): all 94 exit 0;--ranverdict:94 derived famil(ies) accounted for — 94 run, 0 NOT-MEASURED.20526f3d): unit tier 225 files / 3163 tests passed, the same two integration files 25/25, typecheck exit 0.468000c4:origin/maind7c02413merged ase5499d52, then the one-sentencecli.mdxcorrection namingdefineSkillforskill): the 41 docs-scoped gates (dispatch-gates --commands content/docs/deployment/cli.mdx) all exit 0,--ran41 of 41 accounted for, 0 NOT-MEASURED;pnpm lintexit 0;node scripts/check-issue-citations.mjsansweredno issue citations added against d7c024133 (3 file(s) read). The cli test tiers were not re-run locally on this head; CI runs them.Acceptance notes
scripts/check-keyed-text-bounds.mjs's refusal text says 「theos initshape imports only* as Data」. After this change that describes the shape olderos initreleases emitted, not the current one; it is still the right advice for a converted file.scripts/**is read-only for this lane. Carrier: the spec lane when it next touches that gate (for example when Two official scaffolders and two published docs disagree on how a .object.ts may be written — ObjectSchema.create() factory vs plain annotated literal #17418 is unblocked). Noted, not filed.os initproject,os g object order_linewritesname: 'order_line', and the project's ownos validatethen refuses it (Object 'order_line' is missing the package namespace prefix). Measured at the base; this PR does not change it.pnpm check:type-check-debt(--re-measure) runs a whole-workspaceturbo run buildbefore tsc. A local timeout that kills it mid-build leaves some packages'dist/without declarations, andcheck:dual-build-cjs-loadsthen flags them. Rebuilding the two packages cleared it; CI builds fresh.Generated by Claude Code