Skip to content

fix(cli): os generate schema falls back like every other toJSONSchema call site, so the IDE schema it exists to write is written - #17903

Merged
claude[bot] merged 4 commits into
mainfrom
claude/issue-17873-generate-schema-unrepresentable
Sep 12, 2026
Merged

claude[bot] merged 4 commits into
mainfrom
claude/issue-17873-generate-schema-unrepresentable

Conversation

@claude

@claude claude Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #17873

Clause-②: no

Declared by the domain:cli seat (#6024), not by the implementer, and re-measured on this head 30361ba9b45626203f1e95267f0fb0baa22a9f0f rather than carried over from the claim: the per-file matcher over the added lines of packages/cli/src/commands/generate.ts returns zero exported declarations, with the control lit — the word export occurs 28 times in that file, so the zero is a reading and not a dark instrument. Removed export lines: 0. No new flag (Flags. appears on no added line). The handling is confined to the body of the module-private runSchemaGeneration, and packages/cli/src/index.ts publishes this module through export { default as GenerateCommand } — the default export only. ⇒ the axis stands down and the changeset's patch on @objectstack/cli stands. Card-side carrier: comment 5649039554 on #17873.

runSchemaGeneration in packages/cli/src/commands/generate.ts was the one z.toJSONSchema call site in this repository that neither fell back nor used the unrepresentable convention. Its bare two-argument call has no JSON form in either io direction on this tree, so the catch below it did printError(...) + process.exit(1) and the command could never reach its own fs.writeFileSync — for any repository, for any flags.

It now runs the three-tier ladder packages/spec/scripts/build-schemas.ts already runs for every schema it publishes, with packages/metadata-protocol/src/protocol.ts's unrepresentable: 'any' as the third tier. Each tier re-raises anything the known-unsupported predicate does not recognise, so a real conversion failure is still loud. The handling is entirely inside the module-private function: no new flag, no new accepted key, no new exported symbol.

Reproduction, with a lit control

Taken on this branch's merge base, through the built @objectstack/spec, with the card's own control:

CONTROL-lit (output mode, same options object, z.object({ a: z.string() })): OK
generate-schema call (output):                 THROWS - Transforms cannot be represented in JSON Schema
io: 'input':                                   THROWS - Function types cannot be represented in JSON Schema
io: 'input' + unrepresentable: 'any':          OK, 44 properties

Per member, in the strict authoring direction, 4 of 44 have no JSON form — exactly the four the card named:

packages   <-  Function types cannot be represented in JSON Schema
hooks      <-  Custom types cannot be represented in JSON Schema
functions  <-  Function types cannot be represented in JSON Schema
onEnable   <-  Function types cannot be represented in JSON Schema

After the change, a real run writes the artefact:

os generate schema -o objectstack.schema.json   ->  exit 0, 3245514 bytes
  i Converted in the authoring (input) direction - the output direction contains a transform with no JSON form
  i Nodes with no JSON form (live callables) are published as unconstrained - they accept any value in this schema

维护者速读(草稿)

改了什么。 os generate schema 过去 100% 失败——它是全仓唯一一处既不回退、也不用 unrepresentable 约定的 toJSONSchema 调用点。现在它按 build-schemas.ts 已有的三层阶梯降级,第三层用 metadata-protocol 已有的 unrepresentable: 'any'。⛔ 没发明第四种机制,⛔ 没加 flag,⛔ 没加导出符号。

为什么改。 分诊已裁:「命令应当像本仓其他调用点那样处理不可表达成员(回退 + unrepresentable: 'any'),⛔ 不应当裸调用后直接 process.exit(1)。这是往本仓已确立的约定上收,⛔ 不是新决策。」

⭐ 这份 IDE schema 现在对那四个成员承诺什么——逐条读自产物本身,不是读自源码。

产物落在第三层(authoring 方向 + unrepresentable: 'any'),$schema = https://json-schema.org/draft/2020-12/schema,顶层 type: object、44 个 properties、additionalProperties: false。

成员 产物里的片段 对作者的承诺
onEnable 只有 {"description": "…"}——⛔ 无 type、无任何约束 整个成员不再被校验:写什么都通过,IDE 对它一个字都不说
hooks type: "array",items 是完整对象(required: ["name","object","events"]、additionalProperties: false);handler = {"anyOf": [{"type":"string"}, {}]} 数组与每条 hook 的结构照常校验;只有 handler 的「内联函数」那一支不受约束
functions {"description": "…", "anyOf": [映射形式, 数组形式]},两支结构均保留;handler 位置为 {} 两种写法都照常校验;只有 handler 的值不受约束
packages type: "array",items 完整;不受约束的点在 manifest.hooks[].handler 与 manifest.functions 的 handler 上 包体结构照常校验;只有其中嵌套的 handler 可调用体不受约束

⚠️ 一处需要更正卡与分诊的共同措辞:unrepresentable: 'any' 并没有把这四个成员整体变成 {}。整体变成不受约束的只有 onEnable 一个;另外三个成员的结构完整保留,被放宽的是它们内部的可调用叶子节点。另一半读数同样重要:产物里绝大多数 {} 与本次改动无关——只能严格转换、⛔ 根本不经过第三层的成员(objects / apps / views)本身就各带 40 / 11 / 41 个 {},它们来自 spec 里原有的 z.unknown() / z.any()。真正因本次放宽而产生的节点,全文档只有 5 个(4 个在 packages 下的 handler 上,1 个是 onEnable 本身)。

另外,已退役的键(如 hook.timeout)在产物里是 {"description": "[REMOVED] …", "not": {}} —— not: {} 是拒收,不是放宽:墓碑照常拒绝该键,并把迁移说明带给 IDE 用户。

⛔ 没有采纳的三条出路,各自会承诺什么。

  • 按名丢弃这四个成员:IDE 会把 packages / hooks / functions / onEnable 当作未声明的键——在 additionalProperties: false 之下它们会被报成错误,即一份合法的 objectstack.config.ts 被标红。
  • unrepresentable: 'any' 走 output 方向:同样能产出文件,但 output 派生会把 752 处带 default 的属性升级为 required,于是一份合法配置被报成「缺 752 个键」。本 PR 因此取 authoring 方向,并把这一点写成一条断言。
  • 退役该命令:content/docs/api/data-flow.mdx 承诺的那条 IDE 自动补全通路(以及 VS Code 扩展的 jsonValidation 指向一个不存在的文件 —— schemas/objectstack.schema.json 从未存在过,也没有任何脚本生成它 #5028 想要的那个文件)将永久没有生产者。⛔ 退役属维护者底线,本席无权执行。

风险与代价(含回滚)。 风险集中在一句话:这份 IDE schema 从「不存在」变成「存在但对上述可调用叶子不设限」。回滚成本极低——改动全在一个模块私有函数体内,git revert 即回到今天的「命令必失败」状态,⛔ 不影响运行时、⛔ 不影响任何已发布的导出面。

席位意见。 ⛔ 本席不答这一问,也不代答。 这四个成员对外承诺什么是对外承诺的改变,分诊明写「⛔ 不可裁(确属产品)」,交付席与本席都无权替你选;上面那一问就是原样交到你手上的。本席只提供三件可核的事实,⛔ 不含推荐:

  1. 上表逐条读自产物本身,不是读自源码;本席已独立复核其中的载重项(复核见本 PR 的 ## Contract review 评论),并确认卡与分诊共同的措辞「四个成员整体变成 {}」只对 onEnable 成立——这是交付方主动更正上游措辞,⛔ 不是本席事后补的。
  2. 本 PR 不依赖你的答案。 改动全在一个模块私有函数体内,回滚是一次 git revert,⛔ 不动任何已发布的导出面、⛔ 不改运行时行为。你答 no 不会让已落地的东西变成半成品。
  3. 若答 no,另外两条路各自会承诺什么已列在上一节(按名丢弃 ⇒ 合法配置被标红;output 方向 ⇒ 合法配置被报缺 752 个键),⛔ 本席对其中任何一条不作推荐。

⭐ 你要做的(一个 token 即可)。 上表就是本 PR 交付的产物对那四个成员的承诺,现在把它交给你否决:接受吗?yes / no。

Acceptance notes

Measured on the way through; none of it belongs to this card.

  • The pin runs in the NIGHTLY tier, not the merge queue. It spawns the CLI, so it is an e2e test by behaviour and carries the .e2e.test.ts name this repo's name-based switch (scripts/nightly-tiers.mjs) uses to move e2e off per-PR and merge-queue runs. runSchemaGeneration is module-private and §0 forbids exporting it, so spawning is the only way to run the command — there is no cheaper queue-tier pin available without a new export. Naming it out of the .e2e tier purely to buy queue coverage would be gaming that switch, so it is named for what it is and the consequence is declared here instead. Noted, not filed.
  • hooks.timeout is published as a tombstone, and that is the tombstone working. {"description": "[REMOVED] hook.timeout was removed in @objectstack/spec 17 …", "not": {}} — not: {} refuses every value, so an IDE rejects the retired key and shows the migration line. Noted, not filed.
  • Docs read, both ways. The only page in content/docs/** that mentions this command is content/docs/api/data-flow.mdx:200 — "JSON Schema | VS Code, IntelliJ | Autocomplete and validation for objectstack.config.ts (via os generate schema)". It states that the command produces such a schema and says nothing about what it promises per member, so it is true of the artefact this PR now produces and needs no edit. content/docs/deployment/cli.mdx's os generate section documents only the metadata-scaffold types and never mentions the schema sub-command at all. Control for the grep: os generate lights 5 pages in the same sweep. Noted, not filed.
  • The tier order is build-schemas.ts's, and it has one latent consequence worth stating. Tiers 1 and 2 both throw today, so the landing tier is 3 regardless of order. If a future spec removed the transform, tier 1 (output, strict) would land and the document would flip to the output derivation — the 752-defaulted-keys shape above. Assertion (e) of the pin is what would catch that. Noted, not filed: nothing reproduces it today.
  • VS Code 扩展的 jsonValidation 指向一个不存在的文件 —— schemas/objectstack.schema.json 从未存在过,也没有任何脚本生成它 #5028's half is untouched — no jsonValidation pointer is added or moved here.

Verification

leg reading
pin, green Test Files 1 passed (1) · Tests 8 passed (8)
ablation — handling reverted to the bare call on disk Test Files 1 failed (1) · Tests 8 failed (8)
ablation, on-disk proof injected marker = 1 · widenedUnrepresentable = 0 · unrepresentable: 'any', code lines = 0 · mutated blob 6d2865d0 ≠ HEAD blob 1fccbeff
restore git diff HEAD empty · git hash-object = 1fccbeffad798a50a13b3f8a97859ca574e16a23 = the HEAD blob
pin, re-run after restore Test Files 1 passed (1) · Tests 8 passed (8)

The ablation script carried trap restore EXIT INT TERM with an absolute REPO_ROOT, restored with git checkout HEAD -- <path> (never the bare form, which restores from the index), and refused to report a reading at all unless the mutation was proven present on disk — it fired once, correctly, on a first attempt whose expectation had miscounted comment text.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c


Generated by Claude Code


Generated by Claude Code

… call site

`runSchemaGeneration` was the one `z.toJSONSchema` call site in this repo that
neither fell back nor used the `unrepresentable` convention. Its bare
two-argument call throws in BOTH io directions on today's tree — a transform in
output mode, a function type in input mode — so the `catch` below it printed and
exited 1 and the command could never reach its own `fs.writeFileSync`.

Adopt the ladder `packages/spec/scripts/build-schemas.ts` already runs, with
`packages/metadata-protocol/src/protocol.ts`'s `unrepresentable: 'any'` as the
third tier. Each tier re-raises anything the known-unsupported predicate does
not recognise.

Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
Co-authored-by: Claude <noreply@anthropic.com>
Asserts the written file exists (listed, not assumed), its bytes parse, the
document is a JSON Schema of the declared draft, and each of the four members
#17873 required declaring carries the fragment that declaration names. A fifth,
derived assertion pins the authoring derivation: no object schema anywhere lists
a defaulted property as required — 752 do in the output derivation, each one an
IDE reporting a valid config as missing a key nobody had to write.

Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 4 documentable anchor(s).

4 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))
  • 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))

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

  • content/docs/releases/v17/17-4.mdx (via os generate (command, read off packages/cli/src/commands/generate.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
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 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.

Coarse fallback — 24 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 134b410a90c6e5e7311c840076dcd5d486841f1d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9ca24545986759e0b7fcd9b58584b069975e1336 — the merge of head 30361ba9b45626203f1e95267f0fb0baa22a9f0f into base 134b410a90c6e5e7311c840076dcd5d486841f1d, 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 9ca24545986759e0b7fcd9b58584b069975e1336 && git checkout 9ca24545986759e0b7fcd9b58584b069975e1336
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 134b410a90c6e5e7311c840076dcd5d486841f1d 30361ba9b45626203f1e95267f0fb0baa22a9f0f && git checkout -B drift-repro 134b410a90c6e5e7311c840076dcd5d486841f1d && git merge --no-ff 30361ba9b45626203f1e95267f0fb0baa22a9f0f

node scripts/docs-audit/affected-docs.mjs --json 134b410a90c6e5e7311c840076dcd5d486841f1d

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

@claude

claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Contract review — PR #17903 (card #17873)

Head reviewed: 30361ba9b45626203f1e95267f0fb0baa22a9f0f. Three paths, +359/−3. Readings taken 2026-09-12T22:43–23:20Z against origin/* refs and the merge base 134b410a90c6e5e7311c840076dcd5d486841f1d.

⚠️ Independence: the implementer is a mode:subagent dev of this same PM session; ⛔ not an arm's-length second opinion.

① Derived judgments — each measured here, ⛔ none ratified from the report

1. ⛔ The half triage refused to rule is DECLARED, not defaulted — and this seat did not answer it either. The ## 维护者速读(草稿) carries a four-row table read out of the artefact, names all three alternatives with what each would have promised (drop-by-name ⇒ a valid config reported invalid under additionalProperties: false; unrepresentable: 'any' in the output direction ⇒ 752 defaulted properties promoted to required, so a valid config is reported as missing 752 keys; retirement ⇒ the IDE path content/docs/api/data-flow.mdx:200 promises loses its only producer), and ends in one token: 「接受吗?yes / no」. ⚠️ The section shipped with an empty 「席位意见。」 heading — a maintainer-facing blank. This seat filled it, with process only: an explicit ⛔ refusal to answer or recommend, the three checkable facts, and the note that the PR does not depend on the answer. ⛔ No product judgment was added by this seat anywhere.

2. ⭐ The delivery CORRECTED the card and triage's shared wording, and the correction is right. Both said unrepresentable: 'any' turns the four members into {}. Measured on the artefact, that is true of onEnable alone; hooks, functions and packages keep their full structure and only their callable leaves widen — five widened nodes in the whole document. The pin encodes exactly that claim rather than restating it in prose: hooks.handler asserts branches.filter(isUnconstrained) toHaveLength(1) beside a surviving {type:'string'} branch; packages asserts the same length-1 on the nested manifest.hooks[].handler; onEnable asserts Object.keys(onEnable) equals ['description']. ⇒ a future widening of any other branch reds the pin. ⭐ It also says which {} are not this change's (objects 40 · apps 11 · views 41, from the spec's own z.unknown()/z.any()), and that a retired key's {"not": {}} is a refusal, not a widening — the two readings a censusing reviewer would otherwise get backwards.

3. ⭐ The third tier COULD NOT have been spec's, and that is forced by the ruling rather than chosen. build-schemas.ts's third tier is projectByPruningUnionBranches, which exists only at packages/spec/scripts/build-schemas.ts, packages/spec/scripts/lib/union-branch-projection.ts and its test; packages/spec/src/index.ts spells it zero times, with the control lit (export → 72 in that file). ⇒ importing it would require editing packages/spec, which §0 forbids and which this lane's standing rule routes to the domain:spec seat regardless of who needs it. So tier three is metadata-protocol's unrepresentable: 'any' — ⛔ not a fourth mechanism, and exactly what triage's acceptance line prescribed.

4. No published surface moves, and the level follows — re-measured on this head. The per-file matcher over the added lines of packages/cli/src/commands/generate.ts returns zero exported declarations, control lit (export occurs 28 times in that file); zero export lines removed; zero added lines mention Flags. or static flags. packages/cli/src/index.ts publishes this module as export { default as GenerateCommand } — the default export only. Paths under packages/spec/: 0. Under content/docs/releases/: 0. ⇒ Clause-②: no and @objectstack/cli: patch both stand. ⛔ Nothing was raised to quiet a gate and ⛔ no changeset was dropped.

5. The pin asserts the ARTEFACT, ⛔ never 「it did not throw」. Five groups: (a) exit 0 and the output file written by name; (b) the bytes parse as JSON; (c) $schema is the declared draft, $id, type: object, >30 properties, additionalProperties: false; (d) one sub-test per member, above; (e) the authoring derivation, no defaulted property published as required.

6. ⭐ And row (e) was VACUOUS on the first ablation — the dev found that itself and strengthened it BEFORE reporting. An empty document has no offenders either, so offenders == [] proved nothing; it now carries a lit control, expect(census.inspected).toBeGreaterThan(0), and both ablation legs were re-taken against the strengthened file. ⭐ That is the order's own standard (「a pin that stays green under ablation is vacuous and must be strengthened before you report」) met without being asked twice.

7. The ablation is executed, and its restore is proved by STATE rather than by an exit code. Mutated: 8 of 8 rows red, command exit 1, no file written. On-disk proof taken before any verdict was read — injected marker ×1, unrepresentable: 'any' ×0, and a mutated blob 6d2865d0 differing from HEAD's 1fccbeff. Restored: 8 of 8 green, with git diff HEAD empty and git hash-object equal to the HEAD blob. ⭐ A first attempt whose expectation had miscounted comment text was discarded and the anchor corrected, ⛔ not silently re-run.

② semver level — patch on @objectstack/cli

Consistent with ①.4: the command is pulled back to behaviour it already declares (it could never reach its own fs.writeFileSync), with no exported symbol and no new accepted flag or key. Check Changeset is green on this head after the declaration was written into the body — see ③.b.

③ Boundary flags, the docs advisory, and one consequence stated plainly

a. ⚠️ The central pin runs in the NIGHTLY tier, ⛔ NOT the merge queue — and that is the honest naming rather than a gap being hidden. It spawns the CLI, so it is an e2e test by behaviour and carries the .e2e.test.ts name scripts/nightly-tiers.mjs switches on. runSchemaGeneration is module-private and §0 forbids exporting it, so spawning is the only way to run the command at all; renaming it out of the tier purely to buy queue coverage would be gaming that switch. ⇒ the consequence, said plainly: this PR's load-bearing pin does not gate the merge queue, and the first PR to touch this command inherits that. Declared in the PR's own acceptance notes. ⛔ Accepted as scoped, ⛔ not waved off.

b. The red Check Changeset was this seat's to clear, and it was cleared by a BODY EDIT alone. scripts/check-changeset-no-major.mjs reads the clause-② declaration out of github.event.pull_request.body and refuses a PR whose declaration is unreadable while a changeset grades patch a package the diff grew; the delivered body carried no such line. Written 22:43Z after re-measuring the axis on this head (①.4); new generation 103633509537 returned success with no push and no re-run — the workflow subscribes to edited for exactly this. Record 5649185377.

c. ⭐ The docs advisory's four hand-written pages were answered with a reading, and this seat re-took all four independently. Control lit on every page (os generate fires 1 / 7 / 6 / 3):

page what it says falsified?
content/docs/api/data-flow.mdx:200 the only os generate schema mention in the four: the JSON Schema gives VS Code / IntelliJ autocomplete and validation for objectstack.config.ts no — ⭐ and it was describing a capability that could not run; this PR makes the page true
content/docs/deployment/cli.mdx os generate schema ×0; its seven mentions are the metadata-scaffold types and the os generate <type> <name> table row no
content/docs/protocol/kernel/lifecycle.mdx os generate schema ×0; all six are os generate migration no
content/docs/protocol/objectql/types.mdx os generate schema ×0; all three are os generate migration no

content/docs/releases/v17/17-4.mdx is release-owned: 0 paths in the diff, ⛔ not read for editing.

d. Gates. 63 families derived, 63 run, all exit 0. ⭐ Four returned exit 3 = PREREQUISITE NOT MET on the first sweep (check:dual-build-cjs-loads, check:i18n, check:i18n-coverage, check:i18n-walk-parity), all for the same missing packages/cli/dist; re-run after the build, all four returned 0, and neither reading is reported as the other. pnpm lint exit 0 at this head. Package legs: typecheck 0; vitest run 249 files / 3332 tests passed. Heavy runs went through os-verify-lock.sh with verdicts read from its VERDICT line.

e. ③ is already satisfied on this head: 34 check names, all terminal — 29 success / 5 skipped / zero failures.

Independence pair

Implemented-by: claude/issue-17873-generate-schema-unrepresentable (mode:subagent)
Reviewed-by: os-sales — domain:cli execution seat, issue #6024, session_01TSf4DV7ziu4V5j73e46b7c
Independence: SELF-REVIEW — the implementer is a subagent of the reviewing seat's own session

Tier: default judgment — 「余席条款②复核 = 默认判断档自审加门禁」.

Verdict

PASS. ⛔ No carrier to clear: the claim declared the axis no, the measurement on this head agrees, and check-clause2-carriers --pair 17903 returns exit 0 (both carriers agree, no widening tell). ⇒ the landing pre-check is ③ every check green, which this head already meets. ⚠️ Landing this does not answer the maintainer's question in ①.1 — that question stands open on the card after the merge, and ⛔ merging is not consent to it: the rollback is one git revert of a module-private function body.


Generated by Claude Code

@claude
claude Bot marked this pull request as ready for review September 12, 2026 23:22
@claude
claude Bot added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit eadcde6 Sep 12, 2026
49 of 50 checks passed
@claude
claude Bot deleted the claude/issue-17873-generate-schema-unrepresentable branch September 12, 2026 23:41
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

1 participant