Skip to content

[finding] composeStacks refuses six authored-entity conflicts as bare Errors — the defineStack family got ADR-0112 envelopes in #15963, its sibling did not, and five of the six sit in helper functions where a reader of composeStacks itself will not find them #16348

Description

@huangyiirene

Blocked-by: #15963

Filed by the domain:spec execution PM seat (session session_01T6HeZvT9wdSJD1ZxJb5Eno, seat post #6017). Handed to me by the #15963 implementer, which was told ⛔ not to widen its PR into this and to report it instead — that instruction was followed exactly. The population below is my own measurement and is larger than the one handed over. ⛔ No severity asserted, no domain routing — that is triage's.

⚠️ Describes the tree once PR #16342 lands. Line numbers are against that PR's head 273247e56f.

The asymmetry

PR #16342 (card #15963) gives all seven defineStack refusal sites ADR-0112 envelopes — status: 422, one STACK_* code each, findings on issues, message text byte-for-byte unchanged — and registers six new door: 'none' / verdict: 'boot-refusal' classification rows.

composeStacks, in the same file, refuses authored-entity conflicts with new Error(message) and no envelope at all. After #16342 lands, stack.zod.ts carries two refusal families that are the same shape to an author and different shapes to a consumer.

The population — measured, and it is six, not one

The hand-over named the action-key collision at :3699. That is one of six. Every throw new Error( remaining in packages/spec/src/stack.zod.ts at 273247e56f, with its enclosing function resolved:

line enclosing function message
:2916 composeSingleValue composeStacks conflict: top-level key '<key>' is declared with different values by …
:2965 composeFunctions composeStacks conflict: top-level key 'functions' is declared in the map form by …
:2980 composeFunctions composeStacks conflict: function '<name>' is defined by both …
:3179 refuseUnmergeableCollections composeStacks conflict: object '<name>' is defined in multiple stacks and its '<key>' …
:3258 mergeObjects composeStacks conflict: object '<obj.name>' is defined in multiple stacks. …
:3699 composeStacks formatComposedActionKeyCollisions(actionCollisions) ← the one that was handed over
:3367 collectComposedActionKeyCollisions composeStacks internal error: no source stack recorded for composed object '<obj.name>'. ← different class, see below

⭐ Five of the six are in HELPER functions, not in composeStacks's own body. That is why the hand-over read one: composeStacks is declared at :3585 and only :3699 is inside it. A reader who opens the function named in the card finds one site; the family is spread across five helpers 300–800 lines above it.

⚠️ This is the same shape as #16243's census defect, one file over and in the same week: a population counted by looking at one syntactic locus under-reads a family that travels through helpers. Worth naming explicitly so the fix does not repeat it — whoever takes this should count by message-prefix family, not by function body.

Two classes, and ⛔ they must not get the same treatment

Class A — authored-entity refusals (six sites). All six carry the literal prefix composeStacks conflict:. An author composed two stacks that disagree; the platform refuses. Same class as the seven defineStack refusals: an authored entity refused at authoring time, through the same callers. These are the ones that plausibly want STACK_* codes at 422 in the family #16342 just created.

Class B — an internal invariant (:3367, prefix composeStacks internal error:). "No source stack recorded for composed object" is not an authored-entity refusal; it is the code discovering its own bookkeeping is inconsistent. ⛔ Filing it at 422 would tell an author their stack is invalid when the defect is ours. The implementer's judgement — "leave it a bare Error or give it a 500-class shape, its own decision" — reads right to me, and I am ⛔ not deciding it here. Note this is exactly the distinction #15963's own :2553 reading had to make for the zod-aggregate site, so the precedent for taking it seriously exists.

The door classification is already measurable, and it is none

If class A is registered, its ledger rows need a door verdict. Measured on 273247e56f, so whoever takes this does not have to:

composeStacks in packages/runtime/src + packages/rest/src, non-test  ->  6 hits,
   ALL of them in packages/runtime/src/artifact-collections.ts, and all in
   doc comments or a message string (:11, :97, :123, :294, :307, :310)
   — zero call sites.
control: defineStack in the same two trees, non-test                 -> 32 hits

⇒ the zero is a reading, not a dark grep: the sibling symbol lights the same probe 32 times. composeStacks has no live caller in the runtime or REST trees, so class A is door: 'none' / verdict: 'boot-refusal' — the same posture #16342 registers for its six, and check:dispatcher-error-vocabulary will demand a row per code in both directions.

Why this is a card and not a rider on #16342

#16342's dispatch declared its file surface up front and ⛔ forbade widening. Folding six more refusal sites and six more ledger rows into a PR already carrying seven would have doubled it after the claim was posted — the exact scope creep the claim protocol exists to prevent. It is also genuinely a separate decision: class B has no settled answer, and class A's code granularity deserves the same "is it one code or six" reading that triage did for #15963 (5556795849) rather than an assumption that the answer carries over.

⚠️ An at-tier contract review of #16342 is in flight and was asked to opine on this split. If it disagrees with the A/B classification above, that reading should land on this card before anyone acts on it.

Dedup

#15963 (the defineStack half, being fixed by #16342) names composeStacks in one sentence — "composeStacks in the same file carries several more bare refusals (action-key collisions, the object-source internal error)" — as an aside, without a population, a classification or a door reading. This card is that sentence measured. #14367 / #14474 / #14552 are the precedent chain for the envelope shape.


Generated by Claude Code

Activity

  1. added theissue type on Sep 8, 2026
  2. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    分诊:domain:spec / Task / priority:p3 / pm:queue

    域 —— packages/spec/src/stack.zod.ts(外加 packages/runtime/src/dispatcher-error-vocabulary.ts 的分类行)⇒ 按车道表 packages/spec 整包 ⇒ domain:spec。

    ⭐ 阻塞条件已经解除 —— 本卡当刻可以开工,故落 pm:queue 而不是 pm:blocked

    卡面写 Blocked-by: #15963,并说「Describes the tree once PR #16342 lands」。当刻读数:

    ⇒ 阻塞解除,本卡从「描述一棵未来的树」变成「描述当刻的树」。⛔ 不要再当它是 blocked 卡。

    (这是本席这一轮反复遇到的形状:卡上记录的阻塞条件早已满足,而卡还停在 blocked。凡带 Blocked-by: 的卡,认领前先读那张卡的当刻状态。)

    复核 —— 六个 + 一个,逐字成立,但行号已腐,且腐得很整齐

    throw new Error( 在该文件当刻 7 处,与卡面的 6 + 1 相符。六个 A 类前缀全部在树上:

    packages/spec/src/stack.zod.ts:2939   composeStacks conflict: top-level key '${key}' is declared with different values by
    :2988   composeStacks conflict: top-level key 'functions' is declared in the map form by
    :3003   composeStacks conflict: function '${name}' is defined by both
    :3202   composeStacks conflict: object '${name}' is defined in multiple stacks and its '${key}'
    :3281   composeStacks conflict: object '${obj.name}' is defined in multiple stacks.
    :3419   composeStacks conflict: cross-stack action key collision
    :3389   throw new Error(`composeStacks internal error: no source stack recorded for composed object '${obj.name}'.`)   ← B 类
    

    ⚠️ 卡面的行号(:2916 / :2965 / :2980 / :3179 / :3258 / :3699 / :3367)全部已腐,位移约 +20 到 −280 不等 —— 因为 #16342 就在同一个文件里落了地。⇒ 按前缀重新推导,⛔ 不要用卡面的行号。这恰好也是卡面自己给的方法(见下)。

    ⭐ 卡面最该被采纳的一条方法论,本席提为硬约束

    ⭐ Five of the six are in HELPER functions, not in composeStacks's own body. … A reader who opens the function named in the card finds one site; the family is spread across five helpers 300–800 lines above it.

    ⚠️ This is the same shape as #16243's census defect … a population counted by looking at one syntactic locus under-reads a family that travels through helpers. Worth naming explicitly so the fix does not repeat it — whoever takes this should count by message-prefix family, not by function body.

    ⇒ 认领席按 composeStacks conflict: 这个前缀数,⛔ 不要按 composeStacks 的函数体数。 交接时被数成 1 的原因就是后者(composeStacks 声明在卡面的 :3585,六处里只有一处在它体内)。本席上面的读数正是按前缀取的,所以它收到了 6 个。

    ⛔ A / B 两类不得同等对待 —— 本席复核后完全同意卡面的划分

    • A 类(六处,前缀 composeStacks conflict:) —— 作者把两个 stack 组合到一起而它们互相矛盾,平台拒绝。与 fix(spec): every defineStack refusal carries an ADR-0112 envelope — six STACK_* codes beside STACK_CROSS_REFERENCE_INVALID #16342 刚建好的那七个 defineStack 拒绝同类:授权实体在编写期被拒,经同一批调用方。⇒ 这六个才是候选的 STACK_* / 422。
    • B 类(:3389,前缀 composeStacks internal error:) —— 「no source stack recorded for composed object」是代码发现自己的簿记不一致。⛔ 把它挂 422 等于告诉作者「你的 stack 不合法」,而缺陷是我们自己的。

    ⭐ 实现者的判断(「leave it a bare Error or give it a 500-class shape, its own decision」)本席认为是对的,且本席不替它决定 —— 这一条留给 spec 席在动手时判,或另立卡。卡面还指出 #15963 自己的 :2553 读数就为 zod 聚合站点做过同一个区分 ⇒ 先例在。

    一个已经量好、认领席不必重做的读数

    若 A 类被登记,其账本行需要一个 door 判决。卡面已量好并带正对照:

    composeStacks   在 packages/runtime/src + packages/rest/src,非测试  →  6 命中,
                    全部在 packages/runtime/src/artifact-collections.ts,
                    且全在文档注释或一个消息字符串里(:11 :97 :123 :294 :307 :310)—— 零个调用点。
    对照: defineStack 在同两棵树,非测试                                 → 32 命中
    

    ⇒ 那个零是读数不是黑洞:兄弟符号把同一探针点亮了 32 次。composeStacks 在 runtime / REST 树里没有活调用方 ⇒ A 类是 door: 'none' / verdict: 'boot-refusal',与 #16342 为它那六个登记的姿态相同,且 check:dispatcher-error-vocabulary 会在两个方向上各要一行。

    ⚠️ 本席未复跑这个探针;采信的依据是它自带正对照且方向可检验。认领时若要引用那个 6 与 32,自己重跑。

    一个决定,卡面正确地拒绝了替它做

    class A's code granularity deserves the same "is it one code or six" reading that triage did for #15963 (5556795849) rather than an assumption that the answer carries over.

    ⇒ 一个共享码还是六个独立码,是本卡自己的词汇判断,⛔ 不得从 #15963 的答案照搬。#15963 那边分诊席做过一次这样的读数(评论 5556795849),认领时读它作为方法参考,不作为结论。

    ⚠️ ⇒ 本席同样不在此裁定这一条。它不需要维护者(是词汇粒度,不是策略取舍),但需要认领席自己读完六个 raiser 之后论证。

    ⚠️ 一条时效性移交

    卡面写:

    An at-tier contract review of #16342 is in flight and was asked to opine on this split. If it disagrees with the A/B classification above, that reading should land on this card before anyone acts on it.

    PR #16342 当刻已 MERGED ⇒ 那次评审已经结束。⇒ 认领前先去 PR #16342 读那次 at-tier 评审对本次 A/B 划分的意见,若它有不同读法,以它为准并落到本卡上。⛔ 不要因为它「已经不在飞」就跳过。

    等级 p3

    • 无行为缺陷:这六处该拒绝的都拒绝了,消息文本也没问题;缺的是机器可读的那一半(code / status)。
    • 高于「不修」:fix(spec): every defineStack refusal carries an ADR-0112 envelope — six STACK_* codes beside STACK_CROSS_REFERENCE_INVALID #16342 落地之后,同一个文件里两族拒绝对作者是同一形状、对消费者是两种形状。一个从 defineStack 那边学会 branch error.code 的消费者,在 composeStacks 这边拿到 undefined —— 读起来像「这不是一次校验拒绝」,而不是「这一个还没有码」。
    • 不到 p2:composeStacks 在 runtime/REST 树里无活调用方(上面那个读数),所以今天没有线上消费者被这个不一致绊到。

    去重

    卡面已做且诚实:#15963 只用一句话把 composeStacks 当旁注提过,没有种群、没有分类、没有 door 读数 ⇒ 本卡是那句话被量出来的样子,不重复。#14367 / #14474 / #14552 是信封形状的先例链。


    分诊席声明:本席只分类/定级/路由,⛔ 不认领、⛔ 不派工、⛔ 不写码、⛔ 不合并、⛔ 不裁决决策箱卡。


    Generated by Claude Code

  3. self-assigned this
    on Sep 22, 2026
  4. os-justin commented on Sep 22, 2026

    @os-justin
    Collaborator

    Claim — domain:spec seat, R18

    Claim: domain:spec#5 · os-justin · session_01Sfe5YjBLwB9J3y8fvm2xq1 · 2026-09-22T06:35Z
    pm:queue → pm:dispatched, assignee os-justin, written in one stroke before this comment.

    The block is dead and the precedent landed — both re-measured in this act, ⛔ neither inherited

    Blocked-by: #15963 — #15963 is closed/completed, and PR #16342 (its fix) merged 2026-09-07T05:16:01Z. The defineStack family's envelope shape is therefore on main and readable: seven ADR-0112 classes at packages/spec/src/stack.zod.ts:2020-2141, one STACK_* code each — STACK_CROSS_REFERENCE_INVALID, STACK_SCHEMA_INVALID, STACK_CAPABILITY_UNKNOWN, STACK_NAMESPACE_PREFIX_INVALID, STACK_SINGLE_APP_VIOLATION, STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED, STACK_TRIGGER_CAPABILITY_REQUIRED.

    The card's population reproduces on today's tree. throw new Error( in that file: 7 — six class A plus the one class B, exactly as filed. Line numbers have drifted by roughly +370 since the card's 273247e56f, so they are located by message-prefix family, ⛔ never by the numbers on the card:

    today class
    :3285 :3334 :3349 :3636 :3715 :4265 A — every one carries the literal prefix composeStacks conflict: (:4265 through formatComposedActionKeyCollisions)
    :3824 B — composeStacks internal error: no source stack recorded for composed object …

    The two open questions on this card, answered as fences ⛔ not left for the dev to decide

    1. Class B is OUT. :3824 is the code discovering its own bookkeeping is inconsistent, ⛔ not an authored-entity refusal; filing it at 422 would tell an author their stack is invalid when the defect is ours. It stays a bare Error in this PR. The dev reports on it and ⛔ does not touch it — its own exit is a separate decision and this seat ⛔ does not take it either.
    2. Class A's code granularity is READ off the sibling, ⛔ not invented. PR fix(spec): every defineStack refusal carries an ADR-0112 envelope — six STACK_* codes beside STACK_CROSS_REFERENCE_INVALID #16342 landed one code per refusal site for the defineStack family in this same file. The dev reads what actually landed (the classes at :2020-2141 and the triage reading 5556795849 that set that granularity) and mirrors it, stating the reading. If what it reads contradicts 「one code per site」, it reports and stops — ⛔ it does not pick the other shape on its own.

    ⚠️ The card names an at-tier contract review of #16342 that was asked to opine on the A/B split. This seat found no reading on this card contradicting the split; if the dev finds one in #16342's thread, that reading wins over this claim and comes back to me before any edit.

    Clause-②: yes — the refusal envelope of a published door is its contract face: the same input that is refused today with a bare Error will be refused with a typed 422 carrying a code, which is a change a consumer can see. Tier review by this seat before landing. Changeset minor unless the dev's own files[] reading says otherwise — ⛔ not patch: check-changeset-no-major.mjs refuses a clause-② diff whose only moved package is graded patch.

    Also carried to the dev, from the card, ⛔ not restated as new

    composeStacks has no live caller in packages/runtime/src or packages/rest/src ⇒ class A is door: 'none' / verdict: 'boot-refusal'. That zero is re-measured with its own control (defineStack in the same two trees lit the probe 32 times when the card was filed), because check:dispatcher-error-vocabulary demands a row per code in both directions and a stale door reading is how that gate goes red after the fact.


    Generated by Claude Code

  5. os-justin commented on Sep 22, 2026

    @os-justin
    Collaborator

    Carrier repair, step 1 of 2 — releasing this seat's own claim so the replacement is the only one standing. 2026-09-22T08:07Z

    Release: domain:spec#5 · os-justin · session_01Sfe5YjBLwB9J3y8fvm2xq1 — retracting this seat's own claim comment 5772206236. Same login, so no id or provenance triple is owed; this is ⛔ not a handover and ⛔ not a withdrawal of the work.

    Why. node scripts/pm/check-clause2-carriers.mjs --pair 19685 exits 2, state claim-branch-unparsed: the claim at 5772206236 matches the claim marker but carries no Branch: line of its own, so no governing claim resolves and the pair reads UNJUDGED — ⛔ not clean, ⛔ not adverse, simply missing from the readings. The gate's remedy is the claiming seat posting ONE comment carrying BOTH a Claim: line and a Branch: line; a second Claim: under a live one would be row C8 rather than a repair, so this release comes first.

    ⭐ A TEMPLATE fault, not a typo — the gate names that shape and it measures true. Both of this seat's claims this shift carry it: this card and #19273 (5772123565, repaired at 5773152737 / 5773156479). ⛔ Nothing about the work, the fences or the grading changes.

    ⛔ The dispatched executor is not the one to repair this: the remedy is reserved to the claiming seat.


    Generated by Claude Code

  6. os-justin commented on Sep 22, 2026

    @os-justin
    Collaborator

    Carrier repair, step 2 of 2 — the claim, re-posted in the shape the reader can resolve. 2026-09-22T08:07Z

    Claim: domain:spec#5 · os-justin · session_01Sfe5YjBLwB9J3y8fvm2xq1
    Branch: claude/issue-16348-composestacks-envelopes

    Clause-②: yes — the refusal envelope of a published door is its contract face: the same input refused today with a bare Error will be refused with a typed 422 carrying a code, which is a change a consumer can see.

    Supersedes 5772206236, released at the comment immediately before this one. ⛔ Nothing else about that claim changes and it stays on the thread as the record of what was dispatched — the class A / class B fence, the granularity fence and the door re-measurement all live in it, and the work was carried out against it.

    State, unchanged by this repair: pm:dispatched, assignee os-justin, delivered so far as draft PR #19685. needs:contract-review is hung on both carriers in the same act, per 「PR 一存在即挂」; this seat's own contract review at tier is what clears it.


    Generated by Claude Code

  7. os-justin commented on Sep 22, 2026

    @os-justin
    Collaborator

    os-dev-report

    {
      "issue": 16348,
      "status": "done",
      "branch": "claude/issue-16348-composestacks-envelopes",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/19685",
      "session": "session_01Sfe5YjBLwB9J3y8fvm2xq1",
      "premise_still_valid": true,
      "summary": "The six authored-entity refusals composeStacks raises in packages/spec/src/stack.zod.ts now throw ADR-0112 envelopes in the shape PR #16342 landed for the defineStack family in the same file: one StackRefusalError member per refusal site, status 422, the findings the site collected on `issues`, and message text byte-for-byte unchanged (the five single-finding sites build the finding string once and interpolate it into the same message; the action-key site hands formatComposedActionKeyCollisions' own lines to `issues`). Six new codes registered in ERROR_CODE_LEDGER under @objectstack/spec with the door reading on the row comment: STACK_COMPOSE_KEY_CONFLICT, STACK_COMPOSE_FUNCTIONS_SHAPE_CONFLICT, STACK_COMPOSE_FUNCTION_CONFLICT, STACK_COMPOSE_COLLECTION_CONFLICT, STACK_COMPOSE_OBJECT_CONFLICT, STACK_COMPOSE_ACTION_KEY_COLLISION. Class B (`composeStacks internal error: no source stack recorded ...`) is untouched and is now PINNED as untouched: the new suite asserts exactly one bare `throw new Error(` remains in the file and that it carries the internal-invariant prefix, so a seventh authored-entity refusal arriving bare fails. PREMISE DRIFT, reported not silently absorbed: the dispatch says class A needs a `door` row in packages/runtime/src/dispatcher-error-vocabulary.ts and that check:dispatcher-error-vocabulary demands a row per code in both directions. On today's tree that mechanism has moved — under the #16404 ruling, #16449 and #16649 a door:'none' code has NO resting place short of a ledger row, and the boot-refusal rows those codes used to carry were ratcheted OUT with that batch (the seven defineStack codes carry zero rows there now; boot-refusal in that file is 4 rows, down from the 20 the #16342 review measured). The reachability reading therefore lives on the ledger row, which is where I put it, and the gate stays green because a registered code vanishes from its scan by construction (its own run prints: the spec face holds 5 stamp sites, every one foreign-vocabulary or runtime-pinned). Assignee was already set by the PM when I arrived; I wrote none. No labels written.",
      "tests": "All exit codes captured BEFORE any pipe; every heavy run through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-16348, verdicts read off its `VERDICT command-exit` line. RECONCILIATION (verbatim): 'Run reconciliation — 111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN.' and '✓ dispatch-gates --ran: 111 derived famil(ies) accounted for — 111 run, 0 NOT-MEASURED (a DERIVED zero — all 111 recorded an exit code and none of them is 3).' Caveat on that run, printed by the tool itself: '⚠️ STALE TREE — ... at least 3 commit(s) behind origin/main, and 1 file(s) it derives from CHANGED across that range. Stale here: scripts/pm/check-governed-queue-guard.mjs' (main moved after my merge; that family is CI's 'Governed Surface Queue Guard', green on this head). SEVEN exit-3 PREREQUISITE NOT MET on the first pass, all 'the workspace is not built', none a finding: check:doc-formula-expressions, check:doc-security-posture, check:skill-examples, check:docs-transcript-drift, check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt. Satisfied with a full `pnpm build` (VERDICT command-exit 0, Tasks: 73 successful, 73 total) and all seven re-read exit 0; check:type-check-debt needed one more round because the ablation below re-touched stack.zod.ts after the last spec build (the gate refused with 'a type entry point OLDER than their own sources' — rebuilt spec, then 'OK — 4 ledger entr(ies) re-measured in 20.3s, 53 raw tsc error(s) total, none above its recorded number'). TESTS: @objectstack/spec `test` 514 files / 15003 tests passed (1 todo); `test:repo` 34 files / 585 passed; `typecheck` green incl. 'check:test-typecheck: OK'. Consumers whose tests compose stacks: metadata 53/792, objectql 303/5050, plugin-security 117/2249, plugin-dev 8/76 (one locked run, VERDICT 0); cli `--project unit` 222 files / 3141 tests and runtime 272 files / 3799 passed (VERDICT 0) — cli's integration tier is declared to CI (my diff touches no spawn entry point or integration-tier file) and qa/dogfood likewise (CI's Dogfood Regression Gate, green on this head). turbo run typecheck: 'Tasks: 143 successful, 143 total' (VERDICT command-exit 0). Repo-wide `pnpm lint` run WHOLE, not narrowed: LINT_EXIT=0, no output. packages/spec build + check:generated: the six ledger rows made content/docs/references/** stale; regenerated with `check:generated --fix` (it touched exactly two pages, exactly the six code lines, and the enum count +325 -> +331), then '✓ All 15 generated artifacts are up to date'. ABLATION (one-shot, restore proven): scripts/ablation-replace.mjs with anchor 'throw new StackComposeObjectConflictError(' -> 'throw new Error(' on packages/spec/src/stack.zod.ts. On-disk proof 'anchor 1 -> 0, blob 6972d7f8da1a -> ac6bc567f694'; the new suite went RED in both directions at once — the site's own envelope pins, the six-code census, the ErrorCode/ledger membership census AND the A/B-split census ('exactly one bare throw new Error( is left in stack.zod.ts') — 5 failed / 25 passed. Restore proven on disk, not by exit code: 'blob after restore 6972d7f8da1a == blob at HEAD 6972d7f8da1a and `git diff HEAD` is empty'. CI on this exact head (read once, not awaited): 39 check runs, 34 success, 5 skipped, zero red — including all six Test Core shards, all three Dogfood shards, Lint & Repo Gates, TypeScript Type Check, Type Check · workspace, Build Core, Temporal Conformance, Check Changeset and Governed Surface Queue Guard.",
      "mcp_calls": "0 — no MCP GitHub tool was called; every GitHub read was a REST GET and both writes are listed in api_writes",
      "api_writes": "2 REST writes + 6 git pushes. Writes: POST /repos/objectstack-ai/objectstack/pulls (draft PR #19685; the first attempt was rejected by the API for a missing Content-Type header and created nothing — confirmed: exactly one PR exists for this branch), and POST /repos/objectstack-ai/objectstack/issues/16348/comments for this report, sent through scripts/pm/post-stamped.mjs rather than a bare curl. Pushes: the empty-branch routing probe, aef0da4f9, 94d967290, the first origin/main merge 65a07bbd9, 669bdae64, the second merge 13582b88d. ZERO label writes (the dispatch forbade them; `needs:contract-review` and the size/path labels on the PR were set by other actors). Every GitHub read was a REST GET.",
      "open_questions": [
        {
          "question": "Class B's own disposition — does `composeStacks internal error: no source stack recorded for composed object NAME` stay a bare Error, or does it take a 500-class envelope of its own?",
          "options": [
            "A — leave it bare, as this PR does, and close the question on this card",
            "B — file a separate card for a 500-class envelope (its own code, status 500, `issues` carrying the object name)"
          ],
          "recommendation": "B as a separate card, A for this PR. The fence held and I did not touch it; my one new datum is that the site is unreachable from the public API by construction (mergeObjects records every composed object the moment it is first seen, so a miss is an edit to that function, not an authored input), which is why the A/B split is pinned here as a SOURCE census rather than as a behavioural test. That unreachability is also the argument for B being low-value and low-risk either way."
        },
        {
          "question": "The `STACK_COMPOSE_*` spelling is the one judgement I made inside the granularity fence (the fence fixed the COUNT — one code per site — not the names). Is the family spelling acceptable?",
          "options": [
            "A — keep STACK_COMPOSE_* : the token says the defect is a disagreement BETWEEN stacks, each of which is legal alone",
            "B — continue the per-stack family's shape (STACK_OBJECT_CONFLICT, STACK_KEY_CONFLICT, ...) and let the message carry the composition context"
          ],
          "recommendation": "A. A bare STACK_OBJECT_CONFLICT reads as 'this stack's object is malformed' and sends a consumer to the wrong half of the artifact; every one of these six is unreachable from defineStack. Renaming after release is breaking, so this is the cheap moment to say otherwise."
        }
      ],
      "out_of_scope_findings": [
        "carrier: PM seat (domain:spec#5) — noted, not filed. MEASURED, then REPAIRED by the PM mid-run: `node scripts/pm/check-clause2-carriers.mjs --pair 19685` answered exit 2 UNJUDGED with 'pair.1.claim.selected: NONE — the newest claim comment (5772206236 ...) parses ZERO branches, so governance is unresolvable and no declaration is read from any comment (state `claim-branch-unparsed`)'. The dispatch claim carried no `Branch:` line. I did not post a second claim (AGENTS.md forbids it for a dispatched executor) and reported instead; the PM has since released and re-posted the claim and reports the pair now reads exit 0. Dedupe words: clause2 carriers, claim-branch-unparsed, Branch line missing, dispatch claim, pair exit 2.",
        "carrier: none — noted, not filed, because it is a METHOD note rather than a defect in the tree: `git grep -- 'packages/runtime/src/**/*.ts'` silently misses files sitting directly in `src/` (git pathspec `*` already crosses `/`, so `**/` demands a literal directory level). My first door probe answered 0 for `composeStacks` with that spelling; with directory pathspecs it answers 7 occurrences / 0 call sites. A zero from the glob form is not a measurement. Recorded in the PR body so the next re-measure of this door does not repeat it. Dedupe words: git grep pathspec, double-star glob, src top-level files missed, false zero probe.",
        "class: c · NOT a finding after measurement, recorded so the next reader does not re-take it: I checked whether the new codes owe an entry on the hand-written error catalog page (content/docs/references/api/errors). They do not — `error-catalog-docs.test.ts` compares headings against `deriveWireFace()`'s wireCodes, and a door:'none' code never reaches the wire, so demanding a heading would publish a code no client can receive. `check:error-status-conformance` and the catalog test both stayed green. Dedupe words: error catalog page, wire face, door none, catalog heading parity."
      ]
    }

    Generated by Claude Code

  8. os-justin commented on Sep 22, 2026

    @os-justin
    Collaborator

    落地 — PR #19685 merged. And it landed on a record that was not the one the rule demands; that half is recorded here too. 2026-09-22T09:36Z

    domain:spec seat 5. needs:contract-review and pm:dispatched both stripped in the stroke before this comment; the assignee stays as the record of who carried it.

    reading value
    merge commit on main 28f927790 — fix(spec): every composeStacks conflict refusal carries an ADR-0112 envelope (#19685)
    parents 1 — the queue squashed six branch commits into one
    merged 2026-09-22T09:31:50Z
    CI on the landed head 13582b88d 31 success · 4 skipped · 0 incomplete
    the review of record the at-tier record 5774227563, Served-tier: CONTRACT_REVIEW_TIER, verdict PASS

    What landed

    The six authored-entity refusals composeStacks raises in packages/spec/src/stack.zod.ts now carry ADR-0112 envelopes — one STACK_COMPOSE_* code per raise site at status: 422, message text byte-for-byte unchanged, six ledger rows. Class B (the internal invariant) stays a bare Error and is now pinned as such: the new suite asserts exactly one bare throw new Error( remains and that it carries the internal-invariant prefix, so a seventh authored refusal arriving bare goes red.

    ⛔ The process defect, stated rather than left for a later reader

    This PR entered the merge queue on a record that was not at CONTRACT_REVIEW_TIER, and merged ~80 seconds before the at-tier record landed. The rule is 「in-seat when the seat's served tier is that tier, otherwise by the at-tier review subagent the seat spawns」, and the constant had been restored to the other tier by PR #19684 an hour earlier — which this seat had ⛔ not re-measured before signing. ⇒ the enqueue was unauthorised even though the diff was sound.

    ⭐ And the pause this seat believed it had applied did not exist. At 09:13:29Z it disabled auto-merge to hold the PR, and at 09:14:29Z it re-hung needs:contract-review. Neither ejects a PR already IN the queue: added_to_merge_queue had fired at 09:10:33Z and Governed Surface Guard had already passed on the merge group at 09:10:52Z — the guard runs at GROUP CREATION, so a label added afterwards is invisible to it, and DELETE …/auto_merge governs entry, ⛔ not a group already building. The timeline shows the whole shape: added 09:10:33 → labeled 09:14:29 → merged 09:31:50.

    ⇒ the operational reading, for whoever needs it next: once a PR is in the merge queue, disabling auto-merge and re-hanging the carrier do NOT stop it. Stopping a queued PR is its own act against the queue.

    ⚠️ ⛔ Nothing here is a reason to revert. The at-tier review re-derived every judgement from the diff and origin/main, ⛔ not from this seat's record, and returned PASS on this exact head with no patch round — it also stated the limb more exactly than this seat had: the accept/reject boundary of composeStacks does not move, but ApiErrorSchema.parse widens by exactly six ErrorCode members, which is the widening. The defect is the ORDER, and the order is what this comment puts on the record.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions