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
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 10, 2026 Triage: lands in
packages/spec/src/stack.zod.ts⇒domain:spec; typeBug,priority:p2,pm:queue.Class (b), with the contract quoted from the repo's own ADR docblock. ADR-0130 D4 on
ArtifactPackageEntrySchemastates whyplugins/devPluginsare envelope keys: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.⇒
AssembledPackageBodySchemanonetheless carriesfunctions(whoseFlowFunctionEntrySchemaopens with az.function()branch) andhooks(whoseHookSchemacarries az.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/typesserves an EMPTY schema foraction) 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 ispm:dispatchedwithneeds:contract-reviewand is where this was found. ⛔ Do not fold: that card is bound to the read API one layer up, this one movesAssembledPackageBodySchema'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
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. Narrowingfunctions/hooksat the assembled body (or introducing artifact-stage variants ofFlowFunctionEntrySchema/HookSchema) changes whatAssembledPackageBodySchemaaccepts, so the contract-review tier applies andneeds:contract-reviewrides the draft PR from the moment it opens.domain:specexecution seat, PM dispatch, 2026-09-12T15:57Z. The assignee above was set by this seat in the same step as thepm:queue→pm:dispatchedswap, 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:
- finding(spec/runtime):
GET /packagesrows are the ASSEMBLED package body, butInstalledPackageSchematypes them as the AUTHORING manifest — the #14242 stage mismatch, one layer up on the read API #17431 — the card this was found in, and the one the triage comment flags as in flight — isclosed/completed(closed 2026-09-10T21:07:47Z). The fold gate that triage marked as failing outright no longer fails for that reason; it still fails on gate ① (different defect shape, different file) and the round must not fold it. - ArtifactPackageEntrySchema's body half cannot describe the payload the load path actually registers #14242 — the ruling that created
AssembledPackageBodySchema— isclosed/completed(closed 2026-09-02T13:46:07Z). It is an INPUT to read, not a wait, exactly as triage recorded. - metadata-protocol:
/meta/typesserves an EMPTY JSON Schema foraction— the output-mode derivation of itsZodPipehas no properties, and the hand-crafted fallback never fires #17501, spec:retiredKey()tombstones are emitted into a repeater row'sitems.properties, so the served schema offers six columns the parse door refuses #17502 are named by triage as same-family reading, ⛔ NOT as members to fold. spec:retiredKey()tombstones are emitted into a repeater row'sitems.properties, so the served schema offers six columns the parse door refuses #17502 is fenced to another session and this round does not touch it.
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_decisionis a successful outcome for this card, not a failure.
Generated by Claude Code
- finding(spec/runtime):
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 theos-dev-reportcomment 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/specfunction. It buildspackages[i].manifestthroughassemblePackageBody()(packages/spec/src/stack.zod.ts:3502-3509), which copies the authored stack'sfunctionsandhooksverbatim, with no lowering. Measured onorigin/mainated8dea17bd: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 = trueAnd
stack.zod.ts:3489-3491states the invariant that makes that binding, verbatim:{@link AssembledPackageBodySchema}is its declaration and{@link ObjectStackDefinitionSchema}'spackageskey 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/devPluginsfrom 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.
ArtifactPackageandObjectStackDefinitionpublish no JSON Schema becausestack.zod.tsis not one of the 15 subpath namespacesbuild-schemas.tswalks — measured by replicating that walk: 1551 zod exports reached, andAssembledPackageBodySchema,ArtifactPackageSchema,ArtifactPackageEntrySchema,ObjectStackDefinitionSchemaare not among them (lit control:HookSchema,FlowFunctionEntrySchema,AssembledInstalledPackageSchema,ListInstalledPackagesResponseSchemaare). Removing the two branches would not make either file appear.ObjectStackDefinitionSchemahas four unrepresentable members —packages,hooks,functions,onEnable— of which onlypackagesis the assembled body. Repairing the body would not make it emit.- Separately:
HookSchema'sz.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.tsskips their whole schema over an unrepresentablez.date(), and the skip is silent #16431 (a) projection. Running the build's own predicate,Data.HookSchemaEMITS today, projected, pruning#/properties/handler/anyOf/1.FlowFunctionEntrySchemastays 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 itsz.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/specgain 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/hooksasz.unknown(); the next surface re-derives this analysis or copies the override, and "writeunknownon a JSON surface" becomes precedentB declare the inert-JSON stage export the lowered function entry (today a module-local const) plus an artifact-stage body whose hooks[].handlerisz.string(); rebindpackage-api.zod.ts's record body to it2-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.AssembledPackageBodySchemais untouched, socomposeStacksis untouchedC invert the names make AssembledPackageBodySchemathe JSON-only declaration, give the live stage a new namea 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-schemais pushed and carries no commits.
Generated by Claude Code
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
席位复核 — 采信
needs_decision,并独立重取三处承重读数domain:spec执行席,sessionsession_01MkQhmuuJAVDjmeWNixwDDH,复核取数 2026-09-12T16:20Z,读的是origin/main(先git fetch,再git show origin/main:PATH),⛔ 不是轮次的工作树、⛔ 不是本席记忆。⭐ 这一轮是本席派它去测「卡面与分诊哪一个读数被树支持」的,结论是两个都不完全对 —— 卡面说「形状是维护者的选择」,分诊说「#14242 已关 ⇒ 裁决已定、是输入不是等待」;实测的答案是卡面所述的缺陷本身不成立,而它底下另有一个更小、确实属于维护者的问题。⛔ 轮次没有替维护者选边,这是它被要求做的事,不是它没做成的事。
本席逐条重取的三处承重读数(轮次的其余读数见
os-dev-report,本席采信但未逐条重跑):-
packages/spec/src/stack.zod.ts的不变量句,逐字核对一致:{@link AssembledPackageBodySchema}is its declaration and{@link ObjectStackDefinitionSchema}'spackageskey parses against it, so a body this function builds and a body the load path accepts cannot drift.⇒ 卡面方向 (a)(在 assembled body 上收窄那两个集合)会让一个已发布组合函数自己的输出被同一个文件声明「必须接受它」的 schema 拒收。
-
packages/spec/src/data/hook.zod.ts的handler文档块,逐字核对一致:「objectstack buildautomatically lowers inline functions to the string form … The JSON artifact therefore only ever contains the string form.」 -
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
-
49 remaining items
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 19373sees 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_TIER92 any claude-(opus|sonnet|haiku)id0 dark control, an id that cannot exist 0 total "model":occurrences94 ⚠️ 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
functionsspellings? 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-265writesout[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 artifactobjectstack buildwrites 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 ofFlowFunctionEntrySchema, 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,patchorminor? The review sayspatchis correct under the written rule: no export is added, removed or renamed; the payload gains entries the previousz.unknown()declaration already permitted, so no consumer could have relied on their absence;AGENTS.md:1064-1068assigns a bug fix in a released packagepatch. It adds thatminorwould not be an error underlanes/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--checkexited 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-refsis green and the net is 0 tokens — but because theExports:fallback lists the first five non-constant exports in source order (packages/spec/scripts/lib/export-list.ts,slice(0, 5)), thestack.zod.tspointer row now names the two new stage schemas and no longer namesArtifactPackageSchemaorObjectStackDefinitionSchema— 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 onstack.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 aac764cc36113b4e52820c1695715f000ccbe1b4checks on that head 35 runs — 33 success, 2 skipped, 0 adverse mergeability, driver-free, vs origin/mainc5d3d1d8b69exit 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-reviewleft 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 19373exit 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_APPROVERSapproval 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
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_APPROVERSapproval lands. This is the same route #19363 took today (approved, then merged byos-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 itsstack.zod.tsrow and no longer listsArtifactPackageSchemaorObjectStackDefinitionSchema. That is the generator'sslice(0, 5)over source order (packages/spec/scripts/lib/export-list.ts), not anyone's edit, andcheck:skill-refsis 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 onstack.zod.ts, in a separate change, would stop the row depending on export order.
Generated by Claude Code
维护者直派 — resuming this card to drive PR #19373 to MERGED
Authorization, quoted verbatim and untranslated per 裁决引文照抄不译. Given by the maintainer to
session_01LvwGppdonww4zGLWZo5rhoin this seat's session at 2026-09-22T06:41Z:「19373 冲突了,你负责跟进到合并」
⚠️ Provenance, stated because this seat is not the assignee. This card's assignee readsos-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
APPROVEDreviews byos-zhuangare on record — 2026-09-20T23:25Z and 2026-09-21T02:08Z — and the PR has been flipped out of draft. That is the routecheck-governed-merges.mjsnames 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_APPROVERScarries no literal roster in the tree —git grepfinds the name only in prose and in the tier table, never bound to a list — so this seat cannot mechanically verifyos-zhuangis 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--sharedclone withcore.attributesfile=/dev/nulland nomerge.os-regen.driverregistered (git config --getexits 1 there), which is GitHub's own condition:probe reading head aac764cc361vsorigin/main97f4f8c8282exit 1 — conflicts on content/docs/references/index.mdxandpackages/spec/dropped-refinements.baseline.jsondark control — main's own first parent vs main exit 0 ⇒ the instrument does not conflict on everything distance origin/mainis 128 commits past this head's merge base4b58dcf96b3Both conflicts are on recomputable artifacts, ⛔ neither is semantic:
content/docs/references/index.mdx—merge=os-regenrouted (.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-runspnpm --filter @objectstack/spec buildand re-applies the delta it prints. ⛔ Not a semantic collision.」
⚠️ The risk this seat cannot measure, named before actingResolving 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/protectionanswers 403Resource not accessible by integrationthrough the available channel, so it is NOT MEASURED, ⛔ not assumed either way.⇒ The fix proceeds regardless, because a
dirtyPR 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
- Dispatch a merge round to an
os-devworktree (⛔ PM writes no code): mergeorigin/main, resolve both paths by regeneration rather than by hand, ⛔ do not runscripts/pm/os-regen-merge.sh— itsrerunre-entrancy is filed asos-regen-merge.sh: when step 3's commit is refused the record stayspending, so a later run re-entersrerunand 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. - 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_TIERis owed on the new head, tier verified from the reviewing round's harness stamps. - 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
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
LANDED — PR #19373 is MERGED
Read on
origin/mainat 2026-09-22T08:11Z, aftergit 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
completedby GitHub's ownFixes #17518at 2026-09-22T08:10Z;pm:dispatchedremoved in a four-steplabel-write.mjswrite that read backbug,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'smergedfieldprobe count role (#19373)1 the subject (#19315)1 lit control (#99999)0 dark control The same instrument read
(#19373) = 0with the identical controls on every poll from 07:44Z to 08:09Z, so the 1 is an arrival rather than a broken probe.⚠️ auto_mergeread 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:- ⭐ 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.mdxtext-merges cleanly driver-free — so no GitHub-condition probe can ever name it — yet the localos-regendriver deferred it with exit 0 and dropped main's side (merged blob988bedaa480== ours, != theirsd09cd420711). 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. dropped-refinements.baseline.jsonhas 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.- ⛔
scripts/pm/os-regen-merge.shwas not run in either round — itsrerunarm 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 stayspending, so a later run re-entersrerunand 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_TIERother tiers aac764cc3615751097514 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.keyTypesites 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 standingAPPROVEDreviews fromos-zhuang(5262113029,5262584525) perAGENTS.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
- Two of the four
measuredcounters indropped-refinements.baseline.jsonhave NO reader anywhere — they can hold any number and every gate stays green, on a ledger whose whole purpose is to end a silence #19681 — two of the fourmeasuredcounters indropped-refinements.baseline.jsonhave no reader anywhere, so they can hold any number with every gate green. Not hypothetical: this merge left one at 357 while the build measured 366, and only a human reading the build log caught it. Both reviews concur. - C9 has no reachable repair when the earlier claim holder is a dead dev session — and
check-clause2-carriers.mjs:2095andSKILL.md:496prescribe opposite acts for exactly that case #19400 — C9 has no reachable repair here, so--paircannot reach 0 on this card. Carried openly into the landing rather than worked around; the full statement is in 5751042169 and the provenance comment 5772904040.
Generated by Claude Code
- squash
- added 5 commits that reference this issue
on Sep 28, 2026
os-decision-facets
The question: what shape does the REGISTRY RECORD stage take?
⛔ Not a dev's choice: it changes the payload
GET /packagesemits, lands in@objectstack/objectql— another published package, with its own Clause-② — andtoRecordManifest'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.buildmints (lower-callables.tsusesuniqueName(base, taken)and dedupes by function identity) ⇒ a record could assert a ref that resolves in no sibling moduleeffectdeclaration, the one half that survives todaypm:blockedbehind it四棱
① 项目长远合理性 — 三个声明过的阶段(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 whypluginsanddevPluginsare envelope keys that no package body may carry:AssembledPackageBodySchema— the body half ofArtifactPackageSchema, 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 ofAssembledPackageBodySchema.shape:and, on the whole body:
AssembledPackageBodySchemaFunction types cannot be represented in JSON Schema.omit({ functions: true })Custom types cannot be represented in JSON Schema.omit({ hooks: true })Function types cannot be represented in JSON Schema.omit({ functions: true, hooks: true })That is why
ArtifactPackageandObjectStackDefinitionpublish no JSON Schema at all, and it propagates: any published export that embeds the assembled body loses its own JSON Schema and itscontent/docs/references/**page with it. #17431 hit exactly that — binding the body intoListInstalledPackagesResponseSchemaandGetInstalledPackageResponseSchemamade both disappear fromjson-schema/api/, whichbuild-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