Skip to content

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-bill

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, record 5651572469) and withdrew batch #149 item 1 letter B (record 5716259259). ⛔ 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 no os-decision-facets marker in either spelling. ⛔ Nothing already on the face was removed or reworded. The full four-axis analysis, the options and their costs live in comment 5729976367; 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 rows GET /packages serves, toRecordManifest leaves a functions residual 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 outside packages/spec entirely, 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's main: packages/spec/json-schema/ is gitignored and was never version-controlled, while json-schema.manifest/api.json already 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/objectqlanother 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)

⚠️ 那 2 个 ADR 命中是噪声,不是先例。 两条都只命中 lowering 一词,而 ADR-0058 D1/D3 讲的是 CEL→FilterCondition 下推编译器 —— 与 artifact 阶段体无关。⛔ 不要把「2 hits」读成「有两条裁决适用」。

推荐:C。
自检:只看①选 C(record 阶段该有自己的声明,而那要在它自己的包里裁);②③④ 是否翻转:否 —— ② 加固它(欠报独立存在)、③ 加固它(拆开才不会再错位一轮)、④ 加固它(已就绪的不该陪跑)。
置信缺口:NOT MEASURED —— registry 铸的 ref 与 build 铸的 ref 在真实包上到底有多常不相等(uniqueName 撞名加后缀、按函数同一性并键,两条都只从源码读出,⛔ 没有在一个真实多函数包上量过)。选 A 就等于在这个未测量上下注。

⚠️ 另有 GAMMA(保留生产者行为,单给 record 阶段一个「callable 未存活」的声明)⛔ 未给独立字母:它实质上就是 C 的子卡要裁的内容,给字母会让同一问题在两处各裁一次。⛔ 而把 GAMMA 等同于原 letter C 是不准确的 —— 原 letter C 被否的理由是「为同一个设计增加 2–3 个永久导出」,与 record 阶段该不该有声明是两回事。


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].manifest could 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 schema FlowFunctionEntrySchema (automation/flow-function.zod.ts) has z.function() as its first union branch;
  • hooks, whose HookSchema (data/hook.zod.ts) has a z.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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions