Skip to content

fix(cli): os init and os generate object declare the scaffolded object with ObjectSchema.create - #20195

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-19722-scaffold-object-factory
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-19722-scaffold-object-factory

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

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 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 「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 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.
  • 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

…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 27, 2026
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 3 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/cli/src/utils/emitted-source-parses.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-flow.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/deployment/cli.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts), os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/examples.mdx (via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/getting-started/your-first-project.mdx (via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/plugins/index.mdx (via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/protocol/kernel/index.mdx (via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/protocol/kernel/lifecycle.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts))
  • content/docs/protocol/objectql/types.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts))

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

  • content/docs/releases/v17/17-1.mdx (via os init (command, read off packages/cli/src/commands/init.ts))
  • content/docs/releases/v17/17-4.mdx (via os generate (command, read off packages/cli/src/commands/generate.ts), os init (command, read off packages/cli/src/commands/init.ts))

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
  • 1 changed file(s) yielded no anchor (packages/cli/src/utils/emitted-source-parses.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 207 client-bound route-ledger rows — the other 153 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 153: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 98 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 25 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 d7c024133e77f69aa0f26af359391c6a4b0142e4 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5575e5a3039849e34164ca61d00cb483ca9d6e63 — the merge of head 468000c4d33c114821ce729f204f2286965f4673 into base d7c024133e77f69aa0f26af359391c6a4b0142e4, 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 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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3082b024c60d6bd0e137882dbbc7182a120ac757

① Derived judgments

  • Both emitters emit the ruled factory shape, with a value import from a published subpath — correct. packages/cli/src/commands/init.ts TEMPLATES.app.srcFiles['src/objects/__name___item.object.ts'] (:649) and TEMPLATES.plugin (:744), and packages/cli/src/commands/generate.ts GENERATORS.object.generate (:101–137) all emit import { ObjectSchema } from '@objectstack/spec/data'; (no type), one const X = ObjectSchema.create({ … });, export default X;. packages/spec/package.json exports['./data'] maps to dist/data/index.* and files ships dist; packages/spec/src/data/index.ts:124 export * from './object.zod' carries ObjectSchema with its create() (object.zod.ts ~:2498–2514). The two doors agree by construction and by pin (scaffold-object-declaration-shape.test.ts "one signature across every door"). Parity with create-objectstack is on the factory and the same value import from the same subpath (packages/create-objectstack/src/templates/blank/src/objects/note.object.ts export const Note = ObjectSchema.create({…})); export style (named there, default here) and Field.* helpers there vs plain field literals here differ, which the ruling does not govern.
  • Emitted file typechecks, parses and the gate accepts it — correct. CI evidence on the head: scaffold-emission-typechecks.test.ts spawns tsc --noEmit over every TEMPLATES and GENERATOR_SCAFFOLD_TARGETS emission; generate-scaffold-validates.test.ts bundleRequires the os generate object scaffold (so create() executes); init-scaffold-authoring-rules.test.ts writes both init templates and runs validateScaffold (schemaError must be null). None is a .e2e/.live nightly-tier file, so all three are in the queue population Test Core ran (success). Gate: scripts/check-keyed-text-bounds.mjs literalShapeDeclarations (:723–777): BINDING_HEAD matches const myAppItem, its initializer matches CREATE_INITIALIZER and is skipped; DEFAULT_EXPORT_HEAD reaches myAppItem;, not {, so consider returns; CREATE_CALL (:671) counts the one declaration. The gate's own self-test (:1642–1644) names export default ObjectSchema.create({…}) GOOD. The gate's walk never reaches a scaffold (templates are string literals; scaffolds land in tmpdir), so the new pin, not the gate, is what CI holds on this output.
  • Authored OWD comment block preserved — correct. init.ts diff is 6/6 lines: the import line, the const … = ObjectSchema.create({ line and }; → }); in each template; the seven comment lines are unchanged context. init-template-comments-self-contained.test.ts untouched and green.
  • Pins genuine — correct. New packages/cli/test/scaffold-object-declaration-shape.test.ts: roster derived from TEMPLATES and GENERATOR_SCAFFOLD_TARGETS (exported at generate.ts:430); on the old bytes shapeFindings yields absent import, ObjectLiteralExpression initializer and carries a type annotation, which its first control asserts verbatim; second control refuses import type. Repointed generate-refuses-unparseable-name.test.ts:255 and generate-emission-parses.test.ts:148 fail on the old shape; :167 const class = is still a parse failure (reserved word). The remaining edits (scaffold-emission-typechecks.test.ts, generate-refuses-name-outside-charset.test.ts, emitted-source-parses.ts) are docblock-only, as the PR says.
  • Changeset sentences — correct, and the ruling item 3 rewrite is stated. .changeset/19722-scaffold-object-factory.md (@objectstack/cli: patch): the before/after block is byte-shape-identical to the old and new init.ts template ("wrap the literal, drop the annotation, import the factory"). "Projects you already scaffolded keep working" — no reader changed in the diff; ObjectSchema is .strict() at the schema (object.zod.ts:1389), so defineStack parses both shapes alike; PR feat(scripts): refuse a non-factory *.object.ts declaration by name (ruling item 2 + census) #19720's sole file is scripts/check-keyed-text-bounds.mjs, so no published command refuses the old shape. "unknown top-level key … the message names it" — object.zod.ts:2514. Barrel lines unchanged (init.ts:647, generate.ts:970).
  • cli.mdx — one sentence wrong. The new os init paragraph (:144–151) is true on every clause. But the replacement at :1450, "the other types as typed literals (UI.View, Automation.Flow, etc.)", is false for skill: generate.ts:379 emits const …Skill = defineSkill({, its docblock (:375) says "Authored through defineSkill rather than as a bare typed literal", and the same page's callout ~20 lines above says skills are authored "with defineSkill". Five of six other generators are typed literals; the sentence asserts all. Wrong — a false statement introduced on a published page.
  • No other doc page states the old shape as current — correct. concepts/metadata-driven.mdx:363 shows const Account: ServiceObject = { under a "❌ Deprecated" heading; getting-started/quick-reference.mdx:298 is an import type listing; plugins/packages.mdx:34 is an import example; your-first-project.mdx:176 and schema-design.mdx:14–19 show the factory.

② Semver level

patch is right. AGENTS.md Post-Task Checklist 3: a bug fix in a released package takes patch; Clause-②: no is the ruling's own item 4 and holds on the diff — only emitters, docblocks, tests and docs change; no reader, accept-set or published payload moves (the PR's byte-identical dist/objectstack.json is consistent with that). Nothing an author can write is removed or renamed; prior output keeps loading through the same .strict() schema; the only refusal of the old shape is a repo script, not a published command. So users of prior output are not required to convert — the changeset says so and still states the mechanical rewrite as ruling item 3 demands.

③ Boundary flags

  • const X = ObjectSchema.create(…) + export default X rather than export const — sound. Both barrels re-export default (init.ts:647, generate.ts:970) and the init config does import * as objects from './src/objects' + Object.values(objects), so no barrel or config spelling moves; keeping default is what makes the changeset's rewrite exactly the emitted file.
  • Held files untouched — yes. The PR's 10 files contain none of packages/cli/src/utils/scaffold-validate.ts, packages/cli/src/commands/{validate,compile,lint}.ts, packages/spec/**, scripts/**; merge commit 3082b024 equals the parents' auto-merge tree (no evil merge); its diff against 836aad2a is exactly the 10 PR files.
  • CI on the head: 36 check-runs, 34 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in)); all seven required contexts success. PR is draft, no governed path in the file list.
  • Attribution: both commits carry the model-free trailer pair; PR body footer is the session-URL form.
  • End-to-end os init is not exercised by CI; the emitted-file evidence in CI is the three cli pins named above, which is adequate.
  • Residual for the spec lane (correctly not touched here): scripts/check-keyed-text-bounds.mjs:787 refusal text "the os init shape imports only * as Data" now describes pre-change output only; noted in the PR's acceptance notes.

Implemented-by: claude/issue-19722-scaffold-object-factory
Reviewed-by: session_01UYBdGBzWSrAMzpW8ah3GbP

Independence: INDEPENDENT AGENT (fed the card, the ruling and the PR only; not the dispatch order or the seat's conclusions)

VERDICT: FAIL

  • content/docs/deployment/cli.mdx:1450 ("What it does", item 1): correct the clause "the other types as typed literals (UI.View, Automation.Flow, etc.)" so it does not claim os g skill writes a typed literal — generate.ts:379 emits defineSkill({ … }) and the page's own callout above says so. Everything else in the record stands; on that one edit the verdict is PASS.

Generated by Claude Code

… 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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 468000c4d33c114821ce729f204f2286965f4673

Delta of: 5853875973 (FAIL on 3082b024)

① Derived judgments

  • Required change met — correct. The delta on the PR's own files is one line: content/docs/deployment/cli.mdx:1450 (delta stat restricted to the 10 PR files: cli.mdx | 2 +-, nothing else). The clause now reads "an object declared with ObjectSchema.create({ … }), the same shape the os init templates write; a skill declared with defineSkill({ … }); the other types as typed literals (UI.View, UI.Action, Automation.Flow, UI.Dashboard, UI.App)". The "etc." is gone and the list is closed.
  • Every claim in that sentence against GENERATORS at head — correct for all seven keys, none omitted. packages/cli/src/commands/generate.ts: object :111 const X = ObjectSchema.create({ with :106 import { ObjectSchema } from '@objectstack/spec/data'; view :166 const XViews: UI.View = {; action :208 UI.Action; flow :251 Automation.Flow; dashboard :295 UI.Dashboard; app :326 UI.App; skill :379 const XSkill = defineSkill({ (:366, from @objectstack/spec/ai). GENERATORS holds exactly those seven entries (:77–:416); runMetadataGeneration does a direct GENERATORS[type] lookup (:859), no alias or case-folding, so the accepted roster equals the sentence's roster. agent is in RETIRED_GENERATORS (:472) and exits 1 (:848–:857); the page's callout at :1431 says so. types / client / migration / schema are routed before metadata generation (:3154–:3175) and are not in the "Available types" table, so a reader cannot misread them into item 1.
  • "the same shape the os init templates write" — correct. init.ts :649–:651 (app) and :744–:746 (plugin): same value import, const XItem = ObjectSchema.create({, });, export default XItem;. empty has srcFiles: {} (:816), and the os init paragraph names only app and plugin. scaffold-object-declaration-shape.test.ts is byte-identical to 3082b024; its roster is derived from TEMPLATES + GENERATOR_SCAFFOLD_TARGETS (:53–:54, :138, :144) and it asserts "one signature across every door" (:166).
  • The os init paragraph (cli.mdx:144–151) — correct on every clause at head. Factory in both templates; "validates … when the file is evaluated" is object.zod.ts:2873–2915 create(), which throws on unknown keys before parsing and then runs ObjectSchemaBase.parse(withDefaults); "an earlier release carries a Data.ServiceObject-annotated object literal" is the pre-diff template (init.ts diff: -const … : Data.ServiceObject = {); the stated rewrite matches the changeset's.
  • No other sentence on the page misdescribes the emitters — correct. At head the page's only ServiceObject mention is :149 (the paragraph above). The "Available types" table (:1404–:1412) matches each entry's description / defaultDir. At the base d7c02413 the page's only mention was :1441 (the replaced line) and :1323 is a sys_metadata index table, so the PR body's "the ruling's anchor drifted" is true.
  • No other doc page states the old shape as current — still correct after the merge. Tree-wide at head, the only const X: ServiceObject = { under content/docs is concepts/metadata-driven.mdx:363, under "❌ Deprecated".
  • Changeset (.changeset/19722-scaffold-object-factory.md, unchanged in the delta) re-judged at head — every claim holds. "now declare … with ObjectSchema.create": init.ts:651 / :746, generate.ts:111. "parses … when the file is evaluated": create() above. "create-objectstack's starter … already used the factory": packages/create-objectstack/src/templates/blank/src/objects/note.object.ts:5 export const Note = ObjectSchema.create({. "the data-modeling docs": schema-design.mdx:14–19. "every object file in this repository": 110 tracked *.object.ts at head, every one contains ObjectSchema.create, none carries a : ServiceObject annotation (git grep). "the two CLI doors were the outliers": the datasource emitter (os datasource introspect emits the annotated-literal .object.ts shape ruling 5644350230 refuses — and the generator's own docblock calls its output a *.object.ts #19723, closed 2026-09-23) writes export const X = ObjectSchema.create({ at head (packages/services/service-datasource/src/external-datasource-service.ts:956–958); TypeScriptSerializer emits an annotated object literal although packages/metadata's own README says it is for ObjectSchema.create() #19724's TypeScriptSerializer still emits export const metadata: ServiceObject = … (packages/metadata/src/serializers/typescript-serializer.ts:97–99) but that card closed on the measured finding that no caller writes it to a *.object.ts (ACCEPT comment 5793838396, "A1 = NO"), so it is not a *.object.ts door and the claim stands. "barrel lines … unchanged": init.ts:647, generate.ts:970 export { default as X } from …. "the unchanged export default myAppItem;": init.ts:684. "keep working … nothing reads the old file differently": no reader in the PR surface; ObjectSchema is .strict() (object.zod.ts:1389) for both shapes. "the message names it": object.zod.ts:2514.
  • PR body verification section — correct; nothing claimed that the head does not support. Line 27's addition "names defineSkill({ … }) for skill, the one non-object type that is not a typed literal" is true (five typed literals, one factory, one defineSkill). Line 62 "origin/main d7c02413 merged as e5499d52, then the one-sentence cli.mdx correction" matches git log (e5499d52 parents 3082b024 + d7c02413; 468000c4 parent e5499d52, 1 file / 1 line). It says plainly the cli test tiers were not re-run locally; CI ran them (below). The local "41 docs-scoped gates" count is not independently verifiable; it is consistent with Lint & Repo Gates, Build Docs and Check Documentation Links all success on the head.

② Semver level

Unchanged by the delta: patch + Clause-②: no stands. The delta on PR files is one documentation sentence, and the merge changed no PR file: diff d7c02413..e5499d52 is exactly the 10 PR files, and diff 3082b024..e5499d52 is byte-identical to diff 836aad2a..d7c02413 (no evil merge). Main's only packages/spec/src/data change in the merged range is field.zod.ts .describe() strings and docblocks (three hunks, no acceptance change), and its only packages/cli change is a new test file (validate-field-predicate-traversal.test.ts), so neither the scaffolds' field shapes nor the pins move. No reader, accept-set or published payload changes anywhere in the PR surface.

③ Boundary flags

  • Merge e5499d52 is clean. Name list and diff content identical to what main carried in 836aad2a..d7c02413; no PR file touched by it. Held files untouched: packages/spec/**, scripts/**, packages/cli/src/utils/scaffold-validate.ts and packages/cli/src/commands/{validate,compile,lint}.ts are all absent from the 10-file surface.
  • Main since the base: one commit (560b724c: packages/formula, packages/lint tests, packages/spec/src/migrations); touches neither packages/cli nor cli.mdx; PR mergeable_state: clean.
  • CI on 468000c4 (check-runs API): 43 check-runs, 39 success, 4 skipped. The seven required contexts (rulesets API for main: TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Lint & Repo Gates, Governed Surface Queue Guard) are all success. Skips: Console Pin Gate and Packed-tarball smoke (opt-in) (same two as on 3082b024), plus the Auto Label and Check PR Size jobs of the 07:58 PR Automation re-run on the PR-edit event, whose 07:40 runs on the same sha are success. PR is draft; no governed path in the file list.
  • Attribution: both round-1 commits (e5499d52, 468000c4) carry the session trailer and a model-free co-author line; the PR body footer is the session-URL form.
  • Pins: none changed in the delta; Test Core success on the head covers scaffold-emission-typechecks, generate-scaffold-validates, init-scaffold-authoring-rules and the new declaration-shape pin.
  • Residual, not this card's and not a page falsity: generate.ts:3137 Args.type description lists six types and omits skill (pre-existing help string, outside the ruled surface). Still residual for the spec lane: scripts/check-keyed-text-bounds.mjs refusal text describes the pre-change os init shape (in the PR's Acceptance notes). The namespace-prefix finding is filed as [finding] os generate object NAME in an os init -t app project writes name: 'NAME' with no namespace prefix, so the next os validate refuses the object the CLI just generated #20197.

Implemented-by: claude/issue-19722-scaffold-object-factory
Reviewed-by: session_01UYBdGBzWSrAMzpW8ah3GbP

Independence: INDEPENDENT AGENT (fed the card, the prior review, and the PR only; not the dispatch order or the seat's conclusions)

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 08:10
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 0bd1126 Sep 27, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19722-scaffold-object-factory branch September 27, 2026 08:28
os-litant pushed a commit that referenced this pull request Sep 27, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants