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>
…bjectstack-ai#20253) Fixes objectstack-ai#20216 Clause-②: no (narrowing) **Landing order: PR objectstack-ai#20228 → PR objectstack-ai#20229 → this PR.** objectstack-ai#20228 has merged. objectstack-ai#20229 (the `packages[]` read in `artifactProvidedObjectNames`, still in a rework round) edits the same file. This PR does not touch that hunk, its import line, or `packages/lint/src/object-graph.ts`, and a local `git merge-tree` of this branch against objectstack-ai#20229's head `a4b05d6` writes a clean tree. The seat enqueues this PR after objectstack-ai#20229 merges, and this branch merges `origin/main` then. ## CI fix round: `Test Core (6/6)`, and a file-surface amendment - **What went red:** `packages/cli/test/generate-scaffold-validates.test.ts`, "`os g 'view'` writes a stack `os validate` accepts", got `object-reference-unknown at views[0].object: view container object "probe_thing" …`. - **Why:** the harness judged each scaffold in a host stack holding only the collection under test. The view scaffold binds `probe_thing`, the object `os g object probe_thing` writes, but no object was in the stack. The new leg refused it correctly: the omission was the harness's, not the template's. Reproduced red locally before the fix (1 failed, 17 passed). - **Fix (test fixture only):** every `namesObject` generator other than `object` itself (view, action, flow, app) is now judged beside the object scaffold for the same name, materialized through the same `bundle-require` loader. A new pin asserts the view's binding and the seeded object's `name` are the same spelling, and that a non-binding generator (`dashboard`) and `object` itself carry no seeded object. ⛔ The rule is not weakened, `probe_thing` is not special-cased, and no test is skipped. - **File-surface amendment:** `packages/cli/test/generate-scaffold-validates.test.ts`, test fixture only. No other CLI fixture needed a change (see the runs below). - **Other generators:** action, flow and app also bind `probe_thing`. They passed before the seeding and still pass with it. ## What changed A view container's own `object` (`ViewSchema.object`) is the key the runtime indexes views by (`getViewsByObject()` / `GET /meta/view?object=`). Nothing resolved it at authoring time: `defineStack`'s `validateCrossReferences` reads a container's `list.data` / `form.data` bindings, never the container's own key. - `packages/lint/src/validate-object-references.ts` gains a view-container leg beside the relationship-target leg. It uses the same `check` ladder and the same `resolvable` set: the stack's objects plus what its `packages[]` provide. - An unresolved unprefixed name is an **error**, `object-reference-unknown` at `views[N].object`. - A known platform object passes. - A platform-shaped name that nothing registers gets the existing `object-reference-unregistered-platform` advisory. - The refusal is located and carries a prescription: - it lists the objects the stack declares (`Defined objects: ...`); - when the bound name is exactly a declared object minus the stack's `manifest.namespace` prefix, the hint names that object: `write "my_app_order_line", not "order_line"`. - It gates like its sibling. It is the same member of the same reference-integrity suite entry, so `os validate`, `os build` and `os lint` all run it at the same tier. - Not judged, on purpose: - a container with no `object` (its binding falls back to `list.data.object` / `form.data.object` / `name`, which is a different reference); - a runtime-authored container (objectstack-ai#13407 is out of scope). This member does not run on a `view` write at the runtime publish gate, and the runtime is untouched. - ⛔ No second copy in `packages/spec/src/stack.zod.ts`. ## Measured at the public door (CLI built at this branch) The scratch project has `manifest.namespace: 'my_app'`, an object `my_app_order_line`, and a view container with `object: 'order_line'`. | run | `os validate` | `os build` | |:--|:--|:--| | leg disabled (ablation, lint rebuilt, marker proven in `dist/`) | exit 0, "Validation passed", nothing about the view | not run | | this branch, `object: 'order_line'` | exit 1, `object-reference-unknown at views[0].object`, hint names `my_app_order_line` | exit 1, same rule and path | | this branch, `object: 'my_app_order_line'` (control) | exit 0 | exit 0 | `os generate view order_line` in the same namespaced project (after PR objectstack-ai#20214) writes `object: 'my_app_order_line'`, which passes. ## Census of producers (H2) The instrument is one TypeScript-AST scan at `0d60f88760`. It reads `examples/**`, `packages/**` (tests and fixtures included), `skills/**`, and the ts/js code fences in `content/docs/**` md/mdx (generated `references/` excluded). It covers 7242 files and counts object literals that carry a view-container slot (`list`/`form`/`listViews`/`formViews`). A name resolves when it is declared by an object literal (`name` + `fields`) anywhere in the corpus, or when it is a platform-provided object. | tree | containers | carrying `object` | unresolved | |:--|--:|--:|--:| | examples | 10 | 0 | 0 | | packages | 367 | 105 | 11 | | skills | 3 | 0 | 0 | | content/docs | 21 | 0 | 0 | - **Control from the same instrument:** 94 of the 105 containers carrying `object` resolve. - **No example or platform package ships a dangling container.** No example container carries `object` at all; they bind through `list.data.object`. - **The 11 unresolved:** - 10 are non-literal `object` expressions in code, not stored views: schema/`strictObject` definitions in `view.zod.ts`, walkers in `validate-translation-references.ts` / `validate-translatable-sections.ts` / `protocol.ts`, a helper in a rest measurement test, and the parameterised helper in this PR's own test. - 1 literal: `packages/cli/test/format-zod-union.test.ts` (`union_probe_obj`). That specimen fails schema parse first, which that file asserts as exactly one `invalid_union` issue. `os validate` exits at the schema step, so author-time rules never run on it. No pin flips. - **Pin sweep ①:** grepping `object-reference-unknown` and the rule's message across the repo found no pin on a view container. The pins in `packages/cli` (`artifact-packages.test.ts`, `build-multi-package-artifact.e2e.test.ts`, `union-fold-command-parity.test.ts`) have fixtures with no container `object`, so none flips. **②:** nothing flipped, so no load-bearing re-pin was owed. ## Tests (at `60808317dc` unless marked) - `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2`: **109 files / 4242 tests passed**. - `pnpm --filter @objectstack/lint typecheck`: exit 0 (`tsc --noEmit` plus `check:test-typecheck: OK`). - `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2`: **227 files / 3219 tests passed**. This includes the fixed `generate-scaffold-validates.test.ts` (19 of 19). - CLI integration tier, the 21 files that reference views or scaffolds (`--project integration`): **21 files / 196 tests passed**. - `pnpm --filter @objectstack/cli typecheck`: exit 0 (`check:test-typecheck: OK`). - Nightly tier (`OS_TEST_TIERS=nightly`), 7 e2e files on views or scaffolds: 6 files passed. One test failed in `generate-agent-retired.e2e.test.ts` ("`os g object … --dry-run` still previews a typed object file"). It expects `import * as Data from '@objectstack/spec/data'`, but the object template writes `import { ObjectSchema }` since objectstack-ai#20195. That failure is independent of this PR: neither file differs from the merge base (0 diff lines). - New pins in `validate-object-references.test.ts` (two new `describe` blocks, appended so they stay clear of objectstack-ai#20229's hunk): - `object: 'order_line'` in a `my_app` stack is refused, naming `my_app_order_line`; the prefixed container is the control; - the map form of `views` is read; - a non-prefix miss is refused without the namespace prescription; - a container over a `packages[]` sibling's object passes, and the same package alone is refused (control); - `sys_user` passes and `sys_approval_process` advises; - no views, `views: []`, and a container with no `object` stay silent; - the finding reaches the gating tier of `runAuthoringRules` for `validate`, `build` and `lint`. - **Unit ablation** (`scripts/ablation-replace.mjs`, anchor `const bound = strName(view.object);`, anchor count 1 to 0, blob `27699c9` to `03f6f47`): **8 refusal pins red; 48 green, including both controls (prefixed container, silence).** Restored with blob equal to HEAD and `git diff HEAD` empty. Taken at `9fa1ff4c61`. Since then only comment lines changed in `src`. - **Door ablation:** marker planted, `@objectstack/lint` rebuilt, `ablation-dist-preflight` found the marker in 4 built files, and `os validate` exited 0. Then the restore leg: blob equal to HEAD, lint rebuilt, preflight `--absent` confirmed the marker gone from all 14 built files with a clean tree, and `os validate` exited 1 again. ## Gates (derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at `60808317dc`) - 60 families derived (one new: `check:cli-test-child-env`, green) and all run at `60808317dc`. `--ran` reconciliation with exit codes: 59 run, 1 NOT MEASURED, 0 unrun. - The ones this diff actually moves are green: `check-adr-0087-registration` (disposition `not-required (no-migration-prescription)` accepted), `check-changeset-no-major`, `check-empty-changeset`, `check:doc-authoring`, `check:nul-bytes`, `check-closing-keyword-parity`, `check:published-files`, `check:type-check-coverage`, `check:test-source-alias`. - `check:type-check-debt` now measures green (`none above its recorded number`). - NOT MEASURED: `check:dual-build-cjs-loads` (exit 3, PREREQUISITE NOT MET: it needs every package's `dist/`, and this box built the lint and CLI closures only). Declared to CI. - Correction to the first round: I declared the `packages/cli` suites to CI without running them, and the census missed `os g view` because its container sits inside a template string the AST scan cannot see. That is the red this round fixes. The CLI unit project now runs in full here. ## Changeset `.changeset/20216-view-container-object-refused.md`: - `@objectstack/lint: minor`, carrying a **BREAKING** banner and `Clause-②: no (narrowing)`; - a before/after accept-set table; - ADR-0087 `not-required (no-migration-prescription)`: nothing authorable moves in spec, and which object an author meant is a fact about their stack, not a mechanical conversion. The table is a behaviour table (door: FROM exit 0, TO exit 1). My first draft headed it "FROM → TO", and the ADR-0087 gate read that heading as a rewrite prescription and refused the exemption. The heading now says "before and after", and the table carries no rewrite row. ## Acceptance notes (not filed) - The namespace prescription is on this leg only. The other sites on the same rule (a field `reference`, action params, dataset base object) could give the same hint for the same missing-prefix miss. I noted it and did not widen it here. Carrier: none. - A container whose `object` and `list.data.object` name different objects is not judged by any rule. It is out of this card's scope, and I found no instance in the census. --- _Generated by [Claude Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…s validate` and the published per-type schemas (objectstack-ai#19098) (objectstack-ai#20266) Fixes objectstack-ai#19098 Fixes objectstack-ai#20270 Clause-②: no (narrowing) Executes maintainer ruling `5856790152` on objectstack-ai#19098 (batch objectstack-ai#227 item 5, **letter C**, maintainer 「同意」, confirmed `5856865990`): `os generate schema` is retired, not repaired. No file is generated in its place, and nothing in `packages/spec` moves (ruling item 4). ## What changed **`packages/cli/src/commands/generate.ts`** - `RETIRED_GENERATORS` gains `schema` in the `os g agent` shape. The refusal reads "`os g schema` was retired" and cites the maintainer ruling. It names `os validate` (the real parse: `ObjectStackDefinitionSchema.safeParse`), and names the per-type schemas `@objectstack/spec` publishes at `node_modules/@objectstack/spec/json-schema/CATEGORY/TYPE.json`, which carry the published projection plus `x-dropped-refinements`. It exits 1. - The ruling's id lives in the ledger's code comment, not in the refusal. Text an author is shown carries no tracker number (AGENTS.md; `check:doc-authoring`'s cross-package prose-id leg ratchets `#NNN` tokens in `packages/**` strings, and `generate.ts` has no ledger row). - **Deleted, not left dead:** `runSchemaGeneration`, with its three `z.toJSONSchema(ObjectStackDefinitionSchema…)` rungs and both ladder notices; `KNOWN_UNSUPPORTED_JSON_SCHEMA_PATTERNS`; `isKnownUnsupportedJsonSchema`; and the `case 'schema'` route. - Nothing else was schema-only. `-o`, `--dry-run` and `--format` serve `types`, `client` and `migration`. `z` and `ObjectStackDefinitionSchema` were dynamic imports inside the deleted function. `zod` stays a dependency, used by `compile`, `validate` and `format`. The `type` argument's help text derives from `GENERATORS` and never listed `schema`. - **The ledger door moved (PM mechanism assumption 2, measured and falsified).** The lookup sat inside `runMetadataGeneration`, which runs only after two earlier steps: the sub-command `switch` (where `schema` returned first) and the NAME requirement (where a nameless call stopped). So a `schema` entry alone could never be reached from `os generate schema`. - BEFORE, at BASE `0d3ec4713` on the dev runner, `os g agent` with no name printed `Missing required argument` and exited 1. - The lookup now runs first in `Generate.run`, through `refuseRetiredGenerator`, as an own-key read (the package's `Object.prototype.hasOwnProperty.call` idiom). - Consequences, both pinned. `os g agent` with no name now prints the agent retirement. `os g constructor NAME` used to print "`os g constructor` was retired — undefined" and crash with `TypeError: retired.detail is not iterable`. It now falls through to the ordinary checks: exit 1, "No file naming convention is declared for type: constructor". **Tests** - `packages/cli/test/generate-schema-retired.e2e.test.ts` (new) is the retirement pin, in the `agent` precedent's shape: a real child process through `bin/run-dev.js` plus tsx, with assertions on stdout content and the exit status. It covers: - `os generate schema` (no name) and `os g schema -o custom.schema.json` both exit 1 and write nothing (directory listing `[]`); - "was retired", with neither "Unknown type:" nor "Missing required argument"; - "maintainer ruling", `os validate`, and `@objectstack/spec/json-schema/`; - the alias output is byte-equal to the documented spelling's; - `os g agent` (no name) answers the agent retirement; - `os g constructor thing` is not taken for a retired type; - control: `os g object customer --dry-run` still previews. - `packages/cli/test/generate-schema-writes-json-schema.e2e.test.ts` (the objectstack-ai#17873 pin, groups (a) to (e)) is **deleted**. Every assertion in it was about the document the ruling withdrew: the file exists, parses, declares its draft, the four members' fragments, and the input direction. Nothing in it survives the retirement. - Other tests that exercised the command: none. `git grep` for `generate schema`, `runSchemaGeneration`, `objectstack.schema.json` and `isKnownUnsupportedJsonSchema` over `packages/cli/src` and `packages/cli/test` finds only the deleted file. The ladder PR (objectstack-ai#17903) added no pin of its own beyond that file. - `packages/cli/test/generate-refuses-retired-generator.test.ts` (new, patch round 1) is the per-PR guard for the door. It spawns the CLI and is not named `.e2e`, so it runs in the queue's `integration` project. It pins: - `os generate schema` and `os g schema -o custom.schema.json`: exit 1, nothing written, the retirement answered (neither "Missing required argument" nor "Unknown type:"), naming the ruling, `os validate` and `@objectstack/spec/json-schema/`; - `os g constructor thing`: an own-key miss (exit 1, not "was retired", no `TypeError`, nothing written); - control: `os g object customer --dry-run` exits 0 and previews exactly what the exported object template emits. - `packages/cli/test/generate-agent-retired.e2e.test.ts` (objectstack-ai#20270, its own commit `a0cec66e9`): the live-generator control asserted `import * as Data from '@objectstack/spec/data'`, which the object template stopped emitting in `0bd11261e`, so this nightly case was red on main. It now asserts the template's current shape, `ObjectSchema` among the named imports from `@objectstack/spec/data` plus the `ObjectSchema.create({` call, never one exact import line. The case is neither skipped nor quarantined. **Docs (ruling item 2).** The sweep is `git grep -n "generate schema" -- content/docs`, run at BASE: | hit | disposition | |:--|:--| | `content/docs/api/data-flow.mdx:200`, the JSON Schema row ("Autocomplete and validation for `objectstack.config.ts` (via `os generate schema`)") | **removed** | | `content/docs/references/api/plugin-rest-api.mdx:107` | kept: a substring of "Auto-generate schemas", the REST plugin's `generateSchemas` option, in an auto-generated reference; it is not the command | | `content/docs/references/api/plugin-rest-api.mdx:282` | kept: same | - One docs edit goes beyond the grep-found set, declared here. The Mermaid node `D[JSON Schema]` and its two edges, in the same section directly above the row, pictured the removed row's output ("Build Time: generate, then JSON Schema, then feed IDE Autocomplete"). They are removed, and `TypeScript Types` now feeds `IDE Autocomplete` alone. - The page's front matter is untouched. objectstack-ai#20170 landed its title-rule hunk on it, and the merge was clean. - `content/docs/deployment/cli.mdx` (patch round 1) gains a warn-type Callout, "`os generate schema` is retired", in the `os generate` section beside the `os g agent` one, the same shape as the two prior CLI retirements. It points at `os validate` (linked to the page's own `os validate` heading) and at the per-type schemas `@objectstack/spec` publishes. The refusal's `Docs:` link lands on this page. The page never listed a `schema` sub-type, so nothing else on it changes. - Also swept, with nothing to change: - `packages/cli/README.md` carries only the generic `os generate` row; - `skills/` has 0 hits; - `content/docs/protocol/diagram.mdx:243-255` (see Acceptance notes). **Changeset** `.changeset/19098-retire-generate-schema.md`: - `@objectstack/cli: minor`, with a **BREAKING** banner and `Clause-②: no (narrowing)`; - the ADR-0087 marker in the `os g agent` wording (`not-required (no-migration-prescription)`); - reach outside the repository stated as NOT MEASURED (no telemetry); - the refusal's pointer given as the migration: delete the call and any editor mapping of `objectstack.schema.json`, run `os validate`, and point editors at the per-type schemas. ## Verification, patch round 1 (at HEAD `2cb9e9613` unless stated) - **Merge.** `origin/main` `17bd31877` merged by merge as `9f702ee94` (`scripts/pm/os-regen-merge.sh`). No incoming file is one of this PR's. - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 93 commands: round 0's 92 plus `pnpm check:cli-examples-parity`, from the `cli.mdx` edit. All 93 exited 0 at `2cb9e9613`. `--ran` with the recorded exit codes reads "93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero). - **Named gates:** - `pnpm lint`: exit 0 (118 s); - `node scripts/check-issue-citations.mjs --base origin/main` (`ae8e3ca01`): exit 0; - `pnpm check:docs-audit-scope` and `node scripts/docs-audit/check-affected-docs.mjs`: exit 0 each; - `pnpm check:docs-transcript-drift` and `pnpm check:cli-examples-parity`: exit 0 each; - `pnpm --filter @objectstack/cli typecheck`: `VERDICT command-exit 0`, with the new pin inside the `tsconfig.test.json` program (`--listFilesOnly`: 1 hit). - **Per-PR pin.** `pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/generate-refuses-retired-generator.test.ts`: 7/7. It sits in the queue's `integration` population (54 to 55 files) and is absent from the nightly population. The partition pin `test/vitest-tiers-partition.test.ts` passes 22/22. - **Nightly.** `OS_TEST_TIERS=nightly pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/generate-schema-retired.e2e.test.ts test/generate-agent-retired.e2e.test.ts`: 2 files, 19/19. That includes "the generators that were not retired still work", the objectstack-ai#20270 case. - **Shape proof for objectstack-ai#20270.** The two new assertions accept the current emission: through the exported template, the `ObjectSchema` import matches and the `create` call matches. They reject the pre-objectstack-ai#20195 emission taken from `0bd11261e^`: neither matches. - **Ablations on the per-PR file.** These ran on committed state `a2dda36c2`. `generate.ts` and the per-PR file are byte-identical at `2cb9e9613`. | leg | mutation | on-disk proof | per-PR pin result | restore | |:--|:--|:--|:--|:--| | A | `schema` ledger key renamed to `schema_ablated` | anchor 1 to 0; blob `62cb7ec8b8ed` to `7a1f27dd8cfb`; grep `schema-entry=0 ablated-entry=1` | **2 failed**, 5 passed: "is the retirement, not a missing name and not an unknown type"; "names the ruling, `os validate` and the per-type schemas" | blob equals HEAD `62cb7ec8b8ed`, `git diff HEAD` empty | | B | own-key read reverted to `RETIRED_GENERATORS[args.type]` | anchor 1 to 0; blob to `26ec87322f84`; grep `own-key=0 bracket=1` | **1 failed**, 6 passed: the `os g constructor thing` case | same | After both legs, the restored per-PR run passed 7/7. - **History.** The objectstack-ai#20270 fix is its own commit, `a0cec66e9`. A first push, `a2dda36c2`, had carried a first version of it together with the per-PR pin and the callout. Rewriting that pushed commit was refused by the session's permission classifier, so the split line was merged with the pushed head instead (`2cb9e9613`, whose tree is identical to `a0cec66e9`'s). Nothing pushed was discarded. ## Verification, round 0 (at HEAD `c1036ebb9` unless stated) - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 92 commands, and all 92 exited 0 at `c1036ebb9`. `--ran` with the recorded exit codes reads "92 derived, 92 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero). - `check-adr-0087-registration --base origin/main` reads `[BREAKING+clause-②-narrowing] not-required (no-migration-prescription)`. - `check-changeset-no-major` reads "no `major` bump". Its level axis reads the PR body, so it is judged in CI. - **`pnpm lint`** (repo-wide, `eslint . --no-inline-config`): exit 0, 130 s. - **`pnpm --filter @objectstack/cli typecheck`**: `VERDICT command-exit 0`. That is `tsc --noEmit` plus `check:test-typecheck`, and the new pin is inside the `tsconfig.test.json` program (`--listFilesOnly`: 1 hit). - **Board check.** `node scripts/check-issue-citations.mjs --base origin/main` (`a9fb83ef0`) exits 0: 73 citations judged, 71 resolve, 2 cross-repo. - **`unit` layer**, run at merge commit `5c7c5bee3`: - `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2` ran 229 files: 227 passed and 2 failed as prerequisite refusals ("packages/cli is not built"). - After the CLI build, those 2 files passed (29 tests). - `c1036ebb9` differs from `5c7c5bee3` only in the new pin, which is not in the unit population. - **Integration tier.** The retirement pin is `*.e2e.test.ts` like the `agent` precedent, so it is **nightly-tier**: the per-PR queue run does not select it. - Run locally as `OS_TEST_TIERS=nightly pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2` over the new pin plus `generate-agent-retired`, `generate-skill` and `generate-object-namespace-prefix`. - Result: 38 passed and 1 failed. The failure is the pre-existing stale control in `generate-agent-retired.e2e.test.ts` (see Acceptance notes); the new pin is 10/10. - **Reach checks** on the dev runner, BEFORE at BASE and AFTER at HEAD: - `os generate schema -o out.json` went from exit 0 and a 3,335,734-byte file written to exit 1, the refusal, and nothing written; - `os g agent` went from "Missing required argument" to the agent retirement; - `os g constructor foo` went from a `TypeError` crash to the ordinary refusal. **Ablation.** This is a one-shot proof, run against the committed state `c1036ebb9` through `scripts/ablation-replace.mjs` under a script-level `trap` restore. The subject resolves from `src/` via tsx, so no dist is involved. | leg | mutation | on-disk proof | pin result | restore | |:--|:--|:--|:--|:--| | A | `schema` ledger key renamed to `schema_ablated` (the lookup misses it) | anchor 1 to 0; blob `62cb7ec8b8ed` to `7a1f27dd8cfb`; grep `schema-entry=0 ablated-entry=1` | **4 failed**, 6 passed: "was RETIRED", "names the ruling", `os validate`, per-type schemas | blob equals HEAD `62cb7ec8b8ed`; `git diff HEAD` empty | | B | own-key read reverted to `RETIRED_GENERATORS[args.type]` | anchor 1 to 0; blob to `26ec87322f84`; grep `own-key=0 bracket=1` | **1 failed**, 9 passed: the `constructor` own-keys assertion | same | In leg A the exit-1 and writes-nothing assertions stayed green, because the missing-name path also exits 1 without writing. That is exactly why the pin asserts the refusal's content, as the `agent` precedent does. ## Acceptance notes 1. Now repaired here (objectstack-ai#20270, commit `a0cec66e9`): the live-generator control in `packages/cli/test/generate-agent-retired.e2e.test.ts` asserted `import * as Data from '@objectstack/spec/data'`, which the object template stopped emitting in `0bd11261e`. - Triage's note-2 sweep covered all 73 nightly-tier files under `packages/cli` for pre-objectstack-ai#20195 template text (`import * as Data from '@objectstack/spec/data'`, `Data.ServiceObject`). That line was the only nightly hit; the control, the same sweep at `9f702ee94`, finds it. - Two per-PR files carry the old shape on purpose and are not changed: `scaffold-emission-typechecks.test.ts` (a tsc canary fixture) and `scaffold-object-declaration-shape.test.ts` (it refuses the pre-ruling literal). 2. `.changeset/sour-moons-smile.md`, pending and unreleased, records the objectstack-ai#17903 ladder repair this PR deletes. It is left unchanged. - Rewriting it here is the case `check-empty-changeset.mjs` classes as a DELIBERATE CORRECTION (objectstack-ai#18160, ruling D on objectstack-ai#17712). The gate goes red on any modification or deletion of a changeset from the merge base, and its remedy is "do NOT restore it -- say so on the PR and get it confirmed". - The seat answered the open question with A: leave the file. The new changeset tells the reader that this retirement supersedes that repair, and the release owner may drop the file at compilation. 3. The docblock at `packages/spec/scripts/lib/refinement-projection.ts` (the "Producers outside `packages/spec`'s own artifacts" bullet) still names the CLI's `os generate` as a direct `z.toJSONSchema` producer. It goes stale once this lands, but `packages/spec` is untouched by ruling item 4. 4. The `GENERATORS[type]` lookup in `runMetadataGeneration` is still an inherited-key read. So `os g constructor NAME` answers "No file naming convention is declared for type: constructor" rather than "Unknown type". The answer is misleading but does not crash, and it is not changed here. 5. Reach outside this repository is NOT MEASURED (no telemetry). Inside it there is no reader and no editor mapping of `objectstack.schema.json`. 6. `content/docs/protocol/diagram.mdx:243-255` carries a `D[JSON Schema]` node feeding IDE autocomplete, and a row "Generated JSON Schema files for IDE validation". It does not name the command, and the plural "files" reads as the per-type schemas `@objectstack/spec` still publishes, so it is left unchanged. --- _Generated by [Claude Code](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