Skip to content

composeStacks objectConflict: 'merge' merges fields only — the later object's actions (and every other key) replace the earlier package's wholesale, silently dropping its embedded actions #14848

Description

@claude

Found while implementing #14662 (the cross-stack action-key check in composeStacks); out of scope there and recorded here instead. Measured on main @ f3ae441fa, packages/spec/src/stack.zod.ts mergeObjects.

What was measured

objectConflict: 'merge' is implemented as

const merged = { ...existing, ...obj, fields: { ...existing.fields, ...obj.fields } };

fields is the only key merged. Every other key present on the later object — actions, validation, hooks, indexes, labels, sharingModel, and so on — REPLACES the earlier object's value wholesale, and a key the later object omits keeps the earlier value. For the collections this is silent loss:

a = defineStack({ manifest: { id: 'com.example.a' }, objects: [{ name: 'shared', ..., actions: [approve] }] })
b = defineStack({ manifest: { id: 'com.example.b' }, objects: [{ name: 'shared', ..., actions: [archive] }] })
composeStacks([a, b], { objectConflict: 'merge' }).objects[shared].actions = [archive]   // `approve` is gone, no warning

ConflictStrategySchema documents 'merge' as "Shallow-merge items with the same name (later fields win)", which reads as a description of fields, not as "and the earlier package's actions are discarded". Nothing at compose, build or boot time says the action vanished — the same silent-loss class #5005 closed for top-level keys, one level down.

Why it is recorded now

#14662's collision check judges what the composition CARRIES: an embedded action that 'merge' did not carry into the composed object cannot collide, so this loss is invisible to it by design (documented in collectComposedActionKeyCollisions). That is the right boundary for the collision rule, but it means the loss has no reporter at all.

Options (for triage, not decided here)

  1. 'merge' merges the object-level collections by name (actions first, since it is the one with a runtime key) and throws on a same-name pair — the shape composeFunctions already uses for handlers; a same-key pair then surfaces through composeStacks concatenates actions across packages with no same-scope duplicate-key check — two packages each declaring one global:NAME compose into one collapsed handler key #14662's check instead of being dropped.
  2. Keep the shallow semantics and WARN once per dropped collection entry, the way warnMalformedCollectionKey reports a dropped key.
  3. Keep as is and make the describe text say it plainly.

Option 1 is the contract-first direction; whether cross-package object merging is a capability worth that much surface is the four-facet call. Filing unassigned for triage.


Generated by Claude Code

Activity

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

    @os-zhuang
    Contributor

    Triage: needs-user-decision · domain:spec · priority:p3. Lands in packages/spec/src/stack.zod.ts.

    Triage seat, session session_01SwJQDFKe8tVit3BXQ9EfR5, R+145, 2026-09-04T15:52Z. Escalated because every live option changes what a published composition function produces — 协议/公开契约变化, which is on the manual floor whatever the facets say.

    ⭐ The card's own confidence gap, measured — and it changes the shape of the answer

    The card asks whether cross-package object merging is a capability worth the surface, and leaves that open. Measured on origin/main at 15:50Z:

    reading result
    non-test call sites passing objectConflict (packages/**, examples/**) 0
    composeStacks non-test callers 6+ (CLI compile, metadata/src/plugin.ts, plugin-security, plugin-dev, examples/app-multi-package, …)
    objectConflict default (stack.zod.ts:2518) 'error'

    ⇒ 'merge' has zero measured consumers in this tree. Every real caller takes the default, which refuses the collision outright. The silent loss is reachable only by an author who opts into 'merge', and nobody in the repo does.

    ⚠️ ⛔ This does not say the loss is harmless — an external author who reads the describe text and opts in gets it, and the describe text does not warn them. It says the cost of changing 'merge' is near zero, which is what usually makes these forks expensive.

    The options

    1. Merge object-level collections by name (actions first), throw on a same-name pair — the shape composeFunctions already uses.
    2. Keep shallow semantics, WARN once per dropped entry, as warnMalformedCollectionKey does.
    3. Keep as is, make the describe text say it plainly.
    4. ⚠️ Added by triage, ⛔ not in the card: make 'merge' REFUSE when the later object carries a collection the shallow merge cannot combine. 'merge' then means "merge what is mergeable" and says so loudly instead of dropping.
    • ① 项目长远合理性(权重 ≥50%,领起推荐) —— 这里的不变量是声明即强制、不静默丢弃:今天两个包各自声明了同名对象上的 action,合成后有一个消失,compose / build / boot 三处都不出声。这正是 composeStacks silently drops every non-array top-level key — api: today, server: as of #4910 #5005 在顶层键上关掉的那一类,只是低了一层。⇒ ③ 单独出局:把静默损失写进文档,损失照旧静默;文档挡不住代码。①在 1 与 4 之间偏 4 —— 「两个包声明同一个对象」在创业阶段更像作者错误而非要支持的能力,而收窄特例比长出一套逐集合的合并规则更符合长远方向。选项 1 要为每种集合定义合并语义、把 owner 模型从 per-object 改成 per-action,那是一条永久增生的路。
    • ② 实际业务拉动 —— 实测为零(见上表)。无人传 objectConflict,默认即拒绝。按分歧推荐序:零拉动 ⇒ 荐不扩散,即 ⛔ 不选 1。⚠️ 这也是唯一可能翻转推荐的读数 —— 若维护者知道有外部作者在用 'merge' 合并带 actions 的对象,②立刻变重,方向转向 1。
    • ③ 防 AI 犯错 —— 最锐。ConflictStrategySchema 写的是「Shallow-merge items with the same name (later fields win)」,读起来像在讲 fields,⛔ 没有任何一个字说「另一个包的 actions 会被丢掉」。一个 AI 依约推理,推不出这个损失;它写出的合成在运行时少一个动作,而没有任何信号可供自我纠正。响亮拒绝 > 警告 > 文档,这一条把 4 排在 2 前面、把 2 排在 3 前面。
    • ④ 创业阶段不扩散 —— 指向 4。选项 1 是新增能力面(逐集合合并语义 + owner 模型改造 + 既有验收 pin 翻转);选项 4 是删掉一条没人用的宽容路径,面变小。已发布零消费的能力不因沉没成本获得豁免。

    推荐:4(拒绝),回退 2(警告)。 ①以≥50% 权重领起并指向收窄;②零实测拉动、按分歧推荐序取不扩散;③把响亮排在前面;④同向。⭐ 关键在于零消费者让「拒绝」几乎无代价 —— 通常这类收窄要付的迁移账,这里不存在。
    ⛔ 不建议 3:它把一个代码问题降级成文字问题,而损失继续静默,是 ③④ 都反对、且恰好最省事因而最容易被选中的那条路。
    ⛔ 不建议 1,除非 ② 被推翻:它是唯一需要付出永久语义增生的选项,而买到的能力今天无人使用。
    置信缺口: ⛔ 未测量仓外作者。@objectstack/spec 是已发布包,'merge' 是已发布的公开取值,所以「零消费者」只对本树成立,⛔ 对外部不成立 —— 选 4 是一次 minor 破坏性收窄,需要 changeset 明说。

    ⚠️ 一条必须先读的依赖,已在本卡评论里

    复核席(#14662 契约复审)记下:PR #14854 的碰撞检查把每个合成对象的 actions 归属到单一输入 stack(actionsOwner),其 P3 验收 pin 正是依赖 'merge' 把 actions 整个交给后者。⇒ 选项 1 会让 owner 模型变成 per-action,并把那条 pin 从 accepted 翻成 refused。选项 4 同样会动它(冲突改为拒绝)。任何方向的实施都要先读这条,⛔ 不可把 pin 的变红读作回归。

    priority:p3:今天零消费者、零用户可见影响;⛔ 但不是 p4,因为对外已发布的取值带着一句读不出真实后果的 describe。


    Generated by Claude Code

  3. os-warren commented on Sep 4, 2026

    @os-warren
    Collaborator

    Maintainer ruling recorded — 4: objectConflict: 'merge' refuses when the later object carries a collection the shallow merge cannot combine (actions, validation, hooks, indexes, …); it means "merge what is mergeable" and says so loudly instead of dropping

    Director seat, summon #14, session session_01LsEjuNMPitCHwEfYftZ1um (GitHub os-warren), 2026-09-04. Provenance: maintainer, live PM chat, decision batch #38 (item 5, presented with the recommendation 4, fallback 2), verbatim reply 「同意」. Premise: triage facets 5542258615 — zero non-test call sites pass objectConflict in this tree, the default is 'error', so 'merge' has no measured consumer; the describe text ("later fields win") does not say the earlier package's actions are discarded.

    Ruled: 4. In packages/spec/src/stack.zod.ts mergeObjects, when both inputs declare the same object and the later one carries any object-level collection beyond fields that the shallow merge would replace wholesale, composeStacks throws (the same refusal shape 'error' uses, naming the object, the colliding collection and both package ids). fields keeps its documented shallow merge. The ConflictStrategySchema describe text for 'merge' is rewritten to state the rule. Not taken: 1 (per-collection merge by name — permanent semantic growth for a capability nobody uses; the fallback only if ② is overturned by a known external author), 2 (warn per dropped entry — the fallback), 3 (prose only).

    Why (① ≥50%): declared = enforced, and never silent loss — the same class #5005 closed one level up. Two packages declaring one object is, at this stage, more likely an authoring error than a capability to grow rules for; narrowing the special case beats growing a per-collection merge model. ② is measured zero, so no expansion; ③ loud refusal > warning > docs; ④ deletes an unused lenient path.

    Execution: domain:spec lane, S. ⚠️ Read comment 5521297578 first: PR #14854's collision check attributes actions to a single owning stack, and its P3 pin ("both stacks embed the same name on one object … accepted") depends on 'merge' handing actions wholesale to the later object — under 4 that case is now refused at compose, so the pin flips to a refusal pin, ⛔ not a regression. Pins: the refusal for a colliding collection, the fields-only merge still accepted, the default 'error' unchanged. Clause-②: yes (a published value's accept-set narrows) ⇒ needs:contract-review on the PR. Changeset: @objectstack/spec minor with a BREAKING banner ('merge' now refuses object pairs whose collections cannot be merged; previously the earlier package's entries were silently dropped) and an adr-0087 disposition comment.

    State transition, same stroke: needs-user-decision → pm:queue. domain:spec · priority:p3 unchanged. Ledger: director seat post #12708, batch #38.


    Generated by Claude Code

  4. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Claim: PM loop round R2 — domain:spec seat, session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T22:26Z. One dev, branch claude/issue-14848-object-conflict-merge-refuses-collections, mode:subagent, tier claude-fable-5-1. Size S.

    Clause-②: yes

    Ruled card, executed as ruled: director ruling 5542636547 (2026-09-04T15:20Z; maintainer verbatim 「同意」 to decision batch #38 item 5) — 4: in packages/spec/src/stack.zod.ts mergeObjects, when both inputs declare the same object and the later one carries any object-level collection beyond fields that the shallow merge would replace wholesale (actions, validation, hooks, indexes, …), composeStacks under objectConflict: 'merge' THROWS with the same refusal shape 'error' uses, naming the object, the colliding collection and both package ids; fields keeps its documented shallow merge; the ConflictStrategySchema describe for 'merge' states the rule. Not taken: 1 (per-collection merge by name), 2 (warn per dropped entry — the fallback), 3 (prose only). ⚠️ Read comment 5521297578 first: PR #14854's collision check attributes actions to one owning stack and its P3 acceptance pin in compose-stacks-action-key-collision.test.ts depends on 'merge' handing actions wholesale to the later object — under 4 that case is refused at compose, so the pin flips to a refusal pin, ⛔ not a regression. Clause-② yes: a published value's accept set narrows — the seat runs the in-seat contract review, then hangs and clears needs:contract-review on both carriers (PR + this card).

    File face (readings on origin/main 53cbad9f7 at 2026-09-05T22:23Z): packages/spec/src/stack.zod.ts — mergeObjects at :2848 (the 'merge' arm at :2882–:2883 { ...existing, ...obj, fields: { ...existing.fields, ...obj.fields } }, the 'error' refusal message at :2870–:2873, the actionsOwner docblock :2842), ConflictStrategySchema at :2565–:2568 ('merge' — Shallow-merge items with the same name (later fields win)), objectConflict default 'error' at :2578; pins — the refusal for a colliding collection (message names object, collection, both package ids), the fields-only merge still accepted, the default 'error' unchanged, the #14854 P3 pin flipped to refusal; regenerated reference page (content/docs/references/** for the stack / compose schema) if the describe feeds it; one @objectstack/spec changeset (minor, launch-window **BREAKING** banner — 'merge' now refuses object pairs whose collections cannot be merged, previously the earlier package's entries were silently dropped — with the adr-0087 disposition the gate accepts). Hot file: stack.zod.ts is also touched by PR #16051 (#14877, in the landing window, flipping ~22:42Z) in a region-disjoint place (the disposition table :875 vs mergeObjects :2848) — the dev merges origin/main via scripts/pm/os-regen-merge.sh before opening the PR if #16051 has landed, and expects one merge lap otherwise. Dedup: #14662 / PR #14854 (the action-key collision check, landed), #5005 (the top-level key-loss class).


    Generated by Claude Code

  5. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 14848,
      "status": "done",
      "branch": "claude/issue-14848-object-conflict-merge-refuses-collections",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16079",
      "premise_still_valid": true,
      "summary": "Executed ruling 4 as written: in packages/spec/src/stack.zod.ts mergeObjects, objectConflict: 'merge' now throws (the 'error' refusal shape, naming the object, the colliding collection key and both stacks via stackLabel/manifest id) when both objects declare an object-level collection other than fields with different values; fields keeps its shallow merge, scalars/config objects keep later-wins (stated in the docblock), identical declarations pass through (measured choice, mirrors composeSingleValue/deepEqualAuthored), and an explicit undefined on the later object is read as no declaration (neither refuses nor erases). The refusal set is DERIVED from ObjectSchema.shape (array/record after wrappers, union members counted, fields excluded, lazily resolved): indexes, fieldGroups, requiredPermissions, validations, activityMilestones, highlightFields, listViews, searchableFields, actions — the ruling's illustrative 'validation'/'hooks' are not object-level keys (validations is; no object-level hooks). ConflictStrategySchema/composeStacks/collision-walk docblocks rewritten; no export added (api-surface 0/0) so no generated page moved (check:generated 15/15 current). PR #14854 pins flipped per note 5521297578: P3 (made two DIFFERENT embedded declarations) is a merge refusal pin with override acceptance kept plus an identical-embedded acceptance pin; P5b both orders refused under merge, override pins unchanged; P4 (identical built copies) unchanged and still reaches the action-key check; action-echo three-stack merge arm flipped to refusal. New file compose-stacks-merge-collection-refusal.test.ts (62 tests) pins the full message, each collection key by name, first-declarer naming, fields-only merge, identical/absent/explicit-undefined cases, default 'error' verbatim, 'override' unchanged, and the two-direction shape pin. Changeset @objectstack/spec minor with BREAKING banner and adr-0087 not-required (no-migration-prescription). M1 re-measured: zero non-test objectConflict call sites (53cbad9f7 and c463d03e0); no stop condition. Merge lap done: origin/main c463d03e0 merged via os-regen-merge.sh (5 commits, no conflict, nothing to regenerate); #16051 has not landed, so one more merge lap may follow it. Card assignee was set by the dispatch; newest Claim: 5555195352 names this branch; no second claim posted, no assignee written.",
      "tests": "All heavy runs via scripts/pm/os-verify-lock.sh (OS_VERIFY_LOCK_SLOT=issue-14848), exits captured before any pipe. spec build: VERDICT command-exit 0 (check-dts-emitted 34/34). spec suite post-merge @29e4671f6: 'Test Files 477 passed (477) / Tests 12826 passed (12826)' (pre-merge 479/12902, green); spec typecheck exit 0 incl. check:test-typecheck OK (test layer compiled under tsconfig.test.json). Targeted compose-stacks suites: 7 files, 195 passed. check:generated: 'All 15 generated artifacts are up to date' (one earlier red was the #14985 mtime shape after a byte-identical restore; rebuild cleared it). Ablation with fix committed: removed the refuseUnmergeableCollections call (anchor 1 to 0, marker 0 to 1, blob differs from HEAD) -> 'Tests 25 failed | 74 passed (99)' across the three pin files, every refusal pin red; restore via git checkout HEAD -- path proven by blob hash 370b1218… equal to HEAD:path, git diff HEAD empty, porcelain empty; subject resolves from source (./stack.zod), no dist leg. Gates: dispatch-gates --repo objectstack-ai/objectstack --commands on 29e4671f6 derived 72 commands, all 72 ran, --ran reconciliation '72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN'; 70 green (check:doc-formula-expressions after building the @objectstack/lint closure: '58 cases passed', '22 record-scoped formula example(s) across 428 files'); NOT MEASURED locally by their own text, declared to CI Lint & Repo Gates: pnpm check:dual-build-cjs-loads and pnpm check:type-check-debt (exit 3 'nothing was measured', prerequisite whole-repo pnpm build). check:nul-bytes, check:adr-0087-registration, check:empty-changeset, check:changeset-no-major green; control-byte grep over changed files clean. Lint, proved narrowing @29e4671f6: eslint --no-inline-config --format json over the 4 changed TS files: 4 files, 0 errors, 0 warnings; population = repo eslint.config.mjs, which states no parserOptions.project / no typed rules (line 328) so untouched files' verdicts cannot move. Consumers, declared narrowing: turbo ls --affected (merge-base c463d03e0) = 75 packages (cli closure 58, runtime 31, beyond the local cap); check:api-surface green (0 added/0 removed) proves the consumer compile face unchanged; cli/runtime typecheck + tests declared to CI required TypeScript Type Check / Test Core. CI status at report time: in_progress.",
      "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (curl): card + comments + reviewer note read, dedup read (open domain:spec list, 126 cards, control hit #14848 present, no match), finding filed, draft PR opened, report comment posted.",
      "open_questions": [
        {
          "question": "Attribution footer form: the harness instruction ends PR bodies with the '🤖 Generated with [Claude Code]' line + session URL, while AGENTS.md prescribes the rule-line + session-URL '_Generated by_' block; the PR body carries both and the platform re-placed its rule-line block at the tail on read-back. No action taken; flagging the two-standard conflict only.",
          "options": ["A keep both (as landed)", "B seat trims one form on the in-seat contract review"],
          "recommendation": "A, because both surfaces are satisfied and the PR body prose carries the durable attribution regardless."
        },
        {
          "question": "Reading of an explicit `undefined` on the later object under 'merge': the fix drops explicitly-undefined keys from the later object's spread for ALL keys (scalars included), so the refusal, the spread and actionsOwner share one reading of 'declares'. The ruling narrows collections only; this is a one-line coherence choice, stated in the docblock and PR body, for the contract review to confirm or narrow to collection keys.",
          "options": ["A keep the uniform reading (as landed)", "B strip undefined only for collection keys, leave scalars on the bare spread"],
          "recommendation": "A, because zod v4 keeps an explicitly-undefined input key as an own property, so a built stack can carry one, and a scalar erased by undefined is not a declaration any author wrote."
        }
      ],
      "out_of_scope_findings": ["filed as #16075: under objectConflict: 'merge' a fixed-shape config object (enable, access, protection, tenancy, lifecycle, userActions, publicSharing, external) the later object declares still replaces the earlier one wholesale — the config-object half of the boundary the collection-only ruling drew; unlabelled, unassigned, for PM triage"]
    }

    Generated by Claude Code

  6. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Review verdict — PR #16079 at head 29e4671 (base c252041 after the dev's merge lap at c463d03): ACCEPT · Clause-② contract review PASS (in-seat, domain:spec seat, session_01M59rPZZFzqhfMUPFqqZTkf, this session's model is the CONTRACT_REVIEW_TIER; readings 2026-09-05T23:42Z unless stated).

    Checklist against ruling 4 (5542636547):

    • The refusal: mergeObjects' 'merge' arm calls refuseUnmergeableCollections before the spread; a collection the later object declares that the composed object already carries with a different value throws in the 'error' refusal shape, naming the object, the colliding collection and both stacks by manifest id and position (stackLabel), with the first declarer tracked per composed object so a third stack is named against the stack it disagrees with. The message carries the fix (declare once, make identical, or 'override'). Verified in the diff.
    • fields keeps its documented shallow merge; 'error' and 'override' are untouched and their messages pinned verbatim; the ConflictStrategySchema docblock (the enum carries no .describe() string — the JSDoc is the describe text here), the composeStacks docblock and example, and the collision walk's docblock all state the rule. No export added or removed (check:api-surface 0 / 0), so no generated page moved.
    • The refusal set is derived from ObjectSchema.shape (array or record after the optional / default / nullable / readonly / catch wrappers, through lazy and pipe, a union counting when any member is a collection), fields excluded by rule, resolved lazily. Measured on this tree: indexes, fieldGroups, requiredPermissions, validations, activityMilestones, highlightFields, listViews, searchableFields, actions. The ruling's illustrative validation / hooks are not object-level keys (validations is; there is no object-level hooks, and an undeclared key is refused by the strict parse before composition) — the dev reported this against the ruling's wording rather than following it; accepted as measured. The walk reads zod v4's _zod.def internals; the new test pins the derived set against the shape in both directions with an independent walk and carries the literal list, so a zod-internal rename or a new collection key surfaces as a red pin, not as silent fallback to wholesale replacement.
    • The feat(spec): composeStacks refuses two stacks whose actions resolve to one scope-qualified runtime key #14854 pins flipped exactly as reviewer note 5521297578 foresaw: P3 is a refusal pin under 'merge' (two different embedded declarations), 'override' keeps its acceptance, an identical-embedded pair stays accepted under both; P5b refused both orders under 'merge', 'override' pins unchanged; P4 (identical built copies) unchanged and still reaches the cross-stack action-key check (composeStacks concatenates actions across packages with no same-scope duplicate-key check — two packages each declaring one global:NAME compose into one collapsed handler key #14662). The composeStacks re-merges bound standalone actions that defineStack already copied into their objects — every bound action appears twice in the composed object's actions #14847 echo test's three-stack 'merge' arm relied on the dropped case and is a refusal pin now.
    • New pin file compose-stacks-merge-collection-refusal.test.ts (62 tests): the card's own case with the full message, every collection key refused by name, first-declarer naming across three stacks, manifest-less inputs by position, the fields-only merge, identical / absent / explicit-undefined cases, default 'error' verbatim, 'override' unchanged, the two-direction shape pin.
    • Changeset @objectstack/spec minor with the BREAKING banner and the adr-0087: not-required (no-migration-prescription) disposition, as the ruling prescribed; nothing authorable renamed, retired or re-typed. Call sites re-measured: zero non-test objectConflict callers at 53cbad9 and c463d03; no stop condition.
    • Governed surface: node scripts/pm/check-governed-merges.mjs --test on the five-file list reads NOT governed — ordinary queue landing.
    • Dev report 5555561645 read: build green, spec suite 477 files / 12826 tests at 29e4671, typecheck including the test layer, generated artifacts 15/15 current, dispatch-gates 72 derived / 72 run / 0 unrun (two declared to CI by their own text), lint 0 / 0 on the four changed files, ablation (the refusal call removed) went 25 red / 74 green across the three pin files and restored to a proven-identical blob.

    Contract review (Clause-② — a published option value's accept set narrows): composeStacks({ objectConflict: 'merge' }) now refuses same-name object pairs whose object-level collections differ, where it used to compose with the earlier stack's entries silently dropped. Every object key, every option and the enum parse as before; the narrowing is at composition time only, carries its own prescription in the refusal text, and has zero measured consumers in this tree. Identical declarations pass through (the minimal true reading of "would replace wholesale" — an identical replacement drops nothing — and the same reading composeSingleValue gives top-level keys). PASS.

    Open questions in the dev report:

    1. Attribution footer, both forms in the PR body — A, keep both; not a contract matter.
    2. Explicit undefined on the later object stripped before the spread for all keys, scalars included — A, as landed: the ruling's own rationale is never silent loss; an explicitly-undefined key on a built stack (zod v4 keeps it as an own property) is not a declaration any author wrote, so erasing an earlier scalar with it would be the same silent loss one level down. Stated in the docblock; zero measured callers; the maintainer can narrow it to collection keys by a one-line amendment if the uniform reading is unwanted.

    Out of scope: finding #16075 (a fixed-shape config object still replaces wholesale under 'merge' — the config-object half of the boundary the collection-only ruling drew) is left to triage.

    Hot file: PR #16051 (#14877) also changes packages/spec/src/stack.zod.ts and is still in the merge queue (enqueued 22:43:49Z). Its hunks sit at lines 811–1000 (top-level key dispositions); this PR's sit at 2562–3360 (object-level merge). git merge-tree --write-tree f8d52d540 29e4671f6 reads clean at 23:40Z, and the two changes are semantically independent. The queue builds this PR's group on top of #16051's, so the queue run is the combined-tree test; if the queue kicks this PR after #16051 lands, a real merge lap and a delta review at the new head follow before re-enabling auto-merge.

    Next: carriers cycled at this head (label event stream on PR #16079 and this card), then check-clause2-carriers.mjs --pair 16079 must read ✓; flip to ready + auto-merge once CI on 29e4671 is green.


    Generated by Claude Code

  7. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    Landing provenance (domain:spec seat, session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-06T00:12Z): PR #16079 flipped to ready 00:11:4xZ and auto-merge (squash) enabled 00:11:49Z on head 29e4671f6 — every check on that head green (33 success / 8 skipped, none red), mergeable_state: clean; the added_to_merge_queue reading follows on the landing watch (not yet on the timeline at the time of writing). ACCEPT + Clause-② PASS 5555571219 at 29e4671f6; needs:contract-review hung and cleared on both carriers (PR #16079, this card) at that head (23:42Z), pair check ✓; governed predicate 0 of 5 paths. Hot file: PR #16051 (#14877) landed on main as 69602e578 at 23:50Z with disjoint hunks in stack.zod.ts (top-level dispositions vs the object-level merge); the in-memory merge of this head onto that one read clean at 23:40Z, and the merge queue builds this PR's group on the landed tree, so its run is the combined-tree test — a kick would trigger a real merge lap and a delta review before re-enabling. The landing note and the Release: line follow on MERGED; no follow-up card is owed beyond finding #16075, already filed for triage.


    Generated by Claude Code

  8. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    Landed (domain:spec seat, session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-06T01:19Z): PR #16079 merged 2026-09-06T01:18:01Z as 64bd6a399 through the merge queue (squash; added_to_merge_queue 00:12:35Z, about 65 minutes in queue; its group was built on the tree that already carried PR #16051's stack.zod.ts hunks, and the combined-tree run was green — the queue served as the merge lap, no kick). Probe on origin/main at 2026-09-06T01:18Z (tip 2648774b9): git grep -c refuseUnmergeableCollections origin/main -- packages/spec/src/stack.zod.ts = 3; control 0 at c2520416c; compose-stacks-merge-collection-refusal.test.ts is on main. ACCEPT + Clause-② PASS 5555571219 at 29e4671f6, carriers cycled on both, pair ✓; provenance 5555710617.

    composeStacks({ objectConflict: 'merge' }) now refuses same-name object pairs whose object-level collections differ (ruling 4, 5542636547). No follow-up card is owed; finding #16075 (the config-object half of the boundary) stays with triage.

    Release: session_01M59rPZZFzqhfMUPFqqZTkf — reason: landed, card closed by Fixes #14848 — destination: none (closed). Same stroke: pm:dispatched stripped, assignee cleared.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions