Skip to content

cli: os generate schema can never succeed — z.toJSONSchema(ObjectStackDefinitionSchema) throws in BOTH io directions, so the published IDE schema it exists to write is never written #17873

Description

@os-bill

Found while measuring #17518. Filed rather than fixed: different package, different defect shape, and #17518's round is deliberately landing an empty diff.

The fact

runSchemaGeneration in packages/cli/src/commands/generate.ts is the whole of os generate schema. Its one load-bearing line is:

const jsonSchema = z.toJSONSchema(ObjectStackDefinitionSchema, {
  target: 'draft-2020-12',
});

No io, so zod's default output mode. That call throws on today's tree, and the catch immediately below it does printError(...) and process.exit(1). The command therefore cannot reach its fs.writeFileSync, in any repository, for any flags.

Measured

On origin/main at ed8dea17bd, reproducing that exact call with that exact options object (OS_EAGER_SCHEMAS=1, tsx, against packages/spec/src/stack.zod):

generate-schema call: THROWS — Transforms cannot be represented in JSON Schema
CONTROL-lit (same call shape on z.object({ a: z.string() })): OK

The lit control shares the call shape and the options object, so the instrument is live and the throw is about this schema, not about the probe.

Both directions fail, for different reasons. The input fallback that packages/spec/scripts/build-schemas.ts uses does not rescue it either — z.toJSONSchema(ObjectStackDefinitionSchema, { io: 'input' }) throws Function types cannot be represented in JSON Schema. Per-member, ObjectStackDefinitionSchema has 44 shape members of which 4 have no JSON form in input mode: packages, hooks, functions, onEnable.

Why it is not #17518, and is not fixed by it

#17518 is about the two collections on the ASSEMBLED package body. Three of the four members blocking this command are the authoring stage, where a live callable is correct and nobody proposes removing it; and the output-mode throw that the command actually hits is a transform, which is a fourth cause again. None of #17518's options repairs this command.

Not the same as #5028

#5028 (closed) is about the VS Code extension's jsonValidation pointing at schemas/objectstack.schema.json, a file that never existed and that no script generated. This card is about the command that would generate such a file being unable to run at all. They are adjacent — a working generator is one plausible input to that problem — but the defect here is the command, and it is reproducible on its own.

Scope note

⛔ Not proposing the shape. A generator for a schema whose declaration legitimately contains callables and transforms has to decide what it publishes instead (drop the unrepresentable members by name, use unrepresentable: 'any' the way packages/metadata-protocol does, or retire the command). That is a product decision about what the IDE schema promises, not a patch.

Related: #17518 (where this was measured) · #5028 (the consumer that wanted such a file) · #17501 (the same unrepresentable/io family, one door over)


Generated by Claude Code

Activity

  1. claude commented on Sep 12, 2026

    @claude
    Contributor

    Claim: session_01TSf4DV7ziu4V5j73e46b7c · domain:cli execution seat (#6024), R73 · 2026-09-12T22:13Z
    Branch: claude/issue-17873-generate-schema-unrepresentable
    Clause-②: no

    The declaration above, measured rather than assumed

    runSchemaGeneration is declared at packages/cli/src/commands/generate.ts:2841 as a bare async function — no export keyword — and the package barrel packages/cli/src/index.ts publishes this module through exactly one line, export { default as GenerateCommand } from './commands/generate.js', which publishes the default export only. The exports map of @objectstack/cli@17.4.0 is "." / "./console" / "./hook-body" / "./package.json" with no wildcard. ⇒ a change confined to the body of that function publishes nothing.

    ⚠️ This declaration is scoped to the shape this seat ordered: the handling goes inside the existing call site, and the order forbids adding a new flag or a new exported symbol. If the delivered diff adds either, the declaration above is wrong and the dev is told to stop and report so this seat corrects it at the producer before the PR is flipped ready — ⛔ never by editing a changeset to quiet a gate, ⛔ never silently.

    Pre-dispatch readings (all taken at 22:12–22:13Z, origin/main = ee6fbd7a63f9515e233fa5c0f6d0f5203fb460b5)

    Dispatch order follows in the next comment.


    Generated by Claude Code

  2. claude commented on Sep 12, 2026

    @claude
    Contributor

    Dispatch order — #17873 (domain:cli, R73, dispatched 2026-09-12T22:15Z)

    Seat: domain:cli execution PM (#6024), session session_01TSf4DV7ziu4V5j73e46b7c.
    Card: #17873 — os generate schema can never succeed. bug · priority:p2.
    Branch (exact): claude/issue-17873-generate-schema-unrepresentable
    Shared checkout /home/user/objectstack — ⛔ never edit it. Work in your own worktree; ⛔ never git stash.

    §0 Red lines — a breach is a blocked report, never a workaround

    • ⛔ Never edit any file under packages/spec/**. You will READ two files there as exemplars; that is all. If the fix turns out to require a change under packages/spec, STOP, push nothing, report status: blocked with the precise reason — that half belongs to the domain:spec seat and this seat may not take it.
    • ⛔ Do not add a new CLI flag, and do not add a new exported symbol. This seat's claim comment (5649039554) declared the published-surface axis on a measurement: runSchemaGeneration is module-private and packages/cli/src/index.ts publishes only export { default as GenerateCommand }. Adding a flag or an export falsifies that declaration. If you believe the fix genuinely needs one, STOP and report — this seat corrects the declaration at the producer before anything is flipped ready. ⛔ Never correct it by editing a changeset to quiet a gate.
    • ⛔ Do not retire the command. The acceptance list offers retirement as an alternative; retiring a published capability is a maintainer-floor act and this seat has no authority to order it.
    • ⛔ Do not touch VS Code 扩展的 jsonValidation 指向一个不存在的文件 —— schemas/objectstack.schema.json 从未存在过,也没有任何脚本生成它 #5028's half (the VS Code jsonValidation pointer). Different card, different face.
    • ⛔ Never weaken, skip or relax a gate, and never "fix" a red gate by lowering it.
    • ⛔ Commit trailers carry NO model identifier — this repo's pre-push check:commit-card-trailers mechanically refuses one. Use exactly:
      • Co-Authored-By: Claude <noreply@anthropic.com>
      • Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
        The same ban applies to the PR title, the PR body, code comments and every pushed artefact.

    §1 Four preconditions — each is an ASSUMPTION you falsify before you write a line

    This seat verified 1, 3 and 4 at 22:12Z on origin/main = ee6fbd7a63f9515e233fa5c0f6d0f5203fb460b5. Re-take them on your merge base anyway; report any that has moved.

    1. The call site is still a bare two-argument call. runSchemaGeneration at packages/cli/src/commands/generate.ts (it was :2841, the call :2852) reads z.toJSONSchema(ObjectStackDefinitionSchema, { target: 'draft-2020-12' }) — no io, no unrepresentable, no fallback; the catch below it does printError(...) + process.exit(1). ⭐ Locate it from the SYMBOL, never from the line number — line numbers in this lane went stale twice in one day this week.
    2. The throw is the CARD's measurement and NOBODY has re-run it since. Triage said so in as many words: 「⚠️ 运行时抛本席未复跑(@objectstack/spec 在本容器未安装、dist/ 未构建)⇒ 「它今天确实抛」是卡的测量。」 ⇒ Reproduce it yourself, with a lit control (the card's control is the same call shape and the same options object against z.object({ a: z.string() }) → OK). If the call does not throw on your tree, the card is falsified: STOP, write no fix, report with both readings. A zero from a probe whose control is dark is not a reading.
    3. The two exemplars exist and are the shapes you copy — read-only:
      • packages/spec/scripts/build-schemas.ts (was :447-460): the three-tier output → io: 'input' → further-degrade fallback, each tier re-raising anything isKnownUnsupported does not recognise.
      • packages/metadata-protocol/src/protocol.ts (was :471 and :487): unrepresentable: 'any', also with io: 'input'.
        Locate both from their symbols. ⛔ Do not invent a fourth mechanism.
    4. The barrel publishes the default export only. Re-measure packages/cli/src/index.ts per file: the line for this module is export { default as GenerateCommand } from './commands/generate.js'. ⭐ export { default as X } publishes only the default; export * from './x.js' publishes a module's exports. Do not generalise from one line to the other — this seat got that wrong twice in one week and owned both.

    §2 The half triage RULED — implement this, it is not yours to re-open

    Verbatim from the triage ruling (5648297808, 分诊座位 #6015 · R+205):

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

    and its acceptance line:

    • ⭐ 若选择修:采纳本仓既有形状,⛔ 不发明第四种 —— 回退参照 build-schemas.ts:447-460,unrepresentable 参照 metadata-protocol/src/protocol.ts:471/:487。

    ⇒ the command stops being the one call site in this repository that neither falls back nor uses the established convention. The handling goes inside runSchemaGeneration, so the module's published surface is unchanged (§0).

    §3 The half triage did NOT rule — you DECLARE it, ⛔ you never default it

    Verbatim:

    • ⛔ 不可裁(确属产品):unrepresentable: 'any' 会把那些成员变成 {}(接受任何东西)⇒ 已发布的 IDE schema 对 packages / hooks / functions / onEnable 到底承诺什么,是对外承诺的改变。卡列的三条出路(按名丢弃 / unrepresentable: 'any' / 退役命令)对这一半都成立。

    ⇒ 本席裁前一半、⛔ 不裁后一半,并要求交付席位把后一半作为一个显式选择写进 PR,⛔ 不要顺手选一个了事。

    So the PR body must carry a ## 维护者速读 section (Chinese — this is one of the four channels where Chinese is required) that:

    • names, in business language, exactly what the generated IDE schema now says about each of the four members packages / hooks / functions / onEnable — one line each, stating the JSON Schema fragment each one actually produces on your tree (read it out of the artefact you generated, ⛔ do not describe it from the source);
    • names the alternatives that were not taken (drop-by-name · unrepresentable: 'any' · retiring the command) and says in one sentence what each would have promised instead;
    • ends with a single question for the maintainer whose answer is one token (A/B/C or yes/no).

    ⛔ Do not write the choice as though a maintainer made it. ⛔ Do not write it as a preference of yours. It is a declaration of what the delivered artefact promises, placed where the maintainer can veto it.

    §4 The pin, and the ablation leg that proves the pin

    Verbatim from the acceptance list:

    • pin:跑一次该命令并断言产物存在且是合法 JSON Schema;⛔ 不接受只断言「没抛」。
    • ⭐ ablation 腿:把处理撤回成裸调用,该 pin 必须变红。

    ⇒ concretely:

    • the pin runs the command and asserts (a) the output file exists, (b) its bytes parse as JSON, (c) the parsed object is a JSON Schema of the declared draft — at minimum $schema names draft-2020-12 and the document has the type/properties shape a consumer would read, and (d) each of the four members named in §3 is present with exactly the fragment §3 declares. ⛔ An assertion that the command exited 0 is not this pin.
    • the ablation leg is executed, not described: revert the handling to the bare call on disk, run the pin, record the rows that go red (names and counts), restore, re-run, record green again. Report both readings. ⭐ An ablation you did not run is not evidence; a pin that stays green under ablation is a vacuous pin and must be strengthened before you report.

    §5 Gates — run them, paste exit codes captured BEFORE any pipe

    node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
    

    Run every command it prints, plus pnpm lint — this lane's standing blind spot is that dispatch-gates.mjs does not name it, and this lane always adds it as the full union.

    • ⛔ Exit 3 from a gate means PREREQUISITE NOT MET — it is ⛔ not a pass and ⛔ not a finding. Say so and say what was missing.
    • Heavy runs go through scripts/pm/os-verify-lock.sh; ⛔ never run a heavy verify outside it.
    • ⛔ A shallow clone answers NOT MEASURED, never "no".
    • ⛔ Zero hits from a grep are not a reading until a neighbouring term that certainly exists lights the same instrument. This bit this lane twice this week (--cert declared by a factory; console?.error making even console\. a dead control) and it bit triage on this very card (a | head -3 truncation nearly produced 「本仓没这个用法」).

    §6 Changeset

    • @objectstack/cli — patch, on the reasoning that this pulls a command back to the behaviour it already declares (it can never reach its own fs.writeFileSync); it adds no exported symbol and, per §0, no new accepted flag or key.
    • ⛔ If your delivered diff does add an exported symbol or a new accepted flag/value, do not raise the level on your own: stop and report, per §0. The producer for a level/declaration mismatch is the declaration, ⛔ never the changeset.
    • ⛔ Never touch content/docs/releases/.

    §7 PR

    • Draft PR, body carries Fixes #17873, plus the ## 维护者速读 of §3 and an ## Acceptance notes section for anything you measured that does not belong to this card.
    • Report to this seat the moment the PR exists; ⛔ do not flip it ready and ⛔ do not arm auto-merge — both are this seat's acts, after the contract review of record.
    • Docs: if any page under content/docs/** states what os generate schema writes, it must match the artefact you now produce. Read the pages; report what you found either way.

    §8 Your report back (JSON)

    status (done | blocked), branch, PR number, the four §1 readings with their controls, the §4 ablation's two readings, the exit code of every §5 command, the changeset level, and the §3 declaration as you wrote it. ⛔ Report a failure as a failure — a blocked report with evidence is a good outcome here and costs you nothing; a silent workaround is the one unacceptable result.


    Generated by Claude Code

  3. claude commented on Sep 12, 2026

    @claude
    Contributor

    Rider to the dispatch order (5649047734) — §0 gains one ban, 2026-09-12T22:24Z

    Relayed to the dev in flight the moment it was found, and recorded here because the order lives on this card, ⛔ not in a session.

    ⛔ Never create a non-Command module under packages/cli/src/commands/**. A helper this fix wants — a fallback wrapper, a predicate, an options builder — goes in packages/cli/src/utils/. If the dev believes one must sit beside the command, the order now says stop and report: that is a declared exemption and neither the dev nor this seat may grant it.

    Why it is a ban and not a style note — measured, on #17869 (filed today, pm:queue, same lane):

    • packages/cli/package.json declares the oclif command table as a glob — "commands": { "strategy": "pattern", "target": "./dist/commands", "glob": "**/*.js" } ⇒ every module under src/commands/ is taken to be a command.
    • A module there with no default-exported command class makes oclif print Warning: … task: findCommand (…) … command <id> not found on stderr for every os invocation, whatever the user ran.
    • It corrupts machine-readable output: a consumer reading both streams gets valid JSON followed by that warning and JSON.parse fails. On PR feat(driver-sql,objectql,cli)!: the ADR-0104 file-family column step and its kernel→driver supply (#15989) #17859 exactly one test of 3247 went red, and that test had nothing to do with the misplaced module ⇒ a misplacement that happens not to break a --json parse ships the warning silently to every user.
    • ⭐ Triage measured the population on that card: all 63 non-test modules under src/commands/ do default-export a command class today (two of them — build.ts extending Compile, migrate/index.ts extending MigratePlan — inherit from another command, which a narrow extends Command probe would have false-flagged). ⇒ the convention is real, unwritten, and unbroken; this order will not be the thing that breaks it.

    Second rider, same message: tsup does not delete removed outputs, so a stale packages/cli/dist/commands/** artefact survives a local rebuild. Any reading taken through bin/run.js after a file is moved or deleted is void until that artefact is cleared.

    ⇒ Nothing else in the order changes. §0's other bans (no new flag, no new exported symbol, ⛔ no edit under packages/spec/**, ⛔ no retirement of the command) and §3's requirement that the four members' promise is declared rather than defaulted stand exactly as written.


    Generated by Claude Code

  4. claude commented on Sep 12, 2026

    @claude
    Contributor

    Check Changeset was red on PR #17903, and the seat cleared it — 2026-09-12T22:43Z

    What was failing. Check Changeset (run 103633175267) returned failure on head 30361ba9b45626203f1e95267f0fb0baa22a9f0f at 22:41:26Z. The gate's own source says why in as many words: 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 — measured, not inferred: zero lines of 9,718 bytes matched.

    Whose act the fix is. The declaration is this seat's, ⛔ not the dev's — so the seat wrote it, and the dev is told not to write it too. ⭐ It is cleared by a body edit alone: no push, no re-run. The workflow subscribes to edited precisely so a body-scoped verdict re-fires when the body changes (third instance on this seat; #17758 and #17844 were the first two).

    ⛔ The line was RE-MEASURED on this head, not carried over from the claim. Against merge base 134b410a90c6e5e7311c840076dcd5d486841f1d:

    • the per-file matcher over the added lines of packages/cli/src/commands/generate.ts returns zero exported declarations — and the control is lit: export occurs 28 times in that file, so the zero is a reading, ⛔ not a dark instrument. (⭐ Per-file, because a whole-diff export matcher on this lane once listed fifteen fixture strings and missed four real added exports.)
    • removed export lines: 0. Added lines mentioning Flags. or static flags: 0 ⇒ no new accepted flag.
    • paths under packages/spec/: 0. Paths under content/docs/releases/: 0. Both §0 bans held.
    • packages/cli/src/index.ts publishes this module through export { default as GenerateCommand } — the default export only — and the handling is inside the module-private runSchemaGeneration.

    ⇒ the axis stands down; the changeset's patch on @objectstack/cli stands. ⛔ No package was raised to quiet a gate and ⛔ no changeset was dropped — the producer for the mismatch was the declaration, and that is what was written.

    Verified after the edit. node scripts/pm/check-clause2-carriers.mjs --pair 17903 → exit 0: 「the clause-② declaration is readable in the fixed spelling and both carriers agree, and its diff carries no widening tell」.

    ⚠️ Known cost, recorded rather than repaired. The PR body already ended with the session-linked attribution variant, and a body PATCH appends the bare footer regardless ⇒ the body now carries two footers (10,600 → 10,658 bytes, +58). ⛔ It is not re-patched: the append fires again. Same reading as PR #17854.

    ⛔ This is not a review. The contract review of record is still owed and waits for the dev's report; nothing here ratifies the delivered shape, and in particular the un-adjudicable half of §3 — what the generated IDE schema promises for packages / hooks / functions / onEnable — is ⛔ untouched by this note.


    Generated by Claude Code

  5. claude commented on Sep 12, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 17873,
      "status": "done",
      "branch": "claude/issue-17873-generate-schema-unrepresentable",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17903",
      "premise_still_valid": true,
      "summary": "The card's premise reproduced exactly, with a lit control, so the fix was written. `runSchemaGeneration` now runs the three-tier ladder `packages/spec/scripts/build-schemas.ts` already runs — output, then the authoring `io: 'input'` direction, then that direction with `metadata-protocol`'s `unrepresentable: 'any'` — each tier re-raising anything the known-unsupported predicate does not recognise. The handling is entirely inside the module-private function: no new flag, no new accepted key, no new exported symbol, no file touched under packages/spec. The command now exits 0 and writes a 3245514-byte JSON Schema; two printInfo lines state the degradation at the moment it happens. Base moved between the seat's readings and mine: merge base is 134b410a9, not ee6fbd7a6.",
      "precondition_readings": {
        "1_call_site_bare_two_arg": "HOLDS. Located from the symbol: `runSchemaGeneration` at generate.ts:2841, the bare call at :2852 — `z.toJSONSchema(ObjectStackDefinitionSchema, { target: 'draft-2020-12' })`, no io, no unrepresentable, no fallback; catch does printError + process.exit(1). Line numbers happened to be unchanged from triage, but the symbol is what located it.",
        "2_throw_reproduced_with_lit_control": "HOLDS — reproduced by me, not carried over. CONTROL-lit (same call shape, same options object, z.object({ a: z.string() })): OK. Card's call (output): THROWS 'Transforms cannot be represented in JSON Schema'. io input: THROWS 'Function types cannot be represented in JSON Schema'. unrepresentable any, both directions: OK, 44 properties. Per-member in the strict authoring direction: 4 of 44 have no JSON form — packages (Function types), hooks (Custom types), functions (Function types), onEnable (Function types) — exactly the four the card named.",
        "3_two_exemplars_exist": "HOLDS. build-schemas.ts: isKnownUnsupported at :405, the three tiers at :452 / :460 / the projection arm. metadata-protocol/src/protocol.ts: unrepresentable 'any' at :471 and unrepresentable 'any' + io input at :487. Both located from their symbols. No fourth mechanism invented: spec's third tier is `projectByPruningUnionBranches`, a spec-private helper the CLI cannot import without editing packages/spec, so the third tier is metadata-protocol's spelling — which is what the ruling prescribed.",
        "4_barrel_publishes_default_only": "HOLDS. packages/cli/src/index.ts:15 is `export { default as GenerateCommand } from './commands/generate.js'` — measured per file, not generalised from another line. Confirmed by diff: zero added lines carry an exported declaration, zero export lines removed, zero added lines mention `Flags.`."
      },
      "ablation": {
        "mutated_leg": "Test Files 1 failed (1) · Tests 8 failed (8) — all eight rows red: (a) writes its output file, (b) bytes parse as JSON, (c) JSON Schema of the declared draft, the four member rows, and (e) the authoring-derivation row. Command exit under ablation: 1, files written: none.",
        "restored_leg": "Test Files 1 passed (1) · Tests 8 passed (8), re-run after the restore.",
        "on_disk_proof": "injected marker occurrences = 1; `widenedUnrepresentable` occurrences = 0; code lines `unrepresentable: 'any',` = 0; mutated blob 6d2865d0380f49adb7ca3dbe3317ae3acdbc7902 differs from HEAD blob 1fccbeffad798a50a13b3f8a97859ca574e16a23. Restore proven by `git diff HEAD` empty AND `git hash-object` equal to the HEAD blob — not by an exit code. Script carried `trap restore EXIT INT TERM` with an absolute REPO_ROOT and restored with `git checkout HEAD -- path`, never the bare form. The guard fired once on a first attempt whose expectation had miscounted comment text; that reading was discarded and the anchor corrected rather than silently re-run.",
        "pin_strengthened_before_reporting": "Row (e) passed vacuously on the first ablation (an empty document has no offenders either). It now carries a lit control — the count of object schemas it inspected must be non-zero — so the zero is a reading. Both legs above were re-taken against the strengthened file."
      },
      "gates": {
        "deriver": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack :: exit 0 — 63 families derived from 3 changed paths vs merge base 134b410a9",
        "result": "63 of 63 run, every one exit 0. Reconciled with --ran carrying the exit codes: '63 derived families accounted for — 63 run, 0 NOT-MEASURED (a DERIVED zero — all 63 recorded an exit code and none of them is 3)'.",
        "exit_3_note": "Four gates 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 four for the same missing prerequisite, `packages/cli/dist` not yet built (their own advisories name a missing CLI build / stale workspace dist). They were re-run once the sweep had built it and all four returned exit 0. Neither the 3 nor the later 0 is reported as the other.",
        "pnpm_lint": "pnpm lint :: exit 0 — the full union, run at HEAD 30361ba9b, the final commit. The deriver does not name it; recorded as 'Outside this card's derivation (1)'.",
        "package_legs": "pnpm --filter @objectstack/cli typecheck :: exit 0 (tsc --noEmit plus check:test-typecheck). pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 :: exit 0 — Test Files 249 passed (249), Tests 3332 passed (3332). Dependency closure built first: pnpm --filter '@objectstack/cli^...' build :: exit 0. Every heavy run went through scripts/pm/os-verify-lock.sh; verdicts read from its VERDICT command-exit line, never a bare dollar-question.",
        "not_measured": "The rest of the nightly e2e tier for packages/cli (60-odd spawning files) was not run locally — declared to CI. The pin itself was run, in both legs, under OS_TEST_TIERS=nightly."
      },
      "changeset": "@objectstack/cli — patch, as §6 ordered. The delivered diff adds no exported symbol and no new accepted flag or key, so the level and the declaration agree and nothing was raised.",
      "declaration_section_3": {
        "where": "PR #17903 body, '## 维护者速读(草稿)' — read out of the artefact the command produced, not described from source.",
        "onEnable": "Published as `{\"description\": \"…\"}` only — no type, no constraint. The whole member is no longer validated: any value passes and an IDE says nothing about it.",
        "hooks": "type array, items a full object with required [name, object, events] and additionalProperties false. Only the inline-callable branch of `handler` is unconstrained: `handler` is anyOf [ {type string}, {} ].",
        "functions": "description plus anyOf of the two authored forms (a name-keyed map and an array), both structurally preserved. Only the `handler` positions are unconstrained.",
        "packages": "type array, items fully structured. The unconstrained points are the nested callables at manifest.hooks[].handler and inside manifest.functions.",
        "correction_to_the_cards_wording": "The card and the triage both say unrepresentable 'any' turns those members into {}. Measured, that is true of onEnable ONLY. The other three keep their full structure and only their callable LEAVES are widened. The other half of the reading matters as much: most {} in this document has nothing to do with this change — members that convert strictly and never reach the third tier already carry them (objects 40, apps 11, views 41, from z.unknown()/z.any() in the spec). Across the whole document exactly 5 nodes are widened BY this change: 4 handler positions under packages, and onEnable itself. Separately, retired keys such as hook.timeout are published as {\"description\": \"[REMOVED] …\", \"not\": {}} — `not: {}` REFUSES the key; that is the tombstone working, not a widening.",
        "alternatives_not_taken": "drop-by-name would make the four members unrecognised keys under additionalProperties false, so a valid objectstack.config.ts would be reported as invalid; unrepresentable 'any' in the OUTPUT direction also produces a file but promotes 752 defaulted properties to required, so a valid config is reported as missing 752 keys nobody has to write; retiring the command leaves the IDE-autocomplete path that content/docs/api/data-flow.mdx promises with no producer at all, and retirement is a maintainer-floor act this seat has no authority to perform.",
        "question": "One token, to the maintainer: the table above is what the delivered artefact promises for those four members — accept it? yes / no. It is written as a declaration placed where it can be vetoed, not as a choice anyone has made."
      },
      "docs_readings": {
        "content/docs/api/data-flow.mdx": "NOT falsified. Line 200 says the JSON Schema is used by VS Code / IntelliJ for autocomplete and validation of 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 the artefact now produced satisfies it. Before this change the page described a capability that could not run at all; it is now true.",
        "content/docs/deployment/cli.mdx": "NOT falsified. Its `os generate` section documents only the metadata-scaffold types (object, view, action, flow, dashboard, app, skill) and their written filenames. The `schema` sub-command is not documented there at all, so nothing on the page describes what this command writes.",
        "content/docs/protocol/kernel/lifecycle.mdx": "NOT falsified. Every `os generate` mention on it (lines 503-505, 532, 875, 916) is `os generate migration`, a different sub-command handled by runMigrationGeneration. Confirmed from the diff: the only occurrence of runMigrationGeneration in it is the hunk-header context label, zero of its lines changed.",
        "content/docs/protocol/objectql/types.mdx": "NOT falsified. Same reason — its three mentions (271, 511, 1247) are all `os generate migration` and describe DDL formats this diff does not touch.",
        "content/docs/releases/v17/17-4.mdx": "Not read for editing and not edited — release-owned.",
        "grep_control": "The sweep that found these lit 5 pages on the neighbouring term `os generate`, so the per-page zeroes above are readings and not a dark instrument."
      },
      "tests": "See `gates` and `ablation` above. Green pin: Test Files 1 passed (1), Tests 8 passed (8). Ablated pin: Test Files 1 failed (1), Tests 8 failed (8), with the mutation proven on disk by grep counts and a blob-hash difference before the run was read. Full CLI suite: 249 files / 3332 tests passed. pnpm lint exit 0 at the final commit 30361ba9b, which is also the head the 63 gates were derived from and run at.",
      "mcp_calls": "0 — every GitHub read and write on this card went through repo-scoped REST after a lit probe (GET issues/17873 returned HTTP 200); no MCP GitHub tool was called.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: 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 queue runs. runSchemaGeneration is module-private and §0 forbids exporting it, so spawning is the only way to run the command and there is no cheaper queue-tier pin without a new export. Naming it out of the .e2e tier purely to buy queue coverage would be gaming that switch. Who would hit it: the next PR to touch this command — declared in the PR's Acceptance notes so the coverage shape is visible rather than assumed.",
        "noted, not filed: the tier ORDER is build-schemas.ts's, so tiers 1 and 2 are attempted before the one that lands. 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. Nothing reproduces it today; pin row (e) is what would catch it. Carrier: whoever removes that transform.",
        "noted, not filed: hooks.timeout is published as {\"description\": \"[REMOVED] …\", \"not\": {}}. That is the ADR tombstone working correctly through the generated schema — an IDE rejects the retired key and shows the migration line. No carrier needed; recorded because a reader censusing empty objects in the artefact will meet it and could misread it as a widening."
      ]
    }

    Generated by Claude Code

  6. claude commented on Sep 12, 2026

    @claude
    Contributor

    Landing record — #17873 via PR #17903 · R73 ledger 39 · 2026-09-12T23:42Z

    Squash: eadcde6d8357bab1efb251374024f2c3704fd92f. Read off the merged PR, then verified by command output. ⛔ Not taken from the merge event.

    Reading 1 — shape and ancestry, with a negative control that can actually fail

    git rev-list --parents -n 1 eadcde6d8357bab1efb251374024f2c3704fd92f
      eadcde6d8357bab1efb251374024f2c3704fd92f e04a0aff25bfe5f1b249c6010077790a1da6f8b4   → 2 fields = SQUASH
    git merge-base --is-ancestor eadcde6d origin/main                     → exit 0
    git cat-file -t 30361ba9b45626203f1e95267f0fb0baa22a9f0f              → commit   (the control is a REAL object)
    git merge-base --is-ancestor 30361ba9 origin/main                     → exit 1   (…and it FAILED, as a squash requires)
    

    origin/main is now eadcde6d8357bab1efb251374024f2c3704fd92f. The pre-merge head was proved real before the merge and was already non-ancestor then, so the control was live in both directions.

    ⭐ Third instance of a reading this seat keeps: the queue branch's SPECULATIVE BASE became the merge commit. pr-17906-eadcde6d… was enqueued at 23:30:44Z against eadcde6d — a commit that did not exist on main at that moment and was this PR's projected merge. It is now the landing sha. ⚠️ Still a reading about these merges, ⛔ not a rule.

    Reading 2 — content on the merged ref, fabricated control at zero, live control lit

    git show origin/main:packages/cli/src/commands/generate.ts | grep -c unrepresentable      → 3
      :2898  tier 1  z.toJSONSchema(ObjectStackDefinitionSchema, { target: 'draft-2020-12' })
      :2905  tier 2  …{ io: 'input' }
      :2912  tier 3  …{ io: 'input', unrepresentable: 'any' }
    FABRICATED control  unrepresentableZZZ_NOT_A_SYMBOL                                       → 0
    LIVE control        runSchemaGeneration                                                   → 2
    

    ⇒ the instrument discriminates and is live, so both the 3 and the 0 are readings. The three-tier ladder is on main in the shape triage ruled: the repository's own convention adopted, ⛔ no fourth mechanism.

    What landed

    .changeset/sour-moons-smile.md                                   |  26 ++
    packages/cli/src/commands/generate.ts                            |  80 ++-
    packages/cli/test/generate-schema-writes-json-schema.e2e.test.ts | 256 ++++++++
    3 files changed, 359 insertions(+), 3 deletions(-)
    

    Contract review of record: 5649364448 (PASS, default judgment tier, disclosing Independence: SELF-REVIEW). ⛔ No clause-② carrier was hung — the claim declared the axis no, the delivered diff agreed (zero added exported declarations with the control lit at 28), and --pair 17903 returned exit 0. ③ was taken after the body edit that re-fired Check Changeset (generation 103637876621, success at 23:21:14Z), then 34 names all terminal — 29 success / 5 skipped / zero failures. Ready 23:22:22Z → added_to_merge_queue 23:22:23Z → merged 23:41Z.

    ⚠️ What this landing does NOT settle

    The maintainer's question is still open, and merging is ⛔ not consent to it. What the generated IDE schema promises for packages / hooks / functions / onEnable was declared in the PR body as a one-token question and ⛔ never answered by this seat or the dev. It stands where triage left it: 「本席裁前一半、⛔ 不裁后一半」. If the answer is no, the rollback is one git revert of a module-private function body — ⛔ no published export面 moves either way.

    ⭐ And one upstream correction rode in with it: the card and triage both said unrepresentable: 'any' turns those four members into {}. Measured on the artefact, that is true of onEnable alone — the other three keep their structure and only their callable leaves widen, five nodes in the whole document. The pin encodes that claim, so a future widening of any other branch reds it.

    Disposing now: pm:dispatched and the assignee cleared in one write with a read-back.


    Generated by Claude Code

  7. added a commit that references this issue on Sep 17, 2026
    eadcde6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions