Repository navigation
finding(spec): AssembledPackageBodySchema declares callable and custom branches an inert-JSON artifact cannot hold, and every schema embedding it loses its JSON Schema #17518
Description
os-decision-facets
⛔ This is an EXECUTION card stalled at its ruling's OWN step 4 — ⛔ not an unruled question
Ruling-ref: 5729478920— batch #159 item 2 · letter A, maintainer 「同意」. It confirmed the 2026-09-13 ruling (batch #127 item 4, record5651572469) and withdrew batch #149 item 1 letter B (record5716259259). ⛔ The direction is settled and is not re-presented here; what follows is the measurement ruling A's own step 4 asked for, and the shape question that measurement returned.
⚠️ Added by the seat when the half-state patrol row H62 measured this body as carrying noos-decision-facetsmarker in either spelling. ⛔ Nothing already on the face was removed or reworded. The full four-axis analysis, the options and their costs live in comment5729976367; this block is the card-face serialisation the shape requires.Steps 1, 2, 5 and 6 are MEASURED READY. An artifact-stage body built exactly as step 2 prescribes does convert under
z.toJSONSchema. Step 3 is blocked by step 4's own precondition, which fired: on the rowsGET /packagesserves,toRecordManifestleaves afunctionsresidual that is neither a string nor a lowered declaration.⭐ Two things the next reader must not mis-read. (1) The producer is
packages/objectql/src/registry.ts— it is outside the three-file surface ruling A itself names, and outsidepackages/specentirely, so the fix that ruling authorises does not fit the surface that ruling drew. That self-inconsistency is the seat's to have caught before dispatch, and it did not. (2) The card body's second consequence does not reproduce on today'smain:packages/spec/json-schema/is gitignored and was never version-controlled, whilejson-schema.manifest/api.jsonalready carries both response schemas — ⛔ do not read their presence as 「the work is done」, nor their absence from git as 「it is not」.
The question: what shape does the REGISTRY RECORD stage take?
⛔ Not a dev's choice: it changes the payload GET /packages emits, lands in @objectstack/objectql — another published package, with its own Clause-② — and toRecordManifest's docblock states it is a structural projection and ⛔ 「not a key denylist」, so any fix that special-cases a key name overturns a written design rule.
| option | cost, measured | |
|---|---|---|
| A | registry records the lowered form — a dropped callable becomes a handler ref | the only arm that literally satisfies step 4, and it fixes the under-report. ⛔ But it overturns the documented structural rule, and a ref the registry mints is not guaranteed equal to the one build mints (lower-callables.ts uses uniqueName(base, taken) and dedupes by function identity) ⇒ a record could assert a ref that resolves in no sibling module |
| B | a dropped callable discards the whole entry | step 3 becomes immediately doable. ⛔ But it generalises today's bare-entry behaviour to declarative entries — deliberately under-reporting at a read door — and throws away the effect declaration, the one half that survives today |
| C ⭐ | split: file the producer under-report as a sub-issue of this card and rule the record-stage shape there; land steps 1/2/5/6 now; step 3 waits on the sub-card | measured-ready work converts immediately and the undecided part is decided in its own package. ⛔ Cost: an exported artifact-stage body briefly has no consumer, which the PR body must declare as phase one |
| D | same sub-card, but this whole card goes pm:blocked behind it |
nothing lands now; the four ready steps wait on an unformed producer decision |
四棱
① 项目长远合理性 — 三个声明过的阶段(authoring / 内存装配 / 落盘 artifact)之外,registry record 是事实上的第四个阶段:callable 已经没了,却没有 ref 顶上。今天没有任何声明描述它,这才是残骸能存在的根因。⇒ 偏向让 record 阶段拿到自己的声明。
② 实际业务拉动 — ⭐ 欠报是独立于本卡的真缺陷:机器可读的读门把「这个包声明了几个函数」说少了,而且踩在我们自己发布的 showcase 上(config.ts:244-249 正好一条裸的、一条声明式的,两种残骸各一)。⛔ 它不因为本卡怎么裁而消失。
③ 防 AI 犯错 — ⛔ 别再让一次派发同时裁「schema 形状」和「生产者行为」。本卡已因「裁决措辞 ↔ 代码事实」错位空转两轮(B 撤回、A 卡在自己的步骤 4)。B 的失败模式最危险:它让读门静默少报而不响亮拒绝。
④ 创业阶段不扩散 — 步骤 1/2/5/6 已实测就绪。把已就绪的收益押在一个尚未成形的生产者决定后面,是用确定换不确定。
Prior rulings read: assembledpackagebody,inert-json,z.function,lowering → 2 hits; ADR-0058 D1, ADR-0058 D3; thread: 3 ruling(s) (5651572469, 5716259259, 5729478920)
lowering 一词,而 ADR-0058 D1/D3 讲的是 CEL→FilterCondition 下推编译器 —— 与 artifact 阶段体无关。⛔ 不要把「2 hits」读成「有两条裁决适用」。
推荐:C。
自检:只看①选 C(record 阶段该有自己的声明,而那要在它自己的包里裁);②③④ 是否翻转:否 —— ② 加固它(欠报独立存在)、③ 加固它(拆开才不会再错位一轮)、④ 加固它(已就绪的不该陪跑)。
置信缺口: ⛔ NOT MEASURED —— registry 铸的 ref 与 build 铸的 ref 在真实包上到底有多常不相等(uniqueName 撞名加后缀、按函数同一性并键,两条都只从源码读出,⛔ 没有在一个真实多函数包上量过)。选 A 就等于在这个未测量上下注。
Found while landing #17431. Filed rather than fixed: the remedy moves AssembledPackageBodySchema's own shape, which is #14242's ruling territory, and #17431 is bound to the read API one layer up.
The contract, verbatim
ADR-0130 D4's own docblock on ArtifactPackageEntrySchema (packages/spec/src/stack.zod.ts) states why plugins and devPlugins are envelope keys that no package body may carry:
An artifact is inert JSON: a plugin written inside
packages[i].manifestcould never be constructed by a loader, so a reader that resolved it there would register garbage where it used to skip in silence.
AssembledPackageBodySchema — the body half of ArtifactPackageSchema, i.e. one package as assembled into a release artifact — nevertheless carries two collections whose declarations admit values that same sentence says can never exist in an artifact:
functions, whose entry schemaFlowFunctionEntrySchema(automation/flow-function.zod.ts) hasz.function()as its first union branch;hooks, whoseHookSchema(data/hook.zod.ts) has az.custom()branch.
A callable in an artifact is exactly the case the quoted sentence excludes: the artifact is JSON on disk, so the branch describes a value the surface cannot hold.
Measured consequence
Of the assembled body's 55 shape members, exactly those two have no JSON Schema form. Measured with z.toJSONSchema(member, { io: 'input' }) over every key of AssembledPackageBodySchema.shape:
functions: Function types cannot be represented in JSON Schema
hooks: Custom types cannot be represented in JSON Schema
and, on the whole body:
| probe | result |
|---|---|
AssembledPackageBodySchema |
FAIL — Function types cannot be represented in JSON Schema |
.omit({ functions: true }) |
FAIL — Custom types cannot be represented in JSON Schema |
.omit({ hooks: true }) |
FAIL — Function types cannot be represented in JSON Schema |
.omit({ functions: true, hooks: true }) |
OK |
That is why ArtifactPackage and ObjectStackDefinition publish no JSON Schema at all, and it propagates: any published export that embeds the assembled body loses its own JSON Schema and its content/docs/references/** page with it. #17431 hit exactly that — binding the body into ListInstalledPackagesResponseSchema and GetInstalledPackageResponseSchema made both disappear from json-schema/api/, which build-schemas.ts's disappearance ratchet refuses.
So the cost is not local to the artifact schema. It is a standing tax on every future surface that wants to declare the assembled stage — and declaring the assembled stage is the ruled remedy for the #14242 class.
Why this is a contract violation and not a preference
The two branches are not merely unused here: the surface's own declared semantics say they are unreachable. A declaration that admits what its surface cannot hold is a claim nothing enforces, and here it has a measured price paid by unrelated schemas.
⛔ Not proposing the shape. Two obvious directions exist (narrow the two collections at the assembled body, or give artifact-stage variants of the two entry schemas) and both move a declaration #14242 ruled on, so the choice is the maintainer's.
Related: #14242 (the ruling that created AssembledPackageBodySchema) · #17431 (where this was measured) · #11072 (the sibling axis: the same tree also carries a Node-only import, which is a separate matter)
Generated by Claude Code
Activity