Commit 0bd1126
fix(cli): os init and os generate object declare the scaffolded object with ObjectSchema.create (#20195)
Fixes #19722
Clause-②: no
Ruling `5644350230` (director seat, decision batch #122 item 1,
maintainer 「同意」 2026-09-12), item 1:
「`packages/cli/src/commands/init.ts` `TEMPLATES` emit the factory shape;
`content/docs/deployment/cli.mdx:1323` describes it.」 Item 3: 「the
changeset states how a user converts theirs (one mechanical rewrite:
wrap the literal).」 This is the `domain:cli` half; the spec/scripts half
landed as PR #19720 (`42339e2f`). #17418 remains open (it carries
`Blocked-by` on this card and is the spec lane's to move). #19098
remains open (the other `generate.ts` card, serial behind this one).
## What changed
Both doors that write a `*.object.ts` now emit the one authorised shape,
`ObjectSchema.create({ … })`:
| door | before | after |
|:--|:--|:--|
| `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({
… });` |
- The emitted shape is exactly what the changeset's user rewrite
produces from the old one (wrap the literal, drop the annotation, import
the factory), so a scaffold and a converted file look the same.
- `ObjectSchema` is a **value** import: `import type` is erased at
compile time and the module would throw on first evaluation.
- The binding stays the file's **default export**. Both barrels (`os
init`'s `src/objects/index.ts` and the line `os generate` appends for
all seven generators) re-export `default`, so no barrel spelling moves
and no user barrel needs touching.
- The authored OWD comment block is unchanged byte for byte in all three
emitters (only the closing `};` became `});`);
`init-template-comments-self-contained.test.ts` is green.
- `generate.ts`'s docblock states that the init/generate parity now
covers the declaration shape as well as the `sharingModel` value, and
names the pin that holds it.
## Premise check (on `origin/main`, sites located by symbol)
- `TEMPLATES` (both object-bearing entries) and
`GENERATORS.object.generate` emitted the annotated literal: confirmed.
- `create-objectstack`'s bundled `blank/src/objects/note.object.ts` is
already `export const Note = ObjectSchema.create({ … })`: confirmed.
- `scripts/sync-scaffold-emission-policy.mjs` syncs the pnpm/TypeScript
ranges only and reads no declaration shape; `pnpm
check:scaffold-emission-policy` was run (read-only `--check`) and is
green.
- The ruling's `cli.mdx:1323` anchor has drifted with later edits. The
page's only description of the scaffolded object shape was the `os
generate` "What it does" line (it named `Data.ServiceObject`); that line
now describes the factory (and names `defineSkill({ … })` for `skill`,
the one non-object type that is not a typed literal), and the `os init`
section 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/cli` at the base and at this branch, ran `os init
my-app -t app` and `os init my-plugin -t plugin` (`--no-install`, under
`packages/cli/node_modules` so `@objectstack/spec` is found by the
upward walk), then `os g object my_app_order_line` in each, then the
project's own gates. The repo gate was driven through its exported
`sweep()` over a tree holding the four scaffolded object files (plus the
driver file it reads its text family from).
| reading | before (base `3bd28e2b`) | after (this branch) |
|:--|:--|:--|
| `os validate` / `os compile` / `tsc --noEmit`, init only | exit 0 / 0
/ 0 (both templates) | exit 0 / 0 / 0 |
| same, after `os g object` | exit 0 / 0 / 0 | exit 0 / 0 / 0 |
| `check-keyed-text-bounds` `sweep()` over the 4 scaffolded files | 0
objects parsed, **4 shape violations** (`… is declared as a plain object
literal — use ObjectSchema.create`) | **4 objects parsed, 0 shape
violations**, 0 refusals |
| compiled `dist/objectstack.json` | sha256 `3981f1ab…` (app),
`e6d2c61d…` (plugin) | **byte-identical** (`cmp` equal) |
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/`,
and `os validate` / `os compile` never judged the shape. After it, the
gate parses all four. The compiled artifact is byte-identical, which is
the measured basis for `Clause-②: no` (no published payload changes).
## Pins
- **New**: `packages/cli/test/scaffold-object-declaration-shape.test.ts`
reads every emitter's bytes with the TypeScript parser (roster derived
from `TEMPLATES` and `GENERATOR_SCAFFOLD_TARGETS`) and asserts: value
import of `ObjectSchema` from `@objectstack/spec/data`; exactly one
top-level declaration, initialised by `ObjectSchema.create({…})`, with
no annotation; the default export is that binding; and **one signature
across `os init` and `os 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).
- **Repointed** (they asserted the refused spelling, per the
`domain:services` pointer `5788276757`):
`generate-emission-parses.test.ts` (`:148` and the `class`
discriminator, which asserted `const class:`),
`generate-refuses-unparseable-name.test.ts:255`, and the worked examples
in `emitted-source-parses.ts`, `generate-emission-parses.test.ts` and
the `generate.ts` refusal comment. Docblock-only:
`scaffold-emission-typechecks.test.ts` (why the pin still stands after
the annotation is gone) and
`generate-refuses-name-outside-charset.test.ts` (`const class:` → `const
class =`).
- Unchanged and still covering it:
`scaffold-emission-typechecks.test.ts` (tsc over every emitted
scaffold), `generate-scaffold-validates.test.ts` and
`init-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.mjs` swapped the
`os generate object` emitter's `import { ObjectSchema }` for `import
type { ObjectSchema }` and ran the pin: **2 failed / 5 passed**. The
failures were `'os generate object order_line'` (`is not value-imported
… (type-only)`) and `one signature across every door` (the generate
door's signature diverged). Restore proven by the tool: blob
`03b8006959dc` == HEAD and `git diff HEAD` empty. The direction observed
was red, as expected.
## Verification (head `3082b024`, after merging `origin/main`
`836aad2a`; round 1 at `468000c4` below)
`main` moved under this branch with PR #20164 (same package), so the
suite was re-run after the merge:
- `@objectstack/cli` unit tier, `vitest run --project unit
--maxWorkers=2 --shard=N/4` × 4: **226 files / 3199 tests passed** (949
+ 780 + 723 + 747).
- `@objectstack/cli` integration 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 --listFilesOnly` counts it).
- `pnpm lint` (full, `eslint . --no-inline-config`): exit 0.
- Gates derived by `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` (94): all 94 exit 0; `--ran`
verdict: `94 derived famil(ies) accounted for — 94 run, 0 NOT-MEASURED`.
- Before the merge (head `20526f3d`): unit tier 225 files / 3163 tests
passed, the same two integration files 25/25, typecheck exit 0.
- Round 1 (head `468000c4`: `origin/main` `d7c02413` merged as
`e5499d52`, then the one-sentence `cli.mdx` correction naming
`defineSkill` for `skill`): the 41 docs-scoped gates (`dispatch-gates
--commands content/docs/deployment/cli.mdx`) all exit 0, `--ran` 41 of
41 accounted for, 0 NOT-MEASURED; `pnpm lint` exit 0; `node
scripts/check-issue-citations.mjs` answered `no issue citations added
against d7c0241 (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 「the `os
init` shape imports only `* as Data`」. After this change that describes
the shape older `os init` releases 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 #17418 is unblocked). Noted, not filed.
- Reported to the seat, not addressed here: in an `os init` project, `os
g object order_line` writes `name: 'order_line'`, and the project's own
`os validate` then refuses it (`Object 'order_line' is missing the
package namespace prefix`). Measured at the base; this PR does not
change it.
- Local tooling observation: `pnpm check:type-check-debt`
(`--re-measure`) runs a whole-workspace `turbo run build` before tsc. A
local timeout that kills it mid-build leaves some packages' `dist/`
without declarations, and `check:dual-build-cjs-loads` then flags them.
Rebuilding the two packages cleared it; CI builds fresh.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 172b4cf commit 0bd1126
10 files changed
Lines changed: 278 additions & 20 deletions
File tree
- .changeset
- content/docs/deployment
- packages/cli
- src
- commands
- utils
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
141 | 141 | | |
142 | 142 | | |
143 | 143 | | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
144 | 153 | | |
145 | 154 | | |
146 | 155 | | |
| |||
1438 | 1447 | | |
1439 | 1448 | | |
1440 | 1449 | | |
1441 | | - | |
| 1450 | + | |
1442 | 1451 | | |
1443 | 1452 | | |
1444 | 1453 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
93 | 105 | | |
94 | | - | |
| 106 | + | |
95 | 107 | | |
96 | 108 | | |
97 | 109 | | |
98 | 110 | | |
99 | | - | |
| 111 | + | |
100 | 112 | | |
101 | 113 | | |
102 | 114 | | |
| |||
119 | 131 | | |
120 | 132 | | |
121 | 133 | | |
122 | | - | |
| 134 | + | |
123 | 135 | | |
124 | 136 | | |
125 | 137 | | |
| |||
961 | 973 | | |
962 | 974 | | |
963 | 975 | | |
964 | | - | |
965 | | - | |
966 | | - | |
| 976 | + | |
| 977 | + | |
| 978 | + | |
967 | 979 | | |
968 | 980 | | |
969 | 981 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
646 | 646 | | |
647 | 647 | | |
648 | 648 | | |
649 | | - | |
| 649 | + | |
650 | 650 | | |
651 | | - | |
| 651 | + | |
652 | 652 | | |
653 | 653 | | |
654 | 654 | | |
| |||
679 | 679 | | |
680 | 680 | | |
681 | 681 | | |
682 | | - | |
| 682 | + | |
683 | 683 | | |
684 | 684 | | |
685 | 685 | | |
| |||
741 | 741 | | |
742 | 742 | | |
743 | 743 | | |
744 | | - | |
| 744 | + | |
745 | 745 | | |
746 | | - | |
| 746 | + | |
747 | 747 | | |
748 | 748 | | |
749 | 749 | | |
| |||
760 | 760 | | |
761 | 761 | | |
762 | 762 | | |
763 | | - | |
| 763 | + | |
764 | 764 | | |
765 | 765 | | |
766 | 766 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | | - | |
| 14 | + | |
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | | - | |
| 14 | + | |
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| |||
145 | 145 | | |
146 | 146 | | |
147 | 147 | | |
148 | | - | |
| 148 | + | |
149 | 149 | | |
150 | 150 | | |
151 | 151 | | |
| |||
164 | 164 | | |
165 | 165 | | |
166 | 166 | | |
167 | | - | |
| 167 | + | |
168 | 168 | | |
169 | 169 | | |
170 | 170 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
40 | | - | |
| 40 | + | |
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
| |||
Lines changed: 2 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | | - | |
| 45 | + | |
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| |||
252 | 252 | | |
253 | 253 | | |
254 | 254 | | |
255 | | - | |
| 255 | + | |
256 | 256 | | |
257 | 257 | | |
258 | 258 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
31 | 39 | | |
32 | 40 | | |
33 | 41 | | |
| |||
0 commit comments