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/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)

⚠️ 那 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

  1. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    Triage: lands in packages/spec/src/stack.zod.ts ⇒ domain:spec; type Bug, priority:p2, pm:queue.

    Class (b), with the contract quoted from the repo's own ADR docblock. ADR-0130 D4 on ArtifactPackageEntrySchema states why plugins / devPlugins are envelope keys:

    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 nonetheless carries functions (whose FlowFunctionEntrySchema opens with a z.function() branch) and hooks (whose HookSchema carries a z.custom() branch). Both admit values that same sentence says can never exist in an artifact.

    ⭐ And the consequence is not theoretical: every schema embedding it loses its JSON Schema. That puts it in the same family as two other cards graded this shift — objectstack#17501 (/meta/types serves an EMPTY schema for action) and #17502 (tombstones offered as repeater columns) — all three being what the served schema says diverging from what the door accepts. ⛔ Different files, different fixes; ⛔ not folded. ⭐ But whoever takes any of the three should read the other two.

    Not blocked — measured 2026-09-10T19:26:48Z

    The card defers to 「#14242's ruling territory」. #14242 is closed ⇒ the ruling is settled, not pending. ⇒ Dispatchable, and the ruling is an input to read rather than a wait.

    ⚠️ #17431 is pm:dispatched with needs:contract-review and is where this was found. ⛔ Do not fold: that card is bound to the read API one layer up, this one moves AssembledPackageBodySchema's own shape. Folding gate ③ fails outright — #17431 is in flight.

    priority:p2: a published schema declares branches its own contract says cannot occur, and the derivation breaks for every embedder. Loud where it breaks, silent where it merely lies. Not p1: no data moves and nothing shipped rejects a valid artifact today.

    Triage seat · session_017VGfRocA8VjczSe84fgjY3 · R+171 · 2026-09-10T19:27Z (timestamp taken in the same tool call that posts) · comment from the triage seat


    Generated by Claude Code

  2. added theissue type on Sep 10, 2026
  3. self-assigned this
    on Sep 12, 2026
  4. os-bill commented on Sep 12, 2026

    @os-bill
    CollaboratorAuthor

    Claim: session_01MkQhmuuJAVDjmeWNixwDDH · branch claude/issue-17518-assembled-body-json-schema
    Branch: claude/issue-17518-assembled-body-json-schema
    Clause-②: yes — the remedy moves a PUBLISHED declaration. Narrowing functions / hooks at the assembled body (or introducing artifact-stage variants of FlowFunctionEntrySchema / HookSchema) changes what AssembledPackageBodySchema accepts, so the contract-review tier applies and needs:contract-review rides the draft PR from the moment it opens.

    domain:spec execution seat, PM dispatch, 2026-09-12T15:57Z. The assignee above was set by this seat in the same step as the pm:queue → pm:dispatched swap, read back and diffed (['bug','domain:spec','pm:dispatched','priority:p2'], assignees ['os-bill']). The dev round inherits BOTH and posts no second claim.

    Pre-measurement taken by this seat before the claim, read at 2026-09-12T15:55Z, not recalled:

    The card says in its own words 「⛔ Not proposing the shape … both move a declaration #14242 ruled on, so the choice is the maintainer's」, while triage graded it dispatchable on the ground that the ruling is settled. Both readings are on the card and the round is dispatched to settle which one the tree supports: if #14242's landed ruling plus ADR-0130 D4 mechanically determine the direction, implement it; if they leave a real fork, stop with an empty diff and a four-facet block rather than pick for the maintainer. A measured needs_decision is a successful outcome for this card, not a failure.


    Generated by Claude Code

  5. os-bill commented on Sep 12, 2026

    @os-bill
    CollaboratorAuthor

    os-decision-facets

    Dev round verdict: needs_decision, empty diff. The card's central premise is falsified by measurement, and what survives is a smaller question that is genuinely the maintainer's. Full readings with their controls are in the os-dev-report comment below; this comment carries the decision material only.

    What the tree says, quoted

    1. The two branches are NOT unreachable — they are live on the path this schema declares itself for.

    composeStacks(stacks, { manifest: 'preserve' }) is a published @objectstack/spec function. It builds packages[i].manifest through assemblePackageBody() (packages/spec/src/stack.zod.ts:3502-3509), which copies the authored stack's functions and hooks verbatim, with no lowering. Measured on origin/main at ed8dea17bd:

    packages[0].manifest.functions.scoreLead            typeof: function
    packages[0].manifest.functions.syncBilling.handler  typeof: function
    packages[0].manifest.hooks[0].handler               typeof: function
    ArtifactPackageSchema.safeParse(that body).success      = true
    AssembledPackageBodySchema.safeParse(that body).success  = true
    LIT CONTROL — same body with glob `objects`             = false, issue at objects.0
    LOWERED form of the same body                            = true
    

    And stack.zod.ts:3489-3491 states the invariant that makes that binding, verbatim:

    {@link AssembledPackageBodySchema} is its declaration and {@link ObjectStackDefinitionSchema}'s packages key parses against it, so a body this function builds and a body the load path accepts cannot drift.

    ⇒ This rules out the card's direction (a) — narrowing the two collections at the assembled body would make a published composition function's own output refused by the schema the same file says must accept it. The ADR-0130 D4 sentence the card quotes is the justification for excluding plugins / devPlugins from the body as envelope keys; it is not a statement that the assembled body is always JSON. The schema's own header says what it is: "One package as it is ASSEMBLED into a release artifact, and as the load path registers it" (stack.zod.ts:1101-1102).

    2. Two of the card's measured consequences are causally wrong.

    • ArtifactPackage and ObjectStackDefinition publish no JSON Schema because stack.zod.ts is not one of the 15 subpath namespaces build-schemas.ts walks — measured by replicating that walk: 1551 zod exports reached, and AssembledPackageBodySchema, ArtifactPackageSchema, ArtifactPackageEntrySchema, ObjectStackDefinitionSchema are not among them (lit control: HookSchema, FlowFunctionEntrySchema, AssembledInstalledPackageSchema, ListInstalledPackagesResponseSchema are). Removing the two branches would not make either file appear.
    • ObjectStackDefinitionSchema has four unrepresentable members — packages, hooks, functions, onEnable — of which only packages is the assembled body. Repairing the body would not make it emit.
    • Separately: HookSchema's z.custom() branch is already handled at the published-schema level by the landed [finding] Five filter operators ($gt/$gte/$lt/$lte/$between) reach NO published reference page — build-schemas.ts skips their whole schema over an unrepresentable z.date(), and the skip is silent #16431 (a) projection. Running the build's own predicate, Data.HookSchema EMITS today, projected, pruning #/properties/handler/anyOf/1. FlowFunctionEntrySchema stays unemitted because its declaration branch needs a callable in a property position, which is not droppable.

    3. What survives, and it is real. An exported schema that IS in the emit loop and embeds the assembled body cannot emit. That tax is paid exactly once today, in packages/spec/src/api/package-api.zod.ts, whose own docblock says of its z.unknown() override: "on THIS surface those two keys are accepted without being checked."

    The question

    The assembled package body deliberately spans two stages — the in-memory composed body, where callables are legitimate (measured above), and the on-disk artifact body, which is inert JSON carrying only lowered string refs (hook.zod.ts:249 "The JSON artifact therefore only ever contains the string form"; flow-function.zod.ts:223 "The CLI lowers every inline callable to a serialisable string ref BEFORE the stack is parsed"). Only the second is inert JSON.

    Should packages/spec gain a declared inert-JSON stage for it, or do the two keys stay undeclared at every JSON surface?

    option what it does real cost to a consumer
    A close as premise-falsified, change nothing record this reading on the card the one JSON surface that needs an inert-JSON body keeps functions / hooks as z.unknown(); the next surface re-derives this analysis or copies the override, and "write unknown on a JSON surface" becomes precedent
    B declare the inert-JSON stage export the lowered function entry (today a module-local const) plus an artifact-stage body whose hooks[].handler is z.string(); rebind package-api.zod.ts's record body to it 2-3 new permanently published exports; the read API's two keys move from unknown (accepts anything) to a real declaration, which narrows what those two responses accept at runtime. AssembledPackageBodySchema is untouched, so composeStacks is untouched
    C invert the names make AssembledPackageBodySchema the JSON-only declaration, give the live stage a new name a published export's meaning changes under every existing consumer with no compile-time signal; an ADR-0087 disposition becomes owed; composeStacks's own return type moves

    四棱

    ① 项目长远合理性:B 用一个已声明的阶段替掉读 API 上那处 unknown 空洞,缩小特例;C 扩大特例(把一个已发布声明的含义就地换掉);A 不动任何东西,但把「JSON 面就写 unknown」留成先例。
    ② 实际业务拉动:今天只有一处真正撞上——GET /packages 行的 functions / hooks 两个键不受检。卡面所称的 ArtifactPackage / ObjectStackDefinition 掉 JSON Schema 与掉参考页,实测不是这两个分支造成的,所以真实拉动远小于卡面。
    ③ 防 AI 犯错:A 让下一个作者照抄 unknown 覆写,失败方向是静默容忍;B 让写错的 lowered 形状落在具名拒收里,响亮;C 最危险——同名换义,消费者编译通过而语义已变,没有任何提示。
    ④ 创业阶段不扩散:A 零新增永久义务;B 新增 2–3 个永久发布导出;C 既新增又搬迁一个已发布名字。按本轴 A 优于 B 优于 C。

    推荐:B(回退:A)。 ⛔ 拒绝 C:它把一个已落地的已发布声明的含义在消费者脚下换掉,而这正是本仓库所有仪器都抓不到的那一类失败。

    置信缺口(本分析看不见什么): 我没有对着生产行数据实测 GET /packages 这条 wire 上真实流过的 functions / hooks 值形状——只对着 toRecordManifest 的 JSON 投影推断。若真实行里存在既非 string 也非 lowered-record 的残留,B 会把今天能读的行变成拒收;A 不会。

    The four axes above are this repository's own standing set (.claude/skills/pm-dispatch/references/decision-analysis.md), written in Chinese as that reference prescribes; the dispatch word carried no decision frame, so no axes were invented here.

    ⛔ No PR was opened: the round's finding is that neither of the card's two proposed directions should be implemented as filed, and an empty diff is the honest carrier for that. Branch claude/issue-17518-assembled-body-json-schema is pushed and carries no commits.


    Generated by Claude Code

  6. os-bill commented on Sep 12, 2026

    @os-bill
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 17518,
      "status": "needs_decision",
      "branch": "claude/issue-17518-assembled-body-json-schema",
      "pr": null,
      "premise_still_valid": false,
      "summary": "Empty diff, by measurement. The card's DEFECT premise is falsified: the two branches are not unreachable, they are LIVE on the path the schema declares itself for. composeStacks(stacks, { manifest: 'preserve' }) -- a published @objectstack/spec function -- builds packages[i].manifest via assemblePackageBody(), which copies the authored stack's `functions` and `hooks` VERBATIM with no lowering; measured, packages[0].manifest.functions.scoreLead is typeof 'function', functions.syncBilling.handler is typeof 'function', hooks[0].handler is typeof 'function', and BOTH ArtifactPackageSchema.safeParse and AssembledPackageBodySchema.safeParse accept that body (lit control on the same body: globs at `objects` are refused at objects.0; lowered form also accepted). stack.zod.ts:3489-3491 states the invariant that makes this binding: 'AssembledPackageBodySchema is its declaration and ObjectStackDefinitionSchema's packages key parses against it, so a body this function builds and a body the load path accepts cannot drift.' So the card's direction (a) -- narrow the two collections at the assembled body -- would make a published composition function's own output refused by the schema the same file says must accept it. Two of the card's measured consequences are also causally wrong: ArtifactPackage / ObjectStackDefinition publish no JSON Schema because stack.zod.ts is NOT one of the 15 subpath namespaces build-schemas.ts walks (measured: 1551 zod exports reached, these 4 names not reached), not because of the two branches; and ObjectStackDefinitionSchema has FOUR unrepresentable members (packages, hooks, functions, onEnable), only one of which is the assembled body, so repairing the body would not make it emit. What survives is real but smaller: an exported schema that IS in the emit loop and embeds the assembled body cannot emit, and that tax is currently paid exactly once, in packages/spec/src/api/package-api.zod.ts, with an `unknown` override its own docblock calls 'accepted without being checked'. Whether packages/spec should gain a declared INERT-JSON stage is a real fork with a real consumer cost, so it stops here with the four-facet block and one question rather than a picked side.",
      "tests": "NO CODE CHANGED -- the diff is empty, so no package is touched and neither the affected-package build closure nor the per-package test/typecheck is owed; the narrowing is declared here rather than silently taken. Run anyway as a baseline and as a control that the probe environment is sane, through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-17518, verdict read from the wrapper's own 'VERDICT command-exit' line: (1) pnpm --filter @objectstack/spec exec vitest run src/assembled-package-body.test.ts src/api/package-api.test.ts src/kernel/manifest-unknown-keys.test.ts --maxWorkers=2 -- VERDICT command-exit 0, 3 files / 84 tests passed. (2) pnpm --filter @objectstack/spec test:repo -- VERDICT command-exit 0, 30 files / 520 tests passed (held the lock 498s on a SHARED box, not idle-box seconds). NO ABLATION IS OWED OR PRESENTED: an ablation proves a fix can fail, and there is no fix; every reading below is instead a direct probe carrying its own lit and dark control. PROBES (all run with OS_EAGER_SCHEMAS=1 via tsx against src on head ed8dea17bd): P1 z.toJSONSchema(member,{io:'input'}) over all 55 keys of AssembledPackageBodySchema.shape -> EXACTLY 2 fail, functions ('Function types cannot be represented in JSON Schema') and hooks ('Custom types cannot be represented in JSON Schema'); whole body FAILS; .omit({functions}) FAILS on custom; .omit({hooks}) FAILS on function; .omit(both) OK; lit control z.string() OK, dark control z.function() FAILS. So the card's own table reproduces exactly on my head. P2 replicating build-schemas.ts's namespace walk over its 15 subpath imports: 1551 zod exports reached; AssembledPackageBodySchema, ArtifactPackageSchema, ArtifactPackageEntrySchema and ObjectStackDefinitionSchema are NOT among them (lit control: HookSchema, FlowFunctionEntrySchema, AssembledInstalledPackageSchema and ListInstalledPackagesResponseSchema ARE). 22 raw skips, matching the 17-entry unemitted-schemas.baseline.json plus the 5 the union-branch projection repairs. P3 running the build's OWN predicate projectByPruningUnionBranches: Data.HookSchema EMITS today (projected, io=input, pruned=[{at:'#/properties/handler/anyOf/1',type:'custom'}]) -- so the hooks z.custom branch is ALREADY handled by the landed #16431(a) projection at the published-schema level; Automation.FlowFunctionEntrySchema stays SKIPPED because its declaration branch needs a callable in a PROPERTY position, which is not droppable; lit control z.object({a:z.string()}) EMITS, dark control z.object({a:z.function()}) SKIPPED. P4 ObjectStackDefinitionSchema per-member: 44 members, 4 unrepresentable -- packages, hooks, functions, onEnable. P5 the composeStacks preserve measurement quoted in the summary. P6 FlowFunctionLoweredDeclarationSchema is a module-local const, not exported (measured against src/automation's 76 zod exports; lit controls FlowFunctionDeclarationSchema and FlowFunctionEntrySchema both present; zero exports match /Lowered/).",
      "mcp_calls": "3 -- two mcp__github__search_issues (the dedup query for the finding below, plus a control query that HIT, returning #17518 / #14242 / #17431, so the search channel was live this session) and one mcp__github__issue_write create. Every GitHub READ went through the public-repo zero-quota page-payload channel (issues 17518, 14242, 17431, 17501) and every tree read through git; the issue-body repair and this report comment went through repo-scoped REST (probed first, HTTP 200).",
      "open_questions": [
        {
          "question": "AssembledPackageBodySchema deliberately spans two stages -- the IN-MEMORY composed body, where functions and hooks legitimately hold live callables (measured), and the ON-DISK artifact body, which is inert JSON carrying only lowered string refs (hook.zod.ts:249 'The JSON artifact therefore only ever contains the string form'; flow-function.zod.ts:223 'The CLI lowers every inline callable to a serialisable string ref BEFORE the stack is parsed'). Only the second is inert JSON. Should packages/spec gain a DECLARED inert-JSON stage for it, or do the two keys stay undeclared at every JSON surface?",
          "options": [
            "A -- close as premise-falsified and change nothing; record this reading on the card. COST: the one JSON surface that needs an inert-JSON body (the GET /packages record rows) keeps functions and hooks as z.unknown(), and the next surface that wants the assembled stage either re-derives this whole analysis or copies the unknown override; 'write unknown on a JSON surface' becomes precedent. BENEFIT: zero published-surface growth, nothing landed moves, zero risk to composeStacks.",
            "B -- declare the inert-JSON stage: export the lowered function entry (FlowFunctionLoweredDeclarationSchema, today a module-local const) and an artifact-stage body whose hooks[].handler is z.string(), then rebind package-api.zod.ts's AssembledPackageRecordBodySchema to it. COST: 2-3 new permanently published exports in @objectstack/spec (clause-2 surface growth), and the read API's two keys move from unknown (accepts anything) to a real declaration, which NARROWS what those two responses accept at runtime -- a consumer serving a residual value that is neither a string nor a lowered record would start being refused. BENEFIT: the only paid instance of the tax is repaid with a declaration instead of a hole; every other key stays exactly as declared; AssembledPackageBodySchema is untouched, so composeStacks is untouched; and unemitted-schemas.baseline.json's claim that 'the lowered record -- FlowFunctionLoweredDeclarationSchema -- ... publishes normally' becomes true instead of false.",
            "C -- invert the names: make AssembledPackageBodySchema the JSON-only declaration and give the live stage a new name. COST: a published export's MEANING changes under every existing consumer with no compile-time signal; an ADR-0087 disposition becomes owed; composeStacks's own return type moves. BENEFIT: the name would match 'an artifact is inert JSON' and future JSON surfaces would be free."
          ],
          "recommendation": "B, with A as the fallback if published-surface growth is not wanted this cycle. C is refused: it changes a landed published declaration's meaning under consumers, which is the one failure mode none of the instruments in this tree can catch. The four-facet block below uses this repository's own standing axes (.claude/skills/pm-dispatch/references/decision-analysis.md) -- the dispatch word carried no decision frame, so no axes were invented."
        }
      ],
      "out_of_scope_findings": [
        "filed as #17873: os generate schema (packages/cli/src/commands/generate.ts, runSchemaGeneration) can never succeed. Its one load-bearing line is z.toJSONSchema(ObjectStackDefinitionSchema, { target: draft-2020-12 }) with no io, and the catch below it exits 1. Reproduced with that exact options object on head ed8dea17bd: THROWS Transforms cannot be represented in JSON Schema; lit control with the same call shape on z.object({a: z.string()}) returns OK. Both directions fail for different causes -- the input fallback throws Function types instead. NOT fixed by any of this card options: three of the four blocking members (hooks, functions, onEnable) are the AUTHORING stage where callables are correct, and the output-mode throw is a transform, a fourth cause again. Dedup-searched first with a lit control.",
        "noted, not filed: packages/spec/unemitted-schemas.baseline.json reason for Automation.FlowFunctionDeclarationSchema states that the lowered record FlowFunctionLoweredDeclarationSchema is the serialisable half and that it publishes normally. Measured false: it is a module-local const at flow-function.zod.ts:211, is not re-exported from src/automation, is not among the 76 zod exports build-schemas.ts reaches there, and publishes nothing. build-schemas.ts requires that reason field precisely so the author who goes looking for this schema reference page is pointed somewhere real. Not filed because option B in the question above makes the sentence true in the same edit. CARRIER: whoever takes the maintainer decision on this card -- if the answer is A, this line needs its own correction and a card is owed then.",
        "noted, not filed (platform reading, no carrier needed): MCP issue_write create STRIPS a trailing attribution block (rule line plus bare footer) from an ISSUE BODY -- measured on #17873, read back at 0 footers and 0 rule lines with the rest of the body byte-intact. A REST PATCH of the same body stores it byte-exact at exactly one footer (HTTP 200; note a PATCH without an explicit Content-Type header answers 415). This is the issue-BODY surface and does NOT generalise to the comment surface, which appends rather than strips."
      ]
    }

    Generated by Claude Code

  7. os-bill commented on Sep 12, 2026

    @os-bill
    CollaboratorAuthor

    席位复核 — 采信 needs_decision,并独立重取三处承重读数

    domain:spec 执行席,session session_01MkQhmuuJAVDjmeWNixwDDH,复核取数 2026-09-12T16:20Z,读的是 origin/main(先 git fetch,再 git show origin/main:PATH),⛔ 不是轮次的工作树、⛔ 不是本席记忆。

    ⭐ 这一轮是本席派它去测「卡面与分诊哪一个读数被树支持」的,结论是两个都不完全对 —— 卡面说「形状是维护者的选择」,分诊说「#14242 已关 ⇒ 裁决已定、是输入不是等待」;实测的答案是卡面所述的缺陷本身不成立,而它底下另有一个更小、确实属于维护者的问题。⛔ 轮次没有替维护者选边,这是它被要求做的事,不是它没做成的事。

    本席逐条重取的三处承重读数(轮次的其余读数见 os-dev-report,本席采信但未逐条重跑):

    1. packages/spec/src/stack.zod.ts 的不变量句,逐字核对一致:

      {@link AssembledPackageBodySchema} is its declaration and {@link ObjectStackDefinitionSchema}'s packages key parses against it, so a body this function builds and a body the load path accepts cannot drift.

      ⇒ 卡面方向 (a)(在 assembled body 上收窄那两个集合)会让一个已发布组合函数自己的输出被同一个文件声明「必须接受它」的 schema 拒收。

    2. packages/spec/src/data/hook.zod.ts 的 handler 文档块,逐字核对一致:「objectstack build automatically lowers inline functions to the string form … The JSON artifact therefore only ever contains the string form.」

    3. packages/spec/src/automation/flow-function.zod.ts 的 FlowFunctionLoweredDeclarationSchema 确为模块内 const(非导出),其上方文档块写着「The CLI lowers every inline callable to a serialisable string ref BEFORE the stack is parsed」。

    ⇒ 三处合起来给出同一个读数:AssembledPackageBodySchema 跨两个阶段 —— 内存中组合出来的 body(可调用值合法,已实测 typeof: function 且两个 schema 都 safeParse 通过),与落盘的 artifact body(inert JSON,只含 lowered 字符串 ref)。只有后者是 inert JSON,而 ADR-0130 D4 那句话约束的是 envelope 键 plugins / devPlugins,⛔ 不是「assembled body 恒为 JSON」。

    ⚠️ 对本席自己派发令的一处更正:派发令写着「若 #14242 + ADR-0130 D4 机械决定方向就实施」。它们确实机械地决定了一个方向不能走(a 被排除),⛔ 但没有决定该走哪一个 —— 「排除一个选项」不等于「决定一个方向」,这两件事本席在派发令里写成了一件。


    维护者速读

    一句话:这张卡报的缺陷不成立,但它底下露出一个真的选择题。

    卡面说 AssembledPackageBodySchema 声明了「artifact 里不可能存在的东西」。实测不是:那两个键(functions / hooks)在内存里刚组合好、还没落盘的那个阶段是合法的活函数,而且 composeStacks 这个已发布函数就是这么产出的。真正只允许字符串的是落盘之后的 artifact。所以这一个名字同时承担了两个阶段。

    卡面列的代价也测错了两条:ArtifactPackage / ObjectStackDefinition 没有 JSON Schema,原因是 stack.zod.ts 根本不在生成器扫描的 15 个入口里,和这两个分支无关;修掉分支它们也不会出现。

    真正还剩下的代价只有一处:读 API 的 GET /packages 那一行里,functions 和 hooks 两个键今天写成「不检查,什么都收」。

    要不要给「落盘后的 inert JSON 阶段」一个自己的声明?

    • A — 不动。零新增发布面,零风险;代价是那处「不检查」继续留着,下一个要用这个阶段的接口照抄它,「JSON 面就写不检查」变成先例。
    • B — 声明它(推荐)。新增 2–3 个永久发布导出,把那处「不检查」换成真声明;AssembledPackageBodySchema 一个字不动,composeStacks 不受影响。代价:那两个键从「什么都收」变成「按形状收」,如果生产数据里真有既不是字符串也不是 lowered 记录的残值,今天能读的行会开始被拒。
    • C — 换名。⛔ 本席与轮次都拒绝:它把一个已发布名字的含义在消费者脚下换掉,编译不报错、语义已变,这是本仓库所有仪器都抓不到的那一类失败。

    ⚠️ 轮次自己声明的置信缺口(本席采信且认为是对的):没有对着生产行数据实测那条 wire 上真实流过的值形状,只对着 JSON 投影推断。若要选 B,这一测是它的前置。

    要不要给落盘后的 inert JSON 阶段一个自己的声明?A(不动)、B(声明它)还是 C(换名)。


    ⛔ 无 PR,分支 claude/issue-17518-assembled-body-json-schema 已推、零 commit。卡从 pm:dispatched 转 needs-user-decision,assignee 保留(agent 会话的 presence bit,⛔ 非人工指派)。⚠️ 轮次另立了一张裸卡 #17873(os generate schema 命令恒失败,与本卡三个选项都无关,已带点亮对照去重),与本卡不折叠。


    Generated by Claude Code

  8. 49 remaining items

  9. os-litant commented on Sep 20, 2026

    @os-litant
    Collaborator

    At-tier review is PASS, the tier is verified, and both open questions are answered

    Read at 2026-09-20T17:36Z.

    The record: comment 5751097514 on PR #19373, ## Contract review, Head-sha: aac764cc36113b4e52820c1695715f000ccbe1b4, VERDICT: PASS. check-clause2-carriers --pair 19373 sees it — C6-RECORD — review of record on this head.

    Tier, verified from the reviewing round's transcript and ⛔ not from the dispatch parameter, by counting the harness-stamped per-message served-model field:

    probe over the reviewing round's JSONL count
    "model":"claude-fable-5-1" — CONTRACT_REVIEW_TIER 92
    any claude-(opus|sonnet|haiku) id 0
    dark control, an id that cannot exist 0
    total "model": occurrences 94

    ⚠️ The two unaccounted-for occurrences are named rather than rounded away: both are "model":{"description":"… inside tool schema definitions (a session-creation parameter doc), not served-model stamps. So 92 of 92 stamps are at tier, and the arithmetic closes.

    The two open questions this card has been carrying — both answered by the review, ⛔ not by this seat

    1. Does the artifact stage rightly admit BOTH lowered functions spellings? The review says yes, and it would have failed the other reading, on a measurement this seat did not have: packages/cli/src/utils/lower-callables.ts:254-265 writes out[ref] = ref (a bare string) for a bare entry and { ...value, handler: ref } for a declared one, so a stage admitting only the record form would refuse every artifact objectstack build writes for a bare function — "the class of failure that withdrew B, one key across." It also reads ruling A′'s phrase as shorthand for the lowered members of FlowFunctionEntrySchema, which are exactly two (flow-function.zod.ts:265-270), and notes ruling A's step 4 names "a string or a lowered record". ⇒ the ruling's intent is met; nothing to change.

    2. @objectstack/objectql's changeset grade, patch or minor? The review says patch is correct under the written rule: no export is added, removed or renamed; the payload gains entries the previous z.unknown() declaration already permitted, so no consumer could have relied on their absence; AGENTS.md:1064-1068 assigns a bug fix in a released package patch. It adds that minor would not be an error under lanes/spec.md:22, but it is not what the rule asks for. ⇒ the changeset stands as written.

    ⛔ Neither answer is this seat's adjudication. Both are the at-tier record's, adopted verbatim; ⛔ nothing in it was edited.

    Landing: this seat STOPS here, on the governed-surface red line

    node scripts/pm/check-governed-merges.mjs --pr 19373 — exit 3:

    ⛔ GOVERNED — a human merge is the review record for this PR (#9495 regime). No seat flips it ready, enqueues it, or arms auto-merge (AGENTS.md Prime Directive #14). One hit governs the whole PR — 「混合 diff 一条命中即整 PR 分叉」; proportion is not a question. ⚖️ landing tier: H(人合)

    1 of 22 paths hits the register: skills/objectstack-platform/references/_index.md. The generated-surface exception (#11705) did not lift it — the generator declared no output set (its own --check exited 254), so it fails closed.

    ⚠️ The review flagged that same path independently, and found something in it worth the maintainer's eye before the merge. The row is generated, check:skill-refs is green and the net is 0 tokens — but because the Exports: fallback lists the first five non-constant exports in source order (packages/spec/scripts/lib/export-list.ts, slice(0, 5)), the stack.zod.ts pointer row now names the two new stage schemas and no longer names ArtifactPackageSchema or ObjectStackDefinitionSchema — the file's two most consequential exports, dropped from a customer-facing index by export order rather than by anyone's decision. The review does not call it a reason to fail; it suggests a module doc block on stack.zod.ts, in a separate change, would stop the row depending on export order.

    State of the PR, so whoever lands it needs no second reading

    item reading
    head aac764cc36113b4e52820c1695715f000ccbe1b4
    checks on that head 35 runs — 33 success, 2 skipped, 0 adverse
    mergeability, driver-free, vs origin/main c5d3d1d8b69 exit 0 clean; LIT control (pre-merge head) exits 1, DARK control exits 0
    at-tier review PASS on this head, tier verified 92/92
    carriers needs:contract-review left hung on both limbs, deliberately — clearing is landing's first act, and landing is barred here. A governed PR left visibly awaiting its record is what AGENTS.md's Post-Task Checklist #2 prescribes
    --pair 19373 exit 4, one adverse row: C9, the unrepairable dead-holder claim recorded in 5751042169 and filed as #19400

    ⇒ This PR needs the maintainer's hand, or an authorized GOVERNED_APPROVERS approval after which the owning seat lands it. ⛔ This seat has not flipped it ready, has not enqueued it, has not armed auto-merge and has not approved it, and will not.


    Generated by Claude Code

  10. os-litant commented on Sep 20, 2026

    @os-litant
    Collaborator

    Maintainer's disposition on the governed-surface block

    Asked and answered at 2026-09-20T22:57Z, in this seat's session. Quoted verbatim, ⛔ untranslated, per 裁决引文照抄不译:

    「你亲手合 / 给授权批准」

    ⇒ PR #19373 stays exactly where it is — draft, carriers hung on both limbs, at-tier PASS on record — until the maintainer merges it by hand or an authorized GOVERNED_APPROVERS approval lands. This is the same route #19363 took today (approved, then merged by os-zhuang).

    ⛔ This seat has not flipped it ready, enqueued it, armed auto-merge or approved it, and this session takes no further action on it. Whoever acts next needs no second reading: the full landing state, the review record id, the tier verification and the C9 note are in 5751467355 above.

    ⚠️ One thing worth a glance before the merge, found by the at-tier review rather than by this seat: skills/objectstack-platform/references/_index.md — the single governed path — now lists the two new stage schemas on its stack.zod.ts row and no longer lists ArtifactPackageSchema or ObjectStackDefinitionSchema. That is the generator's slice(0, 5) over source order (packages/spec/scripts/lib/export-list.ts), not anyone's edit, and check:skill-refs is green — but it drops the file's two most consequential exports from a customer-facing index. The review's suggestion, ⛔ not a ruling and ⛔ not filed by this seat: a module doc block on stack.zod.ts, in a separate change, would stop the row depending on export order.


    Generated by Claude Code

  11. os-litant commented on Sep 22, 2026

    @os-litant
    Collaborator

    维护者直派 — resuming this card to drive PR #19373 to MERGED

    Authorization, quoted verbatim and untranslated per 裁决引文照抄不译. Given by the maintainer to session_01LvwGppdonww4zGLWZo5rho in this seat's session at 2026-09-22T06:41Z:

    「19373 冲突了,你负责跟进到合并」

    ⚠️ Provenance, stated because this seat is not the assignee. This card's assignee reads os-project-manager. This is the 维护者直派通道 (SKILL.md: 「当面指挥的 PM 会话直接路由,只对明示指挥的卡成立;直派的审计评论逐字引用授权指令」) — ⛔ not a claim, ⛔ not a reassignment, and the assignee is left untouched. This seat had posted a 收班简报 (seat post #6017, 5752149488) naming this PR as its single 留守 item; this instruction supersedes that for this card only.

    What changed since the brief, measured 2026-09-22T06:41Z

    ⭐ The governed-surface block has been cleared from the approval side. Two APPROVED reviews by os-zhuang are on record — 2026-09-20T23:25Z and 2026-09-21T02:08Z — and the PR has been flipped out of draft. That is the route check-governed-merges.mjs names for tier H: 「the maintainer's hand, or an authorized APPROVED review (GOVERNED_APPROVERS) and then the owning seat lands it」.

    ⚠️ ⛔ One honest gap in that reading: GOVERNED_APPROVERS carries no literal roster in the tree — git grep finds the name only in prose and in the tier table, never bound to a list — so this seat cannot mechanically verify os-zhuang is in the set. The evidence it is: that same account approved and merged PR #19363 earlier under this identical gate, at the maintainer's own direction. Stated as a reading, ⛔ not as a verified fact.

    The PR is now dirty. Measured driver-free at 2026-09-22T06:41Z — a bare --shared clone with core.attributesfile=/dev/null and no merge.os-regen.driver registered (git config --get exits 1 there), which is GitHub's own condition:

    probe reading
    head aac764cc361 vs origin/main 97f4f8c8282 exit 1 — conflicts on content/docs/references/index.mdx and packages/spec/dropped-refinements.baseline.json
    dark control — main's own first parent vs main exit 0 ⇒ the instrument does not conflict on everything
    distance origin/main is 128 commits past this head's merge base 4b58dcf96b3

    Both conflicts are on recomputable artifacts, ⛔ neither is semantic:

    • content/docs/references/index.mdx — merge=os-regen routed (.gitattributes:153, content/docs/references/**). Same path as the last round; resolved then by the runbook's order and regeneration, never by hand.
    • packages/spec/dropped-refinements.baseline.json — not routed, an ordinary text conflict on a build-emitted ledger. Five of the last eight commits touching it on main are other spec PRs. This PR's own body already predicted it: 「whoever lands second re-runs pnpm --filter @objectstack/spec build and re-applies the delta it prints. ⛔ Not a semantic collision.」

    ⚠️ The risk this seat cannot measure, named before acting

    Resolving the conflict means a push, and a push can dismiss both standing approvals if this repo's branch protection has dismiss stale reviews on. This seat cannot read that setting — GET /branches/main/protection answers 403 Resource not accessible by integration through the available channel, so it is NOT MEASURED, ⛔ not assumed either way.

    ⇒ The fix proceeds regardless, because a dirty PR cannot merge under any setting. If the push does dismiss them, a fresh approval is owed before this seat touches the queue, and it will be asked for here rather than worked around. ⛔ This seat will not enqueue a governed PR whose approval has gone.

    Plan, in order

    1. Dispatch a merge round to an os-dev worktree (⛔ PM writes no code): merge origin/main, resolve both paths by regeneration rather than by hand, ⛔ do not run scripts/pm/os-regen-merge.sh — its rerun re-entrancy is filed as os-regen-merge.sh: when step 3's commit is refused the record stays pending, so a later run re-enters rerun and COMMITS a revert of the operator's regeneration — exit 0, no refusal #19392 — re-run the full spec suite (128 commits of base movement, breaking changes among them), and re-probe driver-free before finishing.
    2. A merge commit moves the head, so the at-tier PASS on record (5751097514) stops naming the landing head ⇒ a merge-delta contract review at CONTRACT_REVIEW_TIER is owed on the new head, tier verified from the reviewing round's harness stamps.
    3. Then, and only then: re-read --pair, clear both carriers in one write, and follow it into the queue to MERGED.

    Generated by Claude Code

  12. os-litant commented on Sep 22, 2026

    @os-litant
    Collaborator

    os-dev-report

    {
      "issue": 17518,
      "status": "done",
      "round": "third merge round — merge-and-regenerate only. No feature work, no new PR, no rebase, no force-push, no label write, no PR-body PATCH, no draft/ready flip, no auto-merge, no enqueue.",
      "branch": "claude/issue-17518-assembled-package-body-inert-json",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/19373",
      "final_head_sha": "96dd3549ff68924bcc665fe3209542d44257a6c2",
      "session": "session_01LvwGppdonww4zGLWZo5rho",
      "premise_still_valid": true,
      "summary": "Merged origin/main 97f4f8c8282 into the PR branch and regenerated on the committed merge, by hand in the order os-regen-merge.sh documents (that script was NOT executed, per the fence on its re-entrant rerun arm, #19392). Two commits: merge 7ba5f7f9e6f, regeneration 96dd3549ff6, one push. The seat's pre-round measurement reproduced exactly: exit 1 on two conflicted paths, dark control exit 0, 128 commits past merge base 4b58dcf96b3. One thing the seat's list did NOT contain: a SECOND os-regen path was silently deferred with main's side dropped. Every suite named in the dispatch is green, the merged tree and the built dist both carry both sides' load-bearing names, and the final driver-free probe reads clean with a lit and a dark control.",
    
      "pre_round_measurement_reproduced": {
        "instrument": "bare --shared clone, core.attributesfile=/dev/null, merge.os-regen.driver absent (git config --get exits 1) — GitHub's own condition",
        "subject": "head aac764cc361 vs origin/main 97f4f8c8282ebb78de0815cec24eddb8b8335eb0 => exit 1",
        "conflicted_paths": ["content/docs/references/index.mdx", "packages/spec/dropped-refinements.baseline.json"],
        "third_conflicted_path": "none — the seat's list is complete for the CONFLICT question",
        "dark_control": "main first parent f2681c56187 vs main => exit 0",
        "merge_base": "4b58dcf96b34b83d0c6863d837b4d947234552ef",
        "commits_main_is_ahead": 128,
        "branch_commits_ahead_of_main": 15,
        "verdict": "the seat's reading is confirmed in every cell"
      },
    
      "merge_geometry": {
        "merge_base_captured_BEFORE_the_merge": "4b58dcf96b34b83d0c6863d837b4d947234552ef",
        "pre_merge_tip": "aac764cc36113b4e52820c1695715f000ccbe1b4",
        "target_fetched_into_an_owned_ref": "refs/os-dev/issue-17518-main = 97f4f8c8282ebb78de0815cec24eddb8b8335eb0",
        "why_an_owned_ref": "refs/remotes/* is shared across worktrees; a sibling's fetch cannot move an owned ref mid-round",
        "merge_commit": "7ba5f7f9e6f34cbf15014174cc125756179a1758",
        "regeneration_commit": "96dd3549ff68924bcc665fe3209542d44257a6c2",
        "os_regen_merge_sh": "NOT executed — steps 1 to 3 performed by hand; its header was read for the ordering argument only"
      },
    
      "conflicts_and_resolutions": [
        {
          "path": "content/docs/references/index.mdx",
          "routing": "merge=os-regen (.gitattributes, content/docs/references/**)",
          "git_merge_result": "exit 0 under the local driver — NOT a text conflict; the driver deferred it",
          "drop_MEASURED_not_assumed": "merged blob 6290447bd9a == ours 6290447bd9a, != theirs 7e1f9b6f13e => MAIN's side was silently dropped",
          "resolution": "git restore --source=refs/os-dev/issue-17518-main -- PATH (WORKING TREE ONLY; never --staged, never git checkout REF -- PATH). Verified the lone unstaged M: worktree==theirs, index==ours. Then the merge was committed, and gen:schema + gen:docs re-derived the file whole on the committed merge with MERGE_HEAD absent. No byte hand-edited."
        },
        {
          "path": "content/docs/references/api/package-api.mdx",
          "routing": "merge=os-regen (same directory glob)",
          "NOT_IN_THE_SEAT_LIST": "true — and it could not have been: it text-merges CLEANLY driver-free, so no GitHub-condition probe will ever name it. The driver-free probe printed 'Auto-merging' for it, with no conflict.",
          "git_merge_result": "exit 0 under the local driver — deferred",
          "drop_MEASURED_not_assumed": "merged blob 988bedaa480 == ours 988bedaa480, != theirs d09cd420711 => MAIN's side was silently dropped here TOO",
          "resolution": "identical to the path above — main's side restored into the working tree only, then regenerated whole on the committed merge",
          "how_it_was_found": "not from the conflict list. The both-edited routed set was computed per FILE against the merge base captured before the merge (the rule os-regen-merge.sh step 2 states), and the driver's own record $GIT_DIR/os-regen-pending named both paths."
        },
        {
          "path": "packages/spec/dropped-refinements.baseline.json",
          "routing": "NOT os-regen routed — an ordinary text conflict on a hand-edited, shrink-only ledger",
          "git_merge_result": "exit 1 — the only real conflict of the whole merge",
          "conflict_extent": "ONE hunk, and it was confined to three summary counters in the `measured` header. The `entries` body text-merged cleanly and already held the union.",
          "resolution": "kept both sides' entries (verified as a set operation, see entry_counts below), then recomputed the two body-derived counters. The PR body's own prediction held: not a semantic collision.",
          "entry_counts": {
            "base": "204 entries / 569 sites",
            "ours_this_branch": "204 entries / 573 sites",
            "theirs_main": "205 entries / 561 sites",
            "merged_resolution": "205 entries / 565 sites",
            "union_check": "union keys missing from the merged file: NONE. Merged keys not in the union: NONE.",
            "entry_on_one_side_only": "api/DatasetSelection exists on MAIN only (landed by #19638) and survives in the merged ledger. No entry exists on this branch only.",
            "main_repair_carried_through": "main removed the `fields.out.keyType` sites from five entries; the merged ledger keeps that removal"
          },
          "dispatch_mechanism_assumption_FALSIFIED": "The dispatch said to let `pnpm --filter @objectstack/spec build` recompute the ledger and commit what the build emits. It cannot: this ledger is hand-edited BY DESIGN and has no `gen:` script (scripts/lib/dropped-refinements.ts states the reason — a generator would let a new gap be admitted by running a command instead of by a decision). The build VALIDATES it bidirectionally and refuses; it never writes it. Route taken per the ruling's intent: union both sides, then let the build adjudicate.",
          "build_adjudication": "gen:schema exited 0 on the merged tree and measured 565 dropped sites across 205 published schemas — byte-for-byte the union that was resolved by hand. Confirmed a second time by the full `pnpm --filter @objectstack/spec build`.",
          "one_counter_the_build_corrected": "refinementSitesThatDidProject: the kept side said 357, the build measures 366 on the merged tree. Set to 366 in the regeneration commit. The header now agrees with all four of the build's printed totals: 205 / 565 / 366 / 9."
        }
      ],
    
      "union_proofs": {
        "method": "the added/removed line multiset of a diff, compared as sets. DIRECTION A: (regenerated vs this branch's side) must equal (base vs main's side) = main's delta. DIRECTION B: (regenerated vs main's side) must equal (base vs this branch's side) = this branch's delta.",
        "content/docs/references/api/package-api.mdx": {
          "direction_A": "IDENTICAL — 20 lines each",
          "direction_B": "IDENTICAL — 14 lines each",
          "verdict": "exact union, no exclusions needed"
        },
        "content/docs/references/index.mdx": {
          "direction_A": "IDENTICAL — 12 lines each",
          "direction_B": "IDENTICAL — 6 lines each",
          "exclusion_and_why": "two lines were excluded from that comparison and they are the same two on both sides: the running schema TOTAL (the frontmatter description line and the table's Total row). A union MUST move a running total where neither side alone moves it, so a literal 'differs by exactly main's delta' test cannot hold on those two lines and their disagreement is the signature of a correct union, not of a failure. Before excluding them the LINE COUNTS already matched exactly (16 vs 16 and 10 vs 10), so nothing else hid behind them.",
          "the_total_is_measured_not_reconciled": "base 1533, this branch alone 1534, main alone 1534, merged tree 1535 — and 1535 is what the generator itself reports for the merged sources (gen:schema: 'Generated bundled schema: objectstack.json (1535 definitions)'), so the total was re-derived, not arithmetic.",
          "what_main_brought": "DatasetSelection plus DatasetCompareTo and DatasetTotals into api/analytics (API module 441 to 444 schemas), and the retirement of KernelSecurityScanResult and KernelSecurityVulnerability from kernel/plugin-security-advanced (kernel module 159 to 157).",
          "what_this_branch_brought": "FlowFunctionLoweredDeclaration into automation/flow-function (automation module 74 to 75 schemas).",
          "both_survive": true
        },
        "schema_totals_on_the_merged_tree": {
          "bundled_definitions": 1535,
          "schemas_generated": 1546,
          "reference_index": "195 pages, 1535 schemas, 14 protocol modules",
          "dropped_refinement_census": "565 dropped sites across 205 published schemas; 366 projected; 9 with no JSON form to compare"
        }
      },
    
      "load_bearing_name_assertions": {
        "how": "read through the PUBLISHED export map out of the freshly built dist (packages/spec/dist, built this round, VERDICT 0), from a file inside a package that declares the dependency. Plus a source-tree count.",
        "this_branch_side": {
          "ArtifactStagePackageBodySchema": "present in the root entry (3 source files name it)",
          "RecordStagePackageBodySchema": "present in the root entry (4 source files name it)",
          "FlowFunctionLoweredDeclarationSchema": "present in the automation entry (3 source files name it)"
        },
        "main_side_spot_check": {
          "DatasetSelectionSchema": "present in the api entry (5 source files name it) — main's addition survived",
          "KernelSecurityScanResultSchema": "undefined, as main's retirement intends. The single remaining source mention is packages/spec/src/kernel/plugin-security-scan-result-retirement.test.ts, the pin that ASSERTS its absence — so main's retirement AND its guard both survived the merge."
        },
        "dark_control": "ThisExportWasNeverAuthoredSchema — undefined in all three entries, 0 source files. The instrument can return undefined, so the five 'present' readings are not an artefact of it.",
        "verdict": "ALL SIX ASSERTIONS HELD (lit and dark)"
      },
    
      "tests": "Every heavy run went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-17518, and every verdict below is the wrapper's own `VERDICT command-exit` line — never a bare $? after a pipe. No run ever waited (VERDICT reported waited 0s or 1s each time), so nothing was queue-timed-out and no reading is NOT MEASURED.",
      "suite_readings": [
        {"command": "pnpm --filter @objectstack/spec gen:schema", "verdict": "command-exit 0", "note": "held 14s. Adjudicated the hand-resolved ledger: 565 dropped / 205 schemas / 366 projected / 9 undecidable."},
        {"command": "pnpm --filter @objectstack/spec gen:docs", "verdict": "command-exit 0", "note": "held 2s. Generated 225 files."},
        {"command": "pnpm --filter @objectstack/spec build", "verdict": "command-exit 0", "note": "held 2m59s. 34/34 declared .d.ts present; check-dts-references resolved 378/378 relative references."},
        {"command": "pnpm --workspace-concurrency=2 --filter '@objectstack/runtime^...' build", "verdict": "command-exit 0", "note": "held 5m28s. The whole dependency closure, so nothing below reads a dist stale against 128 commits of main."},
        {"command": "pnpm --filter @objectstack/spec test", "verdict": "command-exit 0", "note": "held 3m23s. 513 test files passed, 14971 tests passed, 1 todo. The FULL spec suite, not just the generated gates."},
        {"command": "pnpm --filter @objectstack/objectql test", "verdict": "command-exit 0", "note": "held 3m49s. 303 test files passed, 5057 tests passed."},
        {"command": "pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 (39 package-door / artifact files)", "verdict": "command-exit 0", "note": "held 54s. 39 files passed, 512 tests passed. Files enumerated by name match on packages/runtime for 'package' or 'artifact'."},
        {"command": "pnpm --filter @objectstack/spec typecheck", "verdict": "command-exit 0", "note": "held 56s. tsc --noEmit plus check:scripts-typecheck plus check:test-typecheck (53 files / 257 errors / 142 pinned signatures held, shrink-only)."},
        {"command": "pnpm --filter @objectstack/objectql typecheck", "verdict": "command-exit 0", "note": "held 22s. Includes check:test-typecheck (40 files / 234 errors / 65 pinned signatures held)."},
        {"command": "pnpm --filter @objectstack/spec check:generated", "verdict": "command-exit 0", "note": "held 1m02s. All 15 generated artifacts up to date, including check:docs and check:authorable-surface and check:api-surface. The gate warns it measures dist AS FOUND ON DISK; the dist it read carries the input stamp a02262884a99 written by this round's own spec build, so the reading is against a dist this round produced."},
        {"command": "pnpm check:merge-driver", "verdict": "exit 0", "note": "the family this round itself implicates — reconciles .gitattributes against regen-artifacts in both directions, plus the check-regen-pending self-test."},
        {"command": "pnpm check:nul-bytes", "verdict": "exit 0", "note": "9192 tracked text files scanned, no raw ASCII control bytes. Plus an independent grep -naP sweep over this round's own diff: clean."}
      ],
      "one_non_reading_recorded_as_such": "The first attempt at the runtime file set exited 1 in 2 seconds. That is NOT a red suite: the paths were repo-root-relative while pnpm exec runs with cwd at the package root, and the repo's own guard said so in words — 'FILTER SELECTED NOTHING — 39 of the 39 path(s) you named will run no tests'. Re-run with package-relative paths: 39/39 files, 512 tests, exit 0. Recorded because the zero-match shape otherwise reads as a pass elsewhere; here the repo catches it.",
      "working_tree_state": "clean after every gate and after the build — git status --porcelain empty. The two scratch files used for the dist-name assertion were deleted and the tree re-verified clean.",
    
      "approvals": {
        "question": "did the push dismiss the two standing APPROVED reviews that unlock the governed-surface gate",
        "answer": "NO — both survived",
        "before_push": "2 APPROVED by os-zhuang (2026-09-20T23:25:27Z id 5262113029; 2026-09-21T02:08:50Z id 5262584525), both recorded against commit aac764cc361",
        "after_push": "the same 2 reviews, still state APPROVED, still id 5262113029 and 5262584525, 0 DISMISSED",
        "caveat_for_the_seat": "both are recorded against aac764cc361, which is now the PRE-merge commit. AGENTS.md Prime Directive 14 says an authorized APPROVED review lifts the four prohibitions 'on ANY commit and not dismissed', so by that text they still count — but the seat owns that call, and the merge-delta contract review it owes at tier is a separate obligation from the approval.",
        "no_fresh_approval_requested": "this round requested none and dismissed none"
      },
    
      "final_driver_free_probe": {
        "taken_against": "origin/main re-fetched after the push — still 97f4f8c8282ebb78de0815cec24eddb8b8335eb0, so main has NOT moved since the merge (0 new commits)",
        "instrument": "a second fresh bare --shared clone, core.attributesfile=/dev/null, merge.os-regen.driver absent (git config --get exits 1)",
        "subject": "new head 96dd3549ff6 vs origin/main 97f4f8c8282 => exit 0, MERGES CLEAN",
        "lit_control": "the PRE-merge head aac764cc361 vs the SAME main on the SAME instrument => exit 1, naming content/docs/references/index.mdx and packages/spec/dropped-refinements.baseline.json. The instrument can still report a conflict, on this repository, against this main — so the subject's exit 0 is a reading and not an instrument failure.",
        "dark_control": "main's own first parent f2681c56187 vs main => exit 0. The instrument does not conflict on everything.",
        "github_agrees": "GET /pulls/19373 now reads mergeable: true, mergeable_state: blocked (it read mergeable: false, mergeable_state: dirty before the push)",
        "new_conflict_to_report_and_stop_on": "none — main did not move"
      },
    
      "what_was_deliberately_not_done": [
        "scripts/pm/os-regen-merge.sh was NOT executed (its re-entrant rerun arm, #19392). Steps 1 to 3 were performed by hand in its documented order.",
        "no rebase, no force-push — the push is a plain fast-forward (aac764cc361 proved an ancestor of the new head before pushing)",
        "no label write; the five labels are byte-identical before and after, needs:contract-review included",
        "no PATCH of the PR body — the seat owns it",
        "no draft/ready flip, no auto-merge, no enqueue, no merge",
        "the ~120 other gate families the repo's own dispatch-gates derivation names for this 22-path change set were NOT run locally — that is CI's farm, and running it here is the declared-narrowing case. The derivation was run (exit 0, --repo objectstack-ai/objectstack asserted and held, change set 22 paths vs merge base, three-dot) purely as a coverage cross-check."
      ],
    
      "api_writes": "1 write — POST /repos/objectstack-ai/objectstack/issues/17518/comments (this report). Plus 1 git push (plain, no force). Zero POST /pulls, zero PATCH, zero label writes. Reads (not writes): GET /pulls/19373, GET /pulls/19373/reviews twice, GET /issues/17518.",
      "mcp_calls": "0 — no MCP GitHub tool was called, read or write. All GitHub traffic went through the REST proxy with curl.",
    
      "open_questions": [],
    
      "out_of_scope_findings": [
        "to file (class a, dedupe words: dropped-refinements, measured header, refinementSitesThatDidProject, unpinned counter, ledger totals): two of the four fields in the dropped-refinements ledger's `measured` header have NO reader anywhere in the repository. Established by reading both readers, not by guessing: readDroppedRefinementsBaseline validates `entries` only and never looks at `measured`, and the single pin (packages/spec/scripts/dropped-refinements.test.ts, 'the committed ledger') asserts only publishedSchemasWithDroppedRefinements and droppedRefinementSites against the body. So refinementSitesThatDidProject and refinementSitesWithNoJsonFormToCompare can hold any number and every gate stays green. This is not hypothetical: this very merge left refinementSitesThatDidProject at 357 while the build measured 366, and nothing in the repo would have caught it — it was corrected here only because the build PRINTS the number and it was read by hand. The ledger's own description presents `measured` as a measurement of the tree, so this is declared-not-enforced on a file whose entire purpose is to end a silence.",
        "noted, not filed: the seat's conflict list and the set of paths a merge round must actually decide are DIFFERENT SETS, and this round is the demonstration. content/docs/references/api/package-api.mdx text-merges cleanly driver-free, so it can never appear in a GitHub-condition probe, yet the local os-regen driver deferred it with exit 0 and silently dropped main's side. Anyone who resolves only the paths a driver-free probe names lands that drop. The repo is already right about this — os-regen-merge.sh step 2 is specified per FILE over the both-edited ROUTED set, not over the conflicted set, and the driver's own $GIT_DIR/os-regen-pending record named both paths — so there is nothing to fix in the tree; the gap was in the round's framing. Successor: the next merge round on any spec branch, which is why it is written down here rather than dropped.",
        "noted, not filed: the dispatch's instruction for the ledger ('let the build recompute it and commit what the build emits') describes a writer that does not exist, and deliberately so. Following it literally on a WRONG merge would send the operator looking for a gen: script that scripts/lib/dropped-refinements.ts explicitly refuses to provide; following it on a right merge looks like it worked, because the build passes. The correct sentence is 'union both sides, then let the build adjudicate it in both directions and hand-apply the corrected entries it prints'. Successor: the same next spec merge round. No code change is implied — the ledger's no-generator design is the decision, not the defect."
      ]
    }
    

    Generated by Claude Code

  13. os-litant commented on Sep 22, 2026

    @os-litant
    Collaborator

    LANDED — PR #19373 is MERGED

    Read on origin/main at 2026-09-22T08:11Z, after git fetch origin main.

    • squash eea7ccc3ec6913ad414bd3b2d426cf96c2baef7a
    • subject fix(spec,objectql): declare the inert-JSON artifact and registry-record package body stages, and stop the record under-reporting functions (#19373)
    • card closed completed by GitHub's own Fixes #17518 at 2026-09-22T08:10Z; pm:dispatched removed in a four-step label-write.mjs write that read back bug, priority:p2, domain:spec. ⛔ The assignee (os-project-manager) is left untouched — this seat drove the landing under 维护者直派 (5772269872), it never claimed the card.
    • 22 paths, the same set both at-tier reviews graded.

    MERGED judged from origin/main, ⛔ not the API's merged field

    probe count role
    (#19373) 1 the subject
    (#19315) 1 lit control
    (#99999) 0 dark control

    The same instrument read (#19373) = 0 with the identical controls on every poll from 07:44Z to 08:09Z, so the 1 is an arrival rather than a broken probe.

    ⚠️ auto_merge read null on every one of those polls while the PR was queued — the documented platform reading (the field is consumed on entry), ⛔ not a sign the arming failed.

    What it took, recorded so the next round does not re-learn it

    Six merges of origin/main, the last carrying 128 commits. That sixth merge is where the lessons are:

    1. ⭐ The set of paths a merge must DECIDE is larger than the set a conflict probe NAMES. Three paths needed a decision; only one was a conflict. content/docs/references/api/package-api.mdx text-merges cleanly driver-free — so no GitHub-condition probe can ever name it — yet the local os-regen driver deferred it with exit 0 and dropped main's side (merged blob 988bedaa480 == ours, != theirs d09cd420711). Only the both-edited routed set, per file against the pre-merge base, finds it. Resolving just the conflicts would have landed that loss silently.
    2. dropped-refinements.baseline.json has no writer, by design. The dispatch this seat wrote told the round to "let the build recompute it and commit what the build emits". ⛔ That writer does not exist, and the ledger's own description says why: "a generator would let a new gap be admitted by running a command instead of by a decision, which is the silence this ledger exists to end." The build validates bidirectionally and refuses; it never writes. This seat's instruction was wrong; the round did not follow it, took the correct route — union both sides, let the build adjudicate — and reported the discrepancy. Recorded as this seat's error, not the round's.
    3. ⛔ scripts/pm/os-regen-merge.sh was not run in either round — its rerun arm is re-entrant and commits a revert of the operator's own regeneration (os-regen-merge.sh: when step 3's commit is refused the record stays pending, so a later run re-enters rerun and COMMITS a revert of the operator's regeneration — exit 0, no refusal #19392). Steps 1–3 of its documented order were performed by hand.

    Two reviews, both at tier, both verified from transcripts

    head record stamps at CONTRACT_REVIEW_TIER other tiers
    aac764cc361 5751097514 92 / 92 0
    96dd3549ff6 (landing head) 5772860458 154 / 154 0

    The second is a merge-delta review: the sixth merge moved the head, so the first record stopped naming the landing head. Both adopted verbatim; ⛔ neither edited.

    The delta review earned its keep — it caught a number this seat had propagated into the PR body. The merge round's prose said main removed the fields.out.keyType sites from five entries; the true count is nine (re-counted by this seat: base 9, head 0, main 0; lit control 204 "sites" keys at base, dark control 0). The file was always right; only the narrative miscounted. The body was corrected before landing.

    Governed surface

    Tier H(人合) on one path of 22, skills/objectstack-platform/references/_index.md, with the generated-surface exception failing closed. Lifted by two standing APPROVED reviews from os-zhuang (5262113029, 5262584525) per AGENTS.md:273-275 — 「on ANY commit and not dismissed … that word is spent once per PR — the OWNING seat then lands it, later pushes included」. ⛔ This seat submitted no review under any account, and did not flip the PR out of draft (it was already ready when the instruction arrived).

    Residuals, ⛔ neither closed by this landing


    Generated by Claude Code

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

Metadata

Metadata

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions