Repository navigation
service-automation: the runRegion tagger writes branch on parallel-branch steps and carries the enclosing loop's iteration through nesting — the engine half of #14414 (spec PR #15227) #15230
Description
Activity
Unlock note —
domain:specseat,session_01H2oQebDDxYKfWZusyd8GXk, 2026-09-04T08:34Z. The upstream this card names (Blocked-by: #14414) is closed: PR #15227 merged as8ab926beat 08:14Z, andorigin/main4dd5041bcarries the spec half —ExecutionStepLogSchema.iterationis single-valued (the enclosing loop's iteration) and the new optionalbranchkey is declared atpackages/spec/src/automation/execution.zod.ts:151. The engine tagger inpackages/services/service-automation/src/engine.ts(runRegion) still writes the innermost region only and never writesbranch; itsStepLogEntry.iterationcomment still carries the old two-meaning wording; the QA checklist itemautomation.flow-run-step-nestinghas noloop { parallel }clause yet. All of that is this card's. The unlock scan returns the card topm:queueafter re-verifying its file surface on the merged ref; no label is changed by this note.
Generated by Claude Code
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 4, 2026 分诊路由 + 解锁(本评论来自分诊座位)· R+150
domain:services·bug·priority:p2·pm:blocked→pm:queue。解锁现验(
origin/main,fetch 后实测 2026-09-04T19:5xZ,⛔ 未只信卡上的解锁评论):packages/spec/src/automation/execution.zod.ts:151 branch: z.number().int().min(0).optional().describe('Zero-based index of the enclosing `parallel` branch… …When the parallel node is itself inside a loop body, the loop iteration is reported through `iteration`, never here.')⇒
Blocked-by: #14414(PR #15227)的契约半边已在 main 上,连裁决里那句「iteration 单义、branch 另立」都逐字落进了 describe ⇒ 前置放电。引擎侧同一次读取:
git grep -c "parallel-branch" -- packages/services/service-automation/src/engine.ts→ 1(tagger 在),而卡面要求的branch写入尚未出现 ⇒ 缺陷仍在,活没被人顺手干掉。落点:
packages/services/service-automation/src/engine.ts⇒ 车道表packages/services/*归 services(与卡面自陈一致)。bug判据:tagger 今天让最内层区域整个赢掉,把外层 loop 的iteration丢了 —— 写进运行日志的是错的结构化数据,不是缺一个特性 ⇒ Bug 侧。p2:错的是可观测性数据(运行步骤日志的归组),不动执行语义、不丢用户数据 ⇒ 不是 p1。⚠️ 卡面已写、别漏的两条:① 引擎本地StepLogEntry的iteration注释仍写着「Zero-based loop iteration or parallel branch index」,那句 overload 要随之改单义,且与 spec 类型靠 pin 保持同步,不靠散文;②docs/qa/platform-checklist/areas/automation.json的automation.flow-run-step-nesting要补loop { parallel }子句 —— 它今天写不出来,正是因为引擎还没写branch。⛔ objectui 的FlowRunsPanel归组键不在本卡。
Generated by Claude Code
Claim: PM loop round R4
Session:session_01XpTx2tbq3pZRYAdoGt6E6Y
Branch:claude/issue-15230-region-tagger-branch-key
Worktree:objectstack-issue-15230
Domain:domain:services
File surface:packages/services/service-automation/src/engine.ts·docs/qa/platform-checklist/areas/automation.json· a new pin file inpackages/services/service-automation/src/
Container & model: M,mode:subagent
Clause-②: no — 契约增量已由 PR #15227 落在packages/spec/src/**并在那一轮过了契约复审(5536760564);本卡只让引擎去符合它,不碰任何契约文件。⚠️ 若你发现StepLogEntry是导出的、且这次改动不止是加一个可选输出键,停下来报告,由 PM 改判——这条声明是可审计的,不是免检牌。
Serial constraints cleared:engine.ts上一个在飞 PR #16302(#14955)已于本轮落地(main 的提交信息里(#16302)命中 1,仪器带阳性对照);⛔ 其余三张同落engine.ts的卡(#15429 · #15660 · #15812)本轮不并发派,单写者路径一次只许一个。
前置已放电 —— 我在真实 ref 上重测过,⛔ 不是只读卡上的解锁评论
packages/spec/src/automation/execution.zod.ts:157 branch: z.number().int().min(0).optional().describe('Zero-based index of the enclosing `parallel` branch. Present only on a step inside a parallel branch; absent everywhere else. When the parallel node is itself inside a loop body, the loop iteration is reported through `iteration`, never here.')⚠️ 分诊评论记的是:151,我读到的是:157—— main 移动过。认符号不认行号,你也一样。缺陷仍在,同一次读取:
git grep -c "parallel-branch" origin/main -- .../engine.ts → 1 (阳性对照:tagger 在) git grep -n "branch" origin/main -- .../engine.ts | grep -c "regionKind\|StepLogEntry" → 0 (引擎零处写 branch) engine.ts:745 /** Zero-based loop iteration or parallel branch index of the enclosing region. */ (overload 注释还在)⇒ 活没被人顺手干掉。⛔ 动手前请你自己再跑一遍这三条,读数对不上就停下报告。
要做什么 —— 裁决是维护者的,⛔ 不要重开设计
维护者 2026-09-03 的裁定(#14414 评论 5519397028)逐字如下:
iterationis single-valued: the zero-based iteration of the enclosing loop, carried through any nesting … The branch index lives on a new optionalbranchkey, present only on steps inside aparallelbranch. The engine'srunRegiontagger stops discarding the outer region's index for steps the inner region already tagged.三件事,卡面已写全:
runRegion的tag()闭包不再让最内层区域整个赢掉:parallel分支里的步骤拿branch= 分支序号、regionKind: 'parallel-branch';该 parallel 节点若又坐在loopbody 里,同一步骤还要带上外层 loop 的iteration。loop里的try/catch保持今天的行为(iteration 记 loop 的,无branch)。- 引擎本地
StepLogEntry加branch?: number,并把:745那句「loop iteration or parallel branch index」改成单义。⭐ 它与 spec 类型靠 pin 保持同步,不靠散文 —— 卡面明写,请落成断言。 docs/qa/platform-checklist/areas/automation.json的automation.flow-run-step-nesting补loop { parallel }子句。它今天写不出来,正是因为引擎还没写branch。
Pins(裁决自带的三条,一条都不能少)
loop { parallel }fixture:分支步骤同时带 loop iteration 与 branch 序号;try_catch在loop里 —— 不变(这是防回归的对照臂,不是新行为);parallel节点不在 loop 里 —— 写branch,不写iteration。
⭐ 步骤记录必须能在带 PR #15227 的 head 上通过
ExecutionStepLogSchema解析(branch路径拒负数与小数——请把这条也测出来,⛔ 不要只测正例)。边界
⛔ 不碰 objectui 的
FlowRunsPanel归组键(${regionKind}#${iteration})—— 那是 objectui 的卡,且要等带branch的 spec 发版。
⛔ 不取方案 B(保留 overload、另加loopIteration)—— 裁决没取它。
⛔ 不动packages/spec/**:契约半边已经落了。
⚠️ 改完若有测试转红,那是真实读数,报告,⛔ 不要靠调断言让它变绿。交付纪律
⭐ 红先于绿:三条 pin 要先在未改的树上跑出预期的红(并把预测写在跑之前),再实现。变异证明按本车道惯例走 blob hash,恢复腿用
git checkout HEAD -- 绝对路径并以git diff HEAD为空 + hash 相等双证。
⭐ 门族在最终 head 上用node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands机械推导并全跑;退出码紧接单条重定向之后捕获,⛔ 永不走管道。任何 exit 3PREREQUISITE NOT MET都要满足前置后重跑成真读数,⛔ 不得记作 pass、也不得记作红。
⭐ changeset:判断并说理由(引擎写进运行日志的结构化数据变了,flow 作者可观测 ⇒ 大概率不是skip-changeset形状),⛔ 不要默认。
⛔ 不在 PR 正文预测 CI 状态。
⛔ worktree-first,一任务一 worktree;⛔ 永不git stash。domain:servicesPM 席位 · 认领由 PM 在派发时写
Generated by Claude Code
- added 4 commits that reference this issue
on Sep 6, 2026 os-dev-report
{ "issue": 15230, "status": "done", "branch": "claude/issue-15230-region-tagger-branch-key", "pr": "https://github.com/objectstack-ai/objectstack/pull/16367", "premise_still_valid": true, "summary": "Re-ran all four dispatch probes on origin/main 3e270d4e2 before touching anything and every one matched (execution.zod.ts:157 carries `branch`; `parallel-branch` count in engine.ts = 1, the positive control; engine writes `branch` nowhere = 0; engine.ts:745 still carries the overload comment verbatim). runRegion's tag() closure now splits what \"innermost wins\" governs: IDENTITY (parentNodeId / regionKind / retryAttempt) still belongs to the innermost region outright, while INDEX (iteration / branch) is carried through nesting, an enclosing region filling only what the inner one left undefined — so `loop { parallel }` branch steps carry the row on `iteration` AND the branch on `branch`, `loop { loop }` still keeps the inner loop's iteration, and `loop { try_catch }` is byte-for-byte unchanged. parallel-node.ts now passes `branch: i` instead of `iteration: i`; StepLogEntry gains `branch?: number` with the overload comment rewritten single-meaning. Three files are beyond the surface the card named and each is declared in the PR body: builtin/parallel-node.ts (unavoidable — the tagger writes what `grouping` hands it and the branch index is produced at that call site; the alternative is having the shared tagger sniff `regionKind === 'parallel-branch'` and re-home the value), builtin/try-catch-node.ts (COMMENT ONLY — it asserted \"a loop's own tagger can never reach past this one to a try/catch step\", which this change falsifies; the forwarding it explains is untouched), and one sentence of the pending #14456 changeset which said \"`parallel` branch tagging is unchanged\" and would otherwise ship as a flat contradiction in the same release note as mine. Clause-② check performed: StepLogEntry IS exported, but the change is exactly one optional output key, so the stop condition (\"more than one optional output key\") is not met and I did not stop. The QA clause landed at item revision 3, but its fixture does not exist — showcase declares loop / parallel / try_catch as three separate flows and nests none in another — so the clause scores blocked(fixture); filed as #16356 and named in the item's fixtures.requires rather than left silent.", "tests": "RED FIRST, predictions written before the run and held exactly. Unmodified tree (BASE 3e270d4e2, pin file only, committed at 60f48e6d1 to preserve the reading). vitest `Test Files 1 failed (1) | Tests 2 failed | 2 passed (4)`: loop{parallel} failed with received `leafA@iteration=0/branch=undefined` x3 and `leafB@iteration=1/branch=undefined` x3 against expected iteration=0,1,2 per branch — i.e. `branch` never written and `iteration` carrying the BRANCH index, the defect verbatim; bare parallel failed `AssertionError: expected undefined to be +0` at `expect(leafA?.branch).toBe(0)`. The two control arms were GREEN on that same unmodified tree exactly as predicted: `loop { try_catch }` (try/catch already forwards loopFrame.iteration at its own call site) and the spec's refusal of branch -1 / 1.5 at the `branch` path. The TYPE pin is invisible to vitest (esbuild strips types) and was measured under tsc, where it read `src/builtin/region-index-keys.test.ts(70,27): error TS2344: Type 'RegionKeys' does not satisfy the constraint 'keyof StepLogEntry'. Type '\"branch\"' is not assignable to type 'keyof StepLogEntry'.` NOT-MEASURED trap checked, not assumed: `tsc --listFiles` confirms the pin file is in BOTH the build program (tsconfig.json) and the test program (tsconfig.test.json, 575 files), so the green is about that file. GREEN AFTER, at e64991ffa: `pnpm --filter @objectstack/service-automation test` under the shared lock, `os-verify-lock: VERDICT command-exit 0`, `Test Files 121 passed (121) / Tests 1426 passed (1426)`; `typecheck` (tsc --noEmit + check:test-typecheck) EXIT=0, verdict line `check:test-typecheck: OK — @objectstack/service-automation's test layer compiles ... 0 file(s) / 0 error(s)`. GATES: derived mechanically with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (58 commands) plus the 4 rosters it flags as living in a directory one of my paths is in = 62; every exit code captured as `cmd > f 2>&1; EXIT=$?`, never through a pipe. First pass at fde99b1ea: 60 exit 0 and 2 exit 3 PREREQUISITE NOT MET (check:dual-build-cjs-loads and check:type-check-debt, both naming an unbuilt dist/) — recorded as neither pass nor red, the prerequisite satisfied with `turbo run build --filter='./packages/*' --filter='./packages/*/*'` (VERDICT command-exit 0) and both re-run into real readings, the latter printing `check-type-check-coverage --re-measure: OK — 5 ledger entr(ies) re-measured, 55 raw tsc error(s) total, none above its recorded number`. FULL UNION RE-RUN ON THE FINAL COMMIT e64991ffa: 62/62 exit 0, verdict lines quoted from the gates themselves, e.g. `check-platform-checklist: OK — 15 areas, 264 items ... symbol anchors: 624/624 resolved`. LINT: NOT narrowed — `pnpm lint` = `eslint . --no-inline-config` over the whole repo, LINT_EXIT=0. One further commit followed the union (93fa7157b, dropping an unsupported 'through two releases' claim from one JSON string), so per the discipline the affected + ratchet families were re-run on that new head: check:platform-checklist, check-platform-checklist-watchdog, check:nul-bytes, check:doc-authoring, check:pm-governed-prose, check-affected-docs, check-drift-comment, check:keyed-text-bounds, check:pm-label-desc-cap, check:skills-token-ratchet, check:pm-skill-ratchet, check:dual-build-cjs-loads, check:type-check-debt, check:type-check-coverage — all exit 0. NO ABLATION WAS RUN, and I am not reporting one: this change adds no guard whose ability to fail needed proving. The discriminating evidence is the red-first pin pair measured on the unmodified tree and re-measured after, which is why the pin file was committed at its RED state before the implementation commit.", "mcp_calls": "6 — issue_read get, issue_read get_comments, search_issues (one targeted dedup query, non-empty so no control word needed), issue_write create (#16356), create_pull_request, add_issue_comment (this report). The public-repo zero-quota page payload was used first and returned only 2 of the 3 comments (the PM claim was absent from the render), which is why get_comments was spent.", "open_questions": [], "out_of_scope_findings": [ "filed as #16356: app-showcase nests no control-flow construct inside another (loop / parallel / try_catch are three separate flows), so the new automation.flow-run-step-nesting `loop { parallel }` clause can only score blocked(fixture) until such a flow lands" ] }
Generated by Claude Code
ACCEPT — PR #16367. Verified by PM measurement on the PR's own head
93fa7157b, not from the report.The three files beyond the card's named surface — each checked, each legitimate
file why it is in scope builtin/parallel-node.tsUnavoidable: the branch index is produced at that call site. iteration: i→branch: i. The alternative — having the shared tagger sniffregionKind === 'parallel-branch'and re-home the value — would put a second writer of the same meaning into the tagger, which is the shape this card exists to remove.builtin/try-catch-node.tsComment only in the diff. Its old comment asserted "a loop's own tagger can never reach past this one to a try/catch step" — this change makes that false, so leaving it would ship a comment the code contradicts. The forwarding it explains is untouched. .changeset/contained-failure-visibility.md✅ Verified it is main's file, not another PR's. git show origin/main:resolves it; it landed withd30ccb9bd(PR #15609) and is still unreleased. Its sentence "parallelbranch tagging is unchanged" would otherwise ship as a flat contradiction in the same release note as this entry.Clause-② — the declaration held, and the seat checked rather than assumed
My claim declared
Clause-②: nowith an auditable stop condition: stop ifStepLogEntryis exported AND the change is more than one optional output key. Measured:engine.ts:720—export interface StepLogEntry— so it is exported, and the change is exactly one optional output key (branch?: number). The stop condition is not met; the seat correctly did not stop, and said so instead of staying quiet. That is the declaration limb working as designed.The type-level pin is not vacuous — my own ablation, prediction written first
The card asked for the engine interface and the spec type to be kept in step "by a pin, not by prose". A pin nobody has seen fail is prose. Predicted: deleting
branch?: numberfromStepLogEntrymakesRegionKeys(which lists'branch') stop beingkeyof StepLogEntry⇒ TS2344 at the pin. Observed, ontsc --noEmit -p tsconfig.test.json, exit 1:region-index-keys.test.ts(70,5): error TS2344: Type 'false' does not satisfy the constraint 'true' region-index-keys.test.ts(70,27): error TS2344: Type 'RegionKeys' does not satisfy the constraint 'keyof StepLogEntry'Both halves fire — the
Pickhalf and the invariantEqhalf. The seat's report quoted only(70,27); the(70,5)line means a widening on one side would be caught too, not just a missing key.⚠️ And the seat is right that this is invisible tovitest(esbuild strips types): a greenpnpm testsays nothing about it.HEAD blob 8603e9cca6d5f5dfa485cff673ccabd5b18756fe mutated blob b9ef8373fc2494fd7b59ab6125aa214a496dd6b6 restored 8603e9cca6d5f5dfa485cff673ccabd5b18756fe `git diff HEAD` empty, tree cleanBehavioural ablation — mine, prediction written first
Predicted: moving the index writes back inside the
parentNodeId === undefinedblock turns pin 1 red and leaves pins 2 and 3 green, because the try/catch arm gets itsiterationfrom its own call-site forwarding and the bare parallel getsbranchfrom the innermost grouping. Observed exactly:Tests 1 failed | 3 passed (4), the failure being× loop { parallel }: a branch step carries the loop's iteration AND its own branch
AssertionError: expected [ …(6) ] to deeply equal [ 'leafA@iteration=0/branch=0', …(5) ]⇒ The control arm is really a control (it does not move with the change), and the tagger split is load-bearing for the case the card is about. Unablated, the pin file reads
Test Files 1 passed (1) / Tests 4 passed (4).Two things for the maintainer's eye — flagged, not blocking
⚠️ The objectui window is open now, not later. The card fences out objectui'sFlowRunsPanelgrouping key${regionKind}#${iteration}as "a card in objectui, gated on a spec release that carriesbranch" — but that spec half already landed (execution.zod.tscarriesbranch), so the gate the card named is no longer closed. Between this PR releasing and that objectui card landing, everyparallel-branchstep groups underparallel-branch#undefined, becauseiterationis now deliberately absent there. ⛔ I did not verify the objectui side — that repo is not checked out in this session, so this is the card's own citation plus this diff, not a measurement.- The changeset is
minor, reasoned as matching the contract half. Moving a value from one published key to another is arguably breaking for a reader ofiterationon a parallel-branch step; the changeset says so plainly in its "Reading a run recorded before this change" paragraph rather than hiding it. Naming it because the ruling chose the shape, not the seat.
On the QA clause that cannot run
The added
loop { parallel }clause scoresblocked(fixture)— showcase carriesloopandparallelas separate flows and nests neither. The seat wrote the clause anyway, named the gap insidefixtures.requires, and filed #16356. Correct call: the thing the clause pins is settled by the ruling, and a clause missing from this item is exactly what let theiterationoverload sit unmeasured. ⛔ It is recorded as blocked, not as passing.domain:servicesPM seat · verification by local measurement and two independent ablations, not from the seat's report
Generated by Claude Code
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.- Closing pull request: fix(service-automation):
runRegioncarries the outer region's index through nesting; the parallel branch index moves tobranch(#15230) #16367, merged. - Closing commit
65ec530f8c, merged intomain. - Left untouched:
bug,priority:p2,domain:services— ownership, priority and outcome are not state claims. - The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.posted by half-state-patrol run 34055667490 · trigger
scheduleGenerated by Claude Code
- Closing pull request: fix(service-automation):
- added a commit that references this issue
on Sep 9, 2026
Filed by the
domain:specexecution seat (sessionsession_01H2oQebDDxYKfWZusyd8GXk, seat post #6017) at the ACCEPT of PR #15227 (#14414, contract review 5536760564, 2026-09-04T06:43Z). Unassigned;domain:*, type and priority are triage's — this seat does not produce them (the lane table putspackages/services/*underdomain:services).Named reader: the seat that owns
packages/services/service-automation.Blocked-by: #14414
The ruling this executes (maintainer, 2026-09-03, recorded on #14414 as comment 5519397028)
"
iterationis single-valued: the zero-based iteration of the enclosing loop, carried through any nesting … The branch index lives on a new optionalbranchkey, present only on steps inside aparallelbranch. The engine'srunRegiontagger stops discarding the outer region's index for steps the inner region already tagged." Execution order: "the spec half lands first … then the engine tagger inservice-automationas a follow-on card … ⛔ no engine-only patch in between." The spec half is PR #15227 (ExecutionStepLogSchema.branch, theiterationdescribe rewritten) — this card unblocks when it lands.What to build (
packages/services/service-automation/src/engine.ts)runRegiontagger (thetag()closure that fillsparentNodeId/iteration/regionKindonly on steps that do not already carry aparentNodeId) stops letting the innermost region win outright: a step inside aparallelbranch getsbranch= the branch index andregionKind: 'parallel-branch'; when that parallel node sits inside aloopbody, the step also carriesiteration= the enclosing loop's iteration.try/catchinside a loop keeps today's behaviour (loop iteration oniteration, nobranch).StepLogEntryinterface still documentsiterationas "Zero-based loop iteration or parallel branch index" — it gainsbranch?: numberand the single-meaning comment (it is not derived from the spec type; keep the two in step by a pin, not by prose).docs/qa/platform-checklist/areas/automation.jsonitemautomation.flow-run-step-nestinggains itsloop { parallel }clause (the dev of PR feat(spec)!: ExecutionStepLog.iteration is single-valued (the enclosing loop iteration); the parallel branch index moves to a new optionalbranchkey (#14414) #15227 measured it has none; it is writable only once the engine writesbranch).Pins (the ruling's own): a
loop { parallel }fixture where a branch step carries both the loop iteration and its branch index;try_catchinsideloopunchanged; aparallelnode NOT inside a loop writesbranchand noiteration. The step records must still parse underExecutionStepLogSchemafrom the spec at the head that carries PR #15227 (branchrefuses negative / fractional values at thebranchpath).Re-check (symbols, not lines):
Not this card
⛔ Not the objectui
FlowRunsPanelgrouping key (${regionKind}#${iteration}at its:156) — a card in objectui, gated on a spec release that carriesbranch. ⛔ Not a second index key: option B (keep the overload, addloopIteration) was not taken.Refs: #14414 / PR #15227 (the contract half) · #13681 (the
loop { try_catch }containment sibling; its try/catch rule is unchanged) · ADR-0031 (structured regions)