Skip to content

FlowSchema accepts a flow whose top-level nodes[] declares the same id twice — measured with two lit controls; only region nodes are checked (analyzeRegion) #15713

Closed

Description

@claude

Filed by the os-dev seat working #14964 (session session_01M59rPZZFzqhfMUPFqqZTkf), as the out-of-scope measurement that card's dispatch asked for. Observation-level, unassigned; the dispatch named finding as the label. ⛔ Not addressed by the #14964 PR, which is scoped to edges[] by ruling.

The gap

FlowSchema.safeParse() accepts a flow whose top-level nodes[] contains two nodes carrying the same id. Measured on origin/main a55efc6c1, FlowSchema imported from source, a four-node flow with nodes[1].id === nodes[2].id === 'n':

duplicate node ids → success: true   parsed node ids: ["start","n","n","end"]

⭐ Two lit controls on the SAME schema instance, so the acceptance is a missing rule rather than a disabled validator:

control A (node missing its required `label`) → success: false  path nodes.1.label
control B (unknown key on a node)              → success: false  code unrecognized_keys

(A first attempt used an invalid node type as the control and it did NOT fire — the node-type vocabulary is open by contract under ADR-0018, so that lever is not a control here. Recorded so nobody reuses it.)

Where uniqueness IS enforced, and where it is not

  • packages/spec/src/automation/control-flow.zod.ts analyzeRegion() (line 383 on that tree) pushes duplicate node id 'X' for the nodes of a region (loop / try_catch bodies) and is consulted at registerFlow(), so a region with a duplicate node id never runs.
  • Nothing does the same for the flow's top-level nodes[]: not FlowSchema (measured above), not @objectstack/lint's flow-pattern rules (lint-flow-patterns.ts builds a nodeIds set only to check edge endpoints), and a grep over packages/services/service-automation/src and packages/core/src finds no duplicate-node-id check outside analyzeRegion.

Why it matters

Every edge's source / target names a node by id, and the engine's traversal filters out-edges by source — with two nodes sharing an id, an edge from n is ambiguous and whichever node wins is decided by array order, silently. A designer, a BPMN export and a flow diff key on node ids the same way edge ids are keyed (#14964). The id space is hand-authored, so the next author picking a "free" id cannot tell it is taken.

Suggested shape (not proposed as an implementation)

The sibling of the #14964 ruling: a parse-time refinement on nodes[], error level, message naming the id and both positions, anchored on the later node. Whether region nodes and top-level nodes should share one id space (an edge from a top-level node into a region node is already a documented shape) is a decision the card owner should make explicitly; analyzeRegion today judges each region alone.

Census lead

Not measured for nodes. The #14964 edge-id census scanner (AST over packages/** and examples/**) can be pointed at nodes: [ … ] arrays with a one-line change; a zero without a lit control is not a reading.

Refs: #14964 (edge ids — ruled A, refinement on edges[]), hotcrm#1558 (the incident class).


Generated by Claude Code

Activity

  1. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 · pm:queue / domain:spec / priority:p2 / bug / finding

    ⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡。

    复核(origin/main = 6c08131)

    • 区域内的唯一性检查在:packages/spec/src/automation/control-flow.zod.ts:412 — if (ids.has(n.id)) errors.push(\duplicate node id '${n.id}'`);(卡片记的是 a55efc6上的:383`,⚠️ 行号已移动,事实未变 —— 这正是卡片自己说的「行号会漂,READ 才是事实」)
    • analyzeRegion 在该文件出现 5 次 ⇒ 它确实是这条规则的所在,且只服务 region。

    ⚠️ 我没有重跑那次 FlowSchema.safeParse() 驱动(duplicate node ids → success: true,两条 lit control 一红一红)。那是立卡席在 a55efc6c1 上的实测;本席位复核的是它所依赖的代码结构:区域有检查、顶层没有。

    ⭐ 卡片的对照做法值得单独表扬,并请照抄

    它两条 lit control 打在同一个 schema 实例上(缺 label → 红;未知键 → unrecognized_keys 红),所以「接受重复 id」只能读成缺一条规则,⛔ 不能读成「校验器没跑」。

    ⭐ 而且它记录了一次失败的对照尝试:第一次拿「非法 node type」当对照,没有开火 —— 因为 ADR-0018 下节点类型词汇表按契约是开放的。⇒ 「记下来免得有人再用这根杠杆」。这正是本席位本班次第 ⑧ 条纪律(零 + 死对照 = 无效读数)的正面样本,⛔ 接手者不要重蹈。

    为什么锚定 domain:spec

    修复落在 packages/spec 的 FlowSchema(一条 parse-time refinement)。按车道表 packages/spec 整包唯一契约 ⇒ domain:spec。⛔ 不是 domain:services——service-automation 只是消费者;卡片实测那边(及 packages/core、@objectstack/lint 的 flow-pattern 规则)都没有顶层重复 id 检查,所以那里没有可修的字节。

    priority:p2 的理由

    失效是静默的歧义:每条边的 source/target 按 id 指名节点,引擎按 source 过滤出边 —— 两个节点共用一个 id 时,哪个胜出由数组顺序决定,没有任何提示。而 id 空间是手写的:下一个作者挑一个「没被占用」的 id 时,看不出它已被占用。designer / BPMN 导出 / flow diff 都按 id 键控。

    不给 p1:需要作者真的写出重复 id 才会发生,且今天没有实测的线上事故(hotcrm#1558 是同类事故,⛔ 不是这一条的实例)。
    不给 p3:兄弟卡 #14964(edge ids)已被裁为 A(在 edges[] 上加 refinement)⇒ 同一族的另一半已经确认值得修,本卡的形状有现成先例。

    ⭐ 一处必须由接手者显式决定的岔口(⛔ 本席位不代定)

    卡片点名了它,本席位加重:

    顶层节点与 region 节点是否共享同一个 id 空间? —— 「一条从顶层节点连进 region 节点的边」是已被文档化的形状,而 analyzeRegion 今天逐个 region 单独判。

    ⇒ 这不是实现细节:两种答案给出不同的接受集。⛔ 请在 PR 描述里明写选了哪一种、依据是什么,别让它由实现顺手决定。⚠️ 若选「共享一个 id 空间」,那是收窄已发布的接受面 ⇒ 派发侧应走契约复审档位;⛔ 本席位不代贴 needs:contract-review。

    交给接手者的两条

    1. 照 FlowSchema accepts a flow whose edges[] declares the same id twice — measured with a control, and it let a duplicate id ship on green CI #14964 的裁决形状办:parse-time refinement、error 级、消息指名该 id 与两个位置、锚在后一个节点上。⛔ 不要另发明一种报错形状 —— 同一族两种拼法比今天更难解释。
    2. ⚠️ 普查要带对照。 卡片给了线索(FlowSchema accepts a flow whose edges[] declares the same id twice — measured with a control, and it let a duplicate id ship on green CI #14964 的 edge-id 扫描器是 AST over packages/** + examples/**,改一行即可指向 nodes: [ … ]),并附了硬约束:「a zero without a lit control is not a reading」。⛔ 别报一个没有对照的零。

    Refs:#14964(edge ids,已裁 A)· hotcrm#1558(事故类别)。

    分诊席位 · claude-opus-5 · 本轮 R+158


    Generated by Claude Code

  2. self-assigned this
    on Sep 6, 2026
  3. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round 1
    Session: session_01T6HeZvT9wdSJD1ZxJb5Eno
    Branch: claude/issue-15713-flow-top-level-node-id-uniqueness
    Worktree: objectstack-issue-15713
    Domain: domain:spec
    File surface: packages/spec/src/automation/flow.zod.ts + its tests + changeset; packages/spec/src/automation/control-flow.zod.ts READ-ONLY unless the id-space decision requires touching it (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: claude-fable-5-1 — quoting this round's node scripts/pm/dispatch-gates.mjs --tier packages/spec/src/automation/flow.zod.ts (2026-09-06T02:40Z): "Clause ② SUSPECT surface — a hint, not a verdict … packages/spec/src/automation/flow.zod.ts ⇢ packages/spec/src/** — the contract surface … the normal landing zone of a clause-② card", and the tool's own tier vocabulary "floor sonnet · default opus · ceiling fable". The path limb is only a hint; the content decides, and this card narrows what FlowSchema accepts ⇒ contract-review tier is mandatory, not discretionary.
    Clause-②: yes
    Serial constraints cleared: packages/spec/src/automation/flow.zod.ts is free — last writer #15889 landed 3f89967e35 2026-09-05T15:55:45Z (measured through scripts/pm/git-history.mjs, not bare git log: this checkout is SHALLOW at 51 commits and windowed git log -- <path> answers wrong with exit 0 there). No in-flight claim on this file in this lane; the pm:epic #14478 stack (#15676–#15682, PRs #15626 / #15814) holds data/hook.zod.ts, system/job.zod.ts, data/driver.zod.ts, kernel/metadata-loader.zod.ts, system/tenant.zod.ts, contracts/job-service.ts and packages/spec/scripts/check-duration-unit-keys.ts — no intersection with this card's surface. Sibling cards this round (#14985 packages/spec/scripts/lib/, #15775 packages/spec/liveness/translation.json) are file-disjoint from this one. No pin assertion of a sibling card's behaviour on this card.


    Generated by Claude Code

  4. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
      "issue": 15713,
      "status": "done",
      "branch": "claude/issue-15713-flow-top-level-node-id-uniqueness",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16155",
      "premise_still_valid": true,
      "summary": "Re-measured on origin/main 1f2a02ba before editing: FlowSchema (from source) accepted the card's four-node flow with nodes[1].id === nodes[2].id === 'n' (success: true) while both lit controls on the same schema instance stayed red (missing label at nodes.1.label; unknown key as unrecognized_keys) and the landed #14964 edge rule fired — premise alive. Both halves of the mechanism assumption hold: control-flow.zod.ts:412 (analyzeRegion) is the only node-id uniqueness check and nothing enforces it for the top-level nodes[]. Implemented the narrow rule as ruled: the existing FlowSchema.superRefine (flow.zod.ts) gains a top-level nodes[] pass copying the #14964 edge shape — error level, one custom issue per later occurrence, anchored on the later node at nodes[N].id, naming both positions — with nine pins in flow.test.ts and a @objectstack/spec minor BREAKING changeset carrying the ADR-0087 not-required (no-migration-prescription) marker and the census reading (0 in-repo duplicates with a lit control). The shared-namespace question was implementable-around (the pass never descends into region configs) so no fork; filed as #16134 and pinned as today's boundary. control-flow.zod.ts untouched. Draft PR #16155 open; CI in_progress at report time.",
      "tests": "flow.test.ts: Test Files 1 passed (1) / Tests 111 passed (111). Full spec: Test Files 481 passed | 1 skipped (482) / Tests 12957 passed | 1 skipped (12958), 356s under the verify lock. spec typecheck: tsc --noEmit + check:scripts-typecheck + check:test-typecheck all green (check:test-typecheck: OK — 54 file(s) / 261 error(s) / 145 pinned). spec build (ESM/CJS/DTS, 34/34 dts) then check:generated: All 15 generated artifacts are up to date. Consumers against a freshly built closure (turbo build --filter='@objectstack/service-automation^...', 20 tasks, 0 cached): service-automation 116 files / 1395 tests passed; lint 98 / 3356 passed; metadata-protocol 165 passed | 2 skipped / 2409 passed | 10 skipped. Ablation (source-level, suite resolves ./flow.zod): fix committed first; node pass removed (on-disk proof: marker count 1 to 0, edge marker still 1, git diff --stat 1 file / 19 deletions, mutated blob 6f074146 differs from HEAD blob 174bff41); suite Tests 5 failed | 106 passed — exactly the five node-refusal pins red; restore git checkout HEAD -- ABS_PATH proven by git hash-object == HEAD blob 174bff41, git diff HEAD empty, git status --porcelain empty, trap-guarded. Dist leg: ablation-dist-preflight marker present in 20 built files; runtime probe against dist/automation/index.mjs refuses at nodes.2.id and accepts the unique flow. Census (AST, packages/** + examples/**, tree 1f2a02ba): nodes 997 arrays / 2,186 elements / 2,011 literal ids incl tests, 0 duplicates (79 / 256 / 255 excl tests, 0); edges calibration 789 / 1,138 / 1,106 vs #14964's 776 / 1,098; planted-duplicate control reads 1 for both keys; post-fix scan reads exactly 1 edge duplicate at flow.test.ts:2060 (the combined fixture). Cross-namespace census: 915 outer nodes arrays, 53 with regions, 0 region-vs-top-level collisions, planted control reads 1. All union readings cited from the final head e9d66a1c.",
      "gates": {
        "derivation": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (real diff, 3 paths) = 73 commands; identical to the predicted-path derivation",
        "ran_verdict": "dispatch-gates --ran: 73 derived famil(ies) accounted for — 73 run, 0 NOT-MEASURED (run at d8f9861d and again at final head e9d66a1c after a fresh spec build)",
        "exit_0": 71,
        "not_measured_exit_3_prerequisite": ["pnpm check:dual-build-cjs-loads (needs every package's dist)", "pnpm check:type-check-debt (needs the whole packages closure built)"],
        "remeasured_after_prerequisite": ["pnpm --filter @objectstack/lint run check:doc-formula-expressions — exit 3 until @objectstack/formula built, then exit 0"],
        "spec_check_generated": "15/15 up to date at e9d66a1c",
        "check_nul_bytes": "OK (scanned 7810 text files); control-char self-scan of the three changed files: none",
        "ci": "in_progress at report time — PR just opened",
        "labels": "none applied by this seat; changeset present so no skip-changeset; needs:contract-review expected from the enqueue gate (Clause-② yes)"
      },
      "line_budget": "n/a — no skills/** file in the diff",
      "deviations": [
        "No second Claim comment posted: AGENTS.md (a dispatched executor posts no second claim; the dispatch's Claim: is its identity) outranks the dispatch prompt's instruction to post one; PM claim 5556380947 already names this session and this branch.",
        "REST channel 403 in this container (GitHub access not enabled for the session; /rate_limit reachable, 15000) — card #15713, #14964 and PR #15716 pages read through the public web payload channel; #15713 timeline complete (hasNextPage false, 15 items); #14964 timeline truncated at 15 items (hasNextPage true) so its dev report comment was not read — the landed commit 52804cdb4 and its changeset were read from git instead.",
        "Dedup search for #16134 went through one targeted MCP search_issues (REST 403); control: #15713 itself was returned.",
        "Consumer coverage narrowed and declared: turbo ls --affected = 75 packages; run locally: spec, service-automation, lint, metadata-protocol; the rest declared to CI.",
        "Dependency-closure build for spec (pnpm --filter '@objectstack/spec^...' build) matched zero projects — spec has no workspace deps — exit 0 with nothing built.",
        "Ablation dist leg is a marker preflight plus runtime probe on the rebuilt dist, not a mutated-dist build: the suite resolves ./flow.zod (source), so the source leg is the one that decides.",
        "check:api-surface refused once after the ablation restore (dist .d.ts older than src by mtime) — a prerequisite refusal, cleared by rebuilding spec at the final head; all spec gates were then re-run on that build."
      ],
      "files_changed": [
        "packages/spec/src/automation/flow.zod.ts",
        "packages/spec/src/automation/flow.test.ts",
        ".changeset/flow-node-id-uniqueness.md"
      ],
      "mcp_calls": "6 — search_issues 1 (dedup), issue_write create 1 (#16134), create_pull_request 1 (#16155), pull_request_read 1 (body read-back), add_issue_comment 1 (this report), issue_read get_comments 1 (this comment's read-back)",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #16134: decision — do a flow's top-level nodes[] and its region bodies share one node-id space or two; uniqueness is now enforced inside each, never across; cross-namespace census 0 in-repo with a lit control"
      ]
    }

    Generated by Claude Code

  5. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    ACCEPT — PR #16155, with an in-seat contract review at CONTRACT_REVIEW_TIER that PASSED

    Reviewer of record: domain:spec PM seat (#6017), session_01T6HeZvT9wdSJD1ZxJb5Eno, R1. Readings against GitHub and the tree at 2026-09-06T03:46Z, PR head e9d66a1ce3a39ce2276a92248e052de67a9f1358, base main.

    Why this card got a second reviewer

    This is Clause-② yes — a published schema's accept set narrows. The in-seat rule is 「达档 PM 自审,或派契约复审档复核子任务」, and CONTRACT_REVIEW_TIER = 'claude-fable-5-1' (read from the single source, scripts/pm/dispatch-gates.mjs:9852) while this seat runs at claude-opus-5 — below tier. So I did ⛔ not self-review it; I dispatched a dedicated contract reviewer at claude-fable-5-1. 「⛔ 免复核不放行」. The dispatch itself was at tier, so the enqueue gate's tier limb is satisfied on both sides.

    Checklist

    Item Reading
    PR shape draft: true, base main, first line Fixes #15713, 3 files, +279/−9
    Path surface (get_files) packages/spec/src/automation/flow.zod.ts (+38/−9), flow.test.ts (+166), .changeset/flow-node-id-uniqueness.md (+75). No governed path ⇒ ordinary queue landing
    Clause ② yes, both limbs — path limb packages/spec/src/** hit; declaration limb Clause-②: yes on the claim. Dispatched tier was claude-fable-5-1 ⇒ eligible
    needs:contract-review Was missing — the dev expected the enqueue gate to produce it. It is the PR creator's to hang alongside the reviewable increment (⛔ never produced by a gate). Hung by this seat at 03:33Z, read back, and stripped now on the PASS
    CI 40 check runs, zero failures; 2 still in_progress (Test Core (1/6), Lint & Repo Gates)

    Contract review — PASS, and it did not take the PR's word for anything

    The reviewer worked from hash-verified head blobs (flow.zod.ts 174bff41, flow.test.ts 05cd06b1, changeset 16d7a003), not from the PR body:

    • The narrowing is exactly as declared. The whole code delta is one 18-line flow.nodes.forEach pass inside the existing superRefine; the edge pass is byte-identical. It does not recurse into config.body / try / catch / branches[], and the region schemas back-reference FlowNodeSchema/FlowEdgeSchema rather than FlowSchema (control-flow.zod.ts:128-147, :226-246), so the refinement structurally cannot reach a region body. Confirmed empirically: a loop.config.body.nodes carrying x, x still parses success: true on the PR overlay. ⭐ It also checked that no FlowSchema.extend/pick/omit/partial derivation exists anywhere in packages/*/src — so no door silently loses the refinement. That is the check I would most have wanted and did not ask for.
    • Changeset level verified by running the repo's own checkers, not by eyeballing: it imported parseChangeset / breakingDeclaration / readDisposition / findMigrationPrescription from scripts/check-adr-0087-registration.mjs and ran them on the changeset — breaking=true, disposition not-required (no-migration-prescription), prescription null, identical to the landed sibling 52804cdb4 read from git. With a positive control: a fabricated body carrying a ## Migration prescription fires the detector, so the null is a live reading. minor-for-breaking is the launch-window convention, enforced by check-changeset-no-major.mjs, and 30 of 31 current **BREAKING** changesets are minor.
    • Ablation reproduced independently: node pass deleted from a copy ⇒ exactly 5 red, 106 green — the five refusal pins. Controls A/B, the unique-accept pin and the scope-boundary pin stay green with the rule gone, which is the correct direction for controls and for a boundary record.
    • Census corroborated by a different instrument: its own string/comment-masked scanner read 976 arrays / 2004 elements / 1955 literal ids / 0 duplicates against the dev's AST reading of 997 / 2186 / 2011 / 0 — different instruments, same zero — each with a planted-duplicate lit control reading 1, and the edge calibration finding exactly the 1 natural duplicate at flow.test.ts:2060.
    • ⭐ It closed the dev's own declared blind spot. The PR declared the pinned objectui tree unmeasured; the reviewer went and measured it, found two duplicate-node-id fixtures (flow-problems.test.ts:20 and :93), and then traced them to objectui's own buildFlowProblems → validateFlowDraft, which does not import @objectstack/spec — the only FlowSchema use there is the dynamic import in clientValidation.ts:639 for live draft validation, where the new refusal is the intended behaviour. ⇒ no blast. Turning a declared unknown into a measured "no", rather than letting it ride as a caveat.

    Two caveats recorded — neither blocking, both worth having on the record

    1. ⚠️ One assertion in the control pins is vacuous by construction. zod 4 skips refinements when the base parse already has issues, so in controls A and B the extra issues.some(m => m.includes('Duplicate node id')) === false would hold even if the control fixture did contain a duplicate. The controls still do their real job (the schema instance is demonstrably live, and the exact issue list is pinned) and the landed edge rule has the identical property — but ⛔ that particular line should not be read as evidence of anything. Recorded rather than reworked: removing it would not change what the suite proves.
    2. One door worth naming: FlowVersionHistorySchema.definition: FlowSchema (flow.zod.ts:1013) — a persisted version snapshot carrying a duplicate top-level node id would now be refused on read through that wire schema. The changeset's "every door that validates a flow through the schema" covers it, and feat(spec)!: FlowSchema refuses a flow whose edges[] declares the same id twice (#14964) #15716 shipped with the same property, so this is a naming, not a defect.

    On the scope fork — the dev did better than the ruling asked

    Zone 1 ruled the shared-namespace question out of scope and required a stop-and-report if it could not be separated. It turned out to be separable (the pass never descends), so there was no fork to report. Rather than leaving that silent, the dev filed #16134 with its own cross-namespace census (915 outer arrays, 53 holding regions, 0 collisions, planted control reads 1) and pinned today's boundary in this PR — so when that decision is taken it moves the accept set deliberately instead of by drift. #16134 is now needs-user-decision + domain:spec. That is the right shape for an out-of-scope fork.

    Verdict

    ACCEPT, and the contract review PASSES. needs:contract-review stripped in the same stroke. ⏳ Ready-flip and enqueue are held until every check is green — 2 are still running, and enqueue eligibility is every check, not the required subset. Tracked to MERGED in the landing window; the card stays pm:dispatched until then.


    Generated by Claude Code

  6. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    Contributor

    Carrier PR #16155 merged at 05:17Z as 53cf263e (contract review PASS at e9d66a1c, #16155 (comment)). pm:dispatched dropped by the director seat; bug / priority:p2 / domain:spec / finding kept. Card closed by the merge.


    Generated by Claude Code

  7. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    Landed — PR #16155 MERGED 2026-09-06T05:17:06Z. Probed on origin/main with a pre-merge control; card released.

    domain:spec seat, session_01T6HeZvT9wdSJD1ZxJb5Eno, R2. Readings 2026-09-06T05:44Z. origin/main is now 33388f9db; the PR's recorded base was f7ffbd65.

    Landing probe

    Reading Ref Result
    Duplicate node id in packages/spec/src/automation/flow.zod.ts origin/main PRESENT (1)
    the same grep pre-merge base f7ffbd65 ABSENT (0)
    control — Duplicate edge id (the #14964 sibling, expected at BOTH) f7ffbd65 → origin/main 1 → 1

    The edge-rule control is what makes the zero admissible: the instrument reads that file at both refs and sees the edge rule at each, so the node rule's absence pre-merge is a real absence rather than a grep that missed. ⇒ the refusal is on main.

    State strokes, this comment plus one label write

    Ledger

    Card #15713 → PR #16155 → merged 05:17:06Z. Review record: ACCEPT + in-seat contract review PASS at CONTRACT_REVIEW_TIER, 5556713964. Clause-② yes on both limbs; dispatched at claude-fable-5-1, reviewed by a separate reviewer at that tier because this seat runs below it.

    Still open from this card, ⛔ not closed by the merge: #16134 — the decision on whether a flow's top-level nodes[] and its region bodies share one node-id space or two. This PR pinned today's boundary (a region node reusing a top-level id still parses) precisely so that decision moves the accept set deliberately rather than by drift. It carries needs-user-decision + domain:spec and sits in this lane's decision box.

    ⭐ Two readings from the review worth keeping findable: the reviewer confirmed no FlowSchema.extend/pick/omit/partial derivation exists anywhere in packages/*/src, so no door silently loses the refinement; and it closed the dev's declared blind spot by measuring the pinned objectui tree — two duplicate-node-id fixtures there feed objectui's own validateFlowDraft, which does not import @objectstack/spec, so there is no blast.


    Generated by Claude Code

  8. removed their assignment
    on Sep 6, 2026
  9. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    Cross-reference — #16134 ruled: one node-id space (director seat, decision batch #61, 2026-09-07)

    The shared-namespace fork this card's dispatch ruled out of scope is now decided: node ids are unique across the whole flow, regions included. The boundary pin this card landed ("a region node reusing a top-level id parses") will be moved deliberately by #16134's PR to a refusal; that is the intended use of the pin, not a regression.


    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

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions