Repository navigation
A parallel branch inside a loop body overloads the step record's iteration with the branch index — the enclosing loop iteration is lost, so a branch step cannot be attributed to its row #14414
Description
Activity
- addedpriority:p2Medium: important, M3Medium: important, M3
on Sep 2, 2026 Triage:
priority:p2·needs-user-decision·domain:spec· typeFeature. The standard four-facet block is now on the card face (decision-inbox duty: completed when a card enters the inbox, not when the maintainer arrives).Why the decision inbox and not the queue. The card's own heading is right — this is a contract question. Both limbs change a published record shape:
- A re-points
iterationforparallel-branchsteps, so any consumer reading it as a branch index breaks. That is a breaking change to a published contract. - B is additive but declares a second index whose presence depends on nesting shape — a new permanent key on a published schema.
Neither is a dev-agent edit, and ⛔ there is no third limb where a seat restores a declared contract mechanically: the schema says "loop iteration OR parallel branch index" and means it. The engine is faithfully implementing an ambiguous declaration.
domain:specbecause the spec half lands first under the three-surface rule and decides the key; theservice-automationtagger change follows it and is that lane's follow-up card, not this one.p2. Nothing fails at runtime and no data is lost — the record is written, it just cannot be attributed. That matters where attribution is the point: a per-row failure inside a branch is recorded against a branch index, so "which row broke" is unanswerable from the log for exactly the nesting a bulk sweep uses. Not p1 because no one has reported it and the shape may be rare — which is the measurement the recommendation asks for.
The recommendation is measurement-first, and both numbers are cheap for the spec seat to produce: how many
loop { parallel }nestings exist in the real flow corpus, and how many consumers readExecutionStepLogSchema.iteration(platform-internal plus published surface). Two zeros make A's cost zero and A is then clearly right on axes ① and ③. Anything non-zero turns this into a genuine migration-cost ruling, which is the maintainer's.⛔ One thing that must not happen while this waits: the engine tagger's "innermost region wins" behaviour is not to be patched on its own. Recording the outer index without deciding which key holds it invents a third shape, and the card is explicit that the engine half follows the spec half.
Sibling, not duplicate: #13681 stays open for its own limbs, and its spec PR deliberately declined the
parallelcase for exactly the reason this card gives —try_catchhas no index of its own,paralleldoes. ⛔ Do not fold them.⛔ Not presented in a batch yet: my batch 1 (#14103 #14168 #14104 #14149) is still unanswered, so this rolls into the next batch with the two measurements attached if the spec seat has produced them by then.
Generated by Claude Code
- A re-points
os-project-manager commented
on Sep 3, 2026 CollaboratorMore actionsMaintainer ruling recorded — A:
ExecutionStepLogSchema.iterationalways means the enclosing LOOP's iteration; the parallel branch index moves to a new optionalbranchkeyDirector seat (objectstack #12708), summon #10, session
session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-03.Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, replying to decision batch #15 in which this card was item 4 with the recommendation A (fallback B: keep the overload, add
loopIteration). The triage facets asked for two numbers before a ruling; the director seat measured them and presented A on their strength. Verbatim reply: 「同意」 — A is adopted as recommended.The two measurements the facets asked for, taken by the director seat on the current checkouts (
git grep, non-test sources):question reading loop { parallel }nestings in the real flow corpus0 — objectstack: the only non-test file carrying both node types ( examples/app-showcase/src/automation/flows/index.ts) has itsloop(:868) andparallel(:929,:1207) as sibling nodes, not nested; hotcrm: none; five objectstack test files carry both (fixtures)consumers reading a step record's iteration1 — objectui packages/app-shell/src/views/metadata-admin/previews/FlowRunsPanel.tsx:156, the grouping key${regionKind}#${iteration}; the objectstack engine (engine.ts:7101) is the writer, not a readerBoth readings are at or near zero, so A's one-time migration face is one grouping key in one objectui component and no stored record in any measured corpus.
Ruled: A.
iterationis single-valued: the zero-based iteration of the enclosing loop, carried through any nesting (try_catchalready carries it per #13681's rule;parallelnow does too). 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.Not taken: B (keep the overload and add
loopIteration— freezes a field with two meanings into the contract and adds an index whose presence depends on nesting shape).Execution: three-surface rule — the spec half lands first (
packages/spec/src/automation/execution.zod.ts: thebranchkey, theiterationdescribe rewritten to the single meaning), then the engine tagger inservice-automationas a follow-on card in the same lane,Blocked-by:the spec PR; ⛔ no engine-only patch in between. A published record shape changes ⇒ Clause-② yes,needs:contract-reviewon the card and the PR atCONTRACT_REVIEW_TIER;@objectstack/specchangesetminorwith a BREAKING banner naming theparallel-branchsteps whoseiterationnow reads the loop iteration. objectui: one card for theFlowRunsPanelgrouping key to includebranch(the ui seat's; not this repo's PR). Pins: aloop { parallel }fixture where a branch step carries both the loop iteration and its branch index;try_catchinsideloopunchanged.State transition, same stroke:
needs-user-decision→pm:queue;priority:p2,domain:specretained. Ledger: objectstack director seat post #12708, summon #10.
Generated by Claude Code
Claim: PM loop round R1 —
domain:specseat (seat post #6017). Executing the maintainer ruling recorded by the director seat (comment 5519397028, 2026-09-03, verbatim reply 「同意」 to batch #15 item 4): A —ExecutionStepLogSchema.iterationalways means the enclosing LOOP's iteration; the parallel branch index moves to a new optionalbranchkey. Spec half only (three-surface rule); the engine tagger and the objectui grouping key are follow-on cards filed at ACCEPT.
Session:session_01H2oQebDDxYKfWZusyd8GXk(GitHubos-justin)
Branch:claude/issue-14414-step-record-branch-key
Worktree:objectstack-issue-14414
Domain:domain:spec
File surface:packages/spec/src/automation/execution.zod.ts(ExecutionStepLogSchema: the new optionalbranchkey; theiterationdescribe rewritten to the single meaning; theregionKinddescribe's cross-reference adjusted),packages/spec/src/automation/execution.test.ts(pins: aloop { parallel }fixture where a branch step carries both the loop iteration and its branch index;try_catchinsideloopunchanged;branchrefused outside a parallel-branch step only if the schema can state that — otherwise a describe sentence, no invented refinement), generated followers the gates prove stale (content/docs/references/automation/execution.mdx, json-schema manifest, strictness-ledger counts), the ADR-0087 disposition the changeset needs (a D3 semantic entry underpackages/spec/src/migrations/entries/semantic/+ regeneratedregistry.ts, if that is the disposition the gate accepts), a@objectstack/specchangeset (minor+ BREAKING banner naming theparallel-branchsteps whoseiterationnow reads the loop iteration). ⛔ Notpackages/services/service-automation/**(the tagger —domain:servicesfollow-on), not objectui. Stop on breach; explain in the report.
Container & model: S–M,mode:subagent,model: fable—--tier packages/spec/src/automation/execution.zod.tsatc351a84(05:32Z): no path-derived mandate, Clause ② SUSPECT hint; judged from content (the ruling's own words): a published record shape changes ⇒CONTRACT_REVIEW_TIER.
Clause-②: yes
Serial constraints cleared:packages/spec/src/automation/**— free on the seat post (f3bbbef5, #14149 landed);execution.zod.tslast touchedd261cef(2026-09-03); 0 of 18 open PRs at 05:31Z have a head on it (branch-name read; dev re-checks at worktree time). Sibling #13681 (pm:blocked,loop { try_catch }containment) — its try/catchiterationrule is already onmain(execution.zod.ts:137) and is kept verbatim by this card; no read coupling. The migrations registry (packages/spec/src/migrations/registry.ts) is generated and also touched by PR #15139 (#14180, queued 05:15Z) — os-regen regenerates on the second merge, no hand merge. H17 index: no hit.
Generated by Claude Code
Dispatch (R1, 2026-09-04T05:40Z) —
domain:specseat,session_01H2oQebDDxYKfWZusyd8GXk(os-justin), seat post #6017.mode:subagent,model: fable(CONTRACT_REVIEW_TIER), size S–M, Clause ② yes (dual carrier: the dev hangsneeds:contract-reviewon the PR and on this card in one stroke when the draft PR exists — no pre-hang). The dev leaves its ownClaim:comment below before its first edit.Rulings (not re-decidable). Maintainer ruling recorded 5519397028 (2026-09-03, 「同意」), operative sentences verbatim: "
iterationis single-valued: the zero-based iteration of the enclosing loop, carried through any nesting (try_catchalready carries it per #13681's rule;parallelnow does too). The branch index lives on a new optionalbranchkey, present only on steps inside aparallelbranch." "Not taken: B." "Execution: three-surface rule — the spec half lands first (packages/spec/src/automation/execution.zod.ts: thebranchkey, theiterationdescribe rewritten to the single meaning) … ⛔ no engine-only patch in between. A published record shape changes ⇒ Clause-② yes …@objectstack/specchangesetminorwith a BREAKING banner naming theparallel-branchsteps whoseiterationnow reads the loop iteration. … Pins: aloop { parallel }fixture where a branch step carries both the loop iteration and its branch index;try_catchinsideloopunchanged." Spec half only: ⛔ notpackages/services/service-automation/**, ⛔ not objectui (both are follow-on cards the seat files at ACCEPT).PM mechanism assumptions (verify; falsify freely). (a) The declared-breaking changeset needs an ADR-0087 disposition
scripts/check-adr-0087-registration.mjsaccepts; the precedent from this lane today is a D3 semantic entry underpackages/spec/src/migrations/entries/semantic/withregistry.tsregenerated bygen:migration-registry(PR #15139's shape) — read the gate's header for the disposition vocabulary before choosing; if no disposition fits a runtime-record semantic change, ⛔ do not drop the banner and do not invent a category: stop and report the gate's output as the fork (that gap has a card, #14502). (b) Whether anything parsesExecutionStepLogSchemaat runtime decides the pin strength: find the parse site(s); if the record is validated at write time, a refinement that refusesbranchoutsideregionKind: 'parallel-branch'is in scope and preferred (loud over silent); if nothing parses it at runtime, a describe sentence is the ceiling — report which. (c) Generated followers the gates prove stale:content/docs/references/automation/execution.mdx, json-schema manifest, strictness-ledger counts. (d) The engine's local step type (service-automation/src/engine.ts, its owniteration?: numberinterface) is not derived from the spec type, so the spec change alone leaves that package green; verify with its typecheck and its execution/observability tests and report the readings (consumer-package readings are required for a contract card; state closure build and filter direction).PM suggested route (optional). Add
branch: z.number().int().min(0).optional()besideiterationwith a describe that says "zero-based index of the enclosingparallelbranch; present only on steps inside a parallel branch"; rewriteiteration's describe to the single meaning, keeping the existing try/catch sentence; adjustregionKind's describe cross-reference; pins inexecution.test.ts: theloop { parallel }step fixture (iteration: 3, branch: 1, regionKind: 'parallel-branch') parses and both survive; a try step inside a loop keepsiterationand nobranch; a negative or fractionalbranchis refused.Surface, gates, report. File surface = the claim comment 5536193639 (stop on breach). Derive gates from your actual paths after the last commit (
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands <paths>); the seat's derivation atc351a84named the speccheck:*families,check:generated --fix,check:migration-registry,check-adr-0087-registration --base origin/main,check-changeset-no-major,check-system-context-census(--fixif demanded),check:spec-parsed-alias,check-affected-docs,check:doc-authoring,check:nul-bytes. Report:os-dev-reportfirst as a comment here, then as the return; machine-checkable fields only (status, pr, premise_still_valid, files_changed, gates pinned to the head sha, deviations from the claim surface, open_questions, out_of_scope_findings filed as cards after a dedup search).
Generated by Claude Code
Claim:
Session:session_01H2oQebDDxYKfWZusyd8GXk(dev subagent dispatched by thedomain:specPM seat, seat post #6017; GitHubos-justin)
Branch:claude/issue-14414-step-record-branch-key(pushed empty fromorigin/main919becaas the write-route probe; worktreeobjectstack-issue-14414)
File surface:packages/spec/src/automation/execution.zod.ts(new optionalbranchkey onExecutionStepLogSchema;iterationdescribe rewritten to the single loop meaning, try/catch sentence kept verbatim;regionKinddescribe cross-reference adjusted),packages/spec/src/automation/execution.test.ts(pins:loop { parallel }branch step carrying bothiterationandbranch;tryinsideloopunchanged andbranch-free; negative / fractionalbranchrefused with the zod issue path asserted), one ADR-0087 semantic entry underpackages/spec/src/migrations/entries/semantic/plus regeneratedpackages/spec/src/migrations/registry.ts, generated followers the gates prove stale (content/docs/references/automation/execution.mdx, json-schema manifest,spec-changes.json,docs/protocol-upgrade-guide.md, authorable-surface anchor), one@objectstack/specchangeset (minor, BREAKING banner). Notpackages/services/service-automation/**, not objectui, nevercontent/docs/releases/**.
Premise re-verified onorigin/main919beca:execution.zod.ts:137still declaresiterationwith the two-meaning describe, nobranchkey exists, and the try/catch sentence from the sibling card is present. Runtime parse sites ofExecutionStepLogSchema/ExecutionLogSchema: none outsidepackages/spec(only the schema's own tests and a type-alias pin) — so the describe sentence is the ceiling for "present only inside a parallel branch"; no refinement invented.
Clause-②: yes
Generated by Claude Code
os-dev-report
{ "issue": 14414, "status": "done", "branch": "claude/issue-14414-step-record-branch-key", "pr": "https://github.com/objectstack-ai/objectstack/pull/15227", "head_sha": "a87a736b", "base": "origin/main 919beca (worktree base; c351a84 named in the dispatch had already moved by worktree time)", "premise_still_valid": true, "premise_evidence": "execution.zod.ts:137 on 919beca declared iteration with the two-meaning describe ('Zero-based loop iteration or parallel branch index of the enclosing region. ...'); no branch key in packages/spec/src; the try/catch sentence from the sibling card present and kept verbatim. Runtime parse sites of ExecutionStepLogSchema / ExecutionLogSchema outside packages/spec: none (git grep; hits are spec tests, ExecutionLogSchema.steps, api/automation-api.zod.ts response schemas, a type-alias pin, engine.ts comments) => describe sentence is the ceiling, no refinement invented (mechanism assumption b, direction 'nothing parses at runtime').", "summary": "Spec half only. ExecutionStepLogSchema gains optional branch (integer >= 0, present only on a step inside a parallel branch); iteration describe rewritten to the single loop meaning with the try/catch sentence byte-identical; regionKind describe cross-references branch. ADR-0087 D3 semantic entry execution-step-iteration-single-valued registered (registry.ts regenerated); @objectstack/spec changeset minor with BREAKING banner and the registered marker; generated followers execution.mdx and authorable-surface/automation.json regenerated by the gates. Engine tagger and objectui grouping key untouched per ruling. Assignee untouched (was empty at pickup; PM-side half state).", "files_changed": [ "packages/spec/src/automation/execution.zod.ts", "packages/spec/src/automation/execution.test.ts", "packages/spec/src/migrations/entries/semantic/18.execution-step-iteration-single-valued.ts", "packages/spec/src/migrations/registry.ts", ".changeset/execution-step-branch-key.md", "content/docs/references/automation/execution.mdx", "packages/spec/authorable-surface/automation.json" ], "deviations_from_claim_surface": "none. Followers named as possible in the claim but proven current by the gates and therefore untouched: json-schema manifest, spec-changes.json, docs/protocol-upgrade-guide.md (PROTOCOL_VERSION 17.0.0; step-18 entries are not projected until that major ships), authorable-surface.base.json anchor (manual-only, check:authorable-surface green).", "tests": "spec pins (vitest, under os-verify-lock): 'Tests 42 passed (42)' at a87a736b. Reverse verification: committed first; branch declaration line deleted under a trap EXIT INT TERM restore; mutation on disk by anchor count 1 -> 0 (HEAD blob 1816cc70..., mutated blob 0c3b44eb...); no build needed (pins import ./execution.zod from source); observed 'Tests 4 failed | 38 passed (42)' = exactly the four new branch pins red, try/catch pin green, direction as predicted; restore via git checkout HEAD -- absolute-path, proven by hash-object == HEAD blob, git diff HEAD empty, git status --porcelain empty. Consumer (downstream of @objectstack/spec), closure built first (pnpm --filter '@objectstack/service-automation^...' build, 21 packages, exit 0): package has NO typecheck script (filtered run would be a zero-match false green) so direct 'pnpm exec tsc --noEmit -p packages/services/service-automation/tsconfig.json' => exit 2, 3 errors, all TS2341 at src/nested-region-parity.test.ts 95/151/180 = the ratchet ledger row for this package (check-type-check-coverage.mjs: errors: 3, same lines), file not in this diff, 0 errors mention branch/iteration/ExecutionStepLog; region/observability tests (nested-region-parity, builtin/parallel-node, loop-node, try-catch-node, try-catch-failed-attempt-steps, try-catch-returned-failure-steps, loop-dying-body-steps, run-summary, run-history, trigger-dispatch-observability): 'Test Files 10 passed (10) / Tests 152 passed (152)'.", "gates": { "head_sha": "a87a736b", "pnpm --filter @objectstack/spec typecheck": "0 — tsc --noEmit clean; 'check:test-typecheck: OK — ... 54 file(s) / 261 error(s) / 145 pinned signature(s) held' (unchanged)", "pnpm --filter '@objectstack/spec...' build": "0 — 'check-dts-emitted: @objectstack/spec - 34/34 declared declaration file(s) present.'", "check:generated --fix, then clean run": "0 / 0 — '--fix: regenerating 1 of the 1 stale artifact(s)' (check:docs) / '✓ All 15 generated artifacts are up to date.'", "check:migration-registry": "0 — '✓ src/migrations/registry.ts is current (149 semantic, 86 retired-key, 92 retired-def)'", "check-adr-0087-registration --base origin/main": "0 — '✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.' / '[BREAKING+bang] registered execution-step-iteration-single-valued (new here: execution-step-iteration-single-valued)'", "check-changeset-no-major --base origin/main": "0 — '✓ This diff introduces no major bump.'", "check-system-context-census": "0 — 'check-system-context-census: OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read.' (no --fix demanded)", "check:spec-parsed-alias": "0 — 'ADR-0122 type-alias convention: 1521 bare z.input aliases, 830 pinned isomorphic, 691 paired with an XParsed. OK'", "docs-audit/check-affected-docs": "0 — pass (route-ledger reachability table; trailing help text last line)", "check:doc-authoring": "0 — '✓ doc authoring guard: sibling-package prose ids hold the baseline — 831 pinned site(s) across 231 file(s) ... no growth, no burn-down unrecorded.'", "check:nul-bytes": "0 — 'check-nul-bytes: OK (scanned 8263 text file(s) ... no raw ASCII control bytes).'", "eslint --no-inline-config (4 edited/regenerated TS files)": "0 — no output", "check:api-surface": "0 — '@objectstack/spec public API surface + factory signatures unchanged ✓'", "check:authorable-surface": "0 — '✅ Successfully generated 1604 schemas.'", "check:docs / check:export-origins / check:skill-refs": "0 — '✅ 230 generated files in sync' / '✅ export-origins/ is current: 5263 exports across 17 entry points' / '✅ 9 generated files in sync'", "check:liveness": "0 — '✓ every governed-type property ... is classified ... no ledger row outlives its property' + '✓ liveness/state-counts.md is current — the same 35 row(s)' (ExecutionStepLog is a runtime record, not a governed metadata type; no row asked for)", "check:strictness-ledger": "0 — '✓ ... counts.md is current — 440 site(s) measured, 1 authorable strip site(s) left.'", "check:dual-source-exports / check:exported-any / check:entry-nameability / check:browser-reachable-entries": "0 — '✅ no new dual-source exports: 5054 names across 17 entry points' / '✅ no exported type resolves to any: 2459 types + 1526 schemas' / '✅ entry-nameability: 434 call probes across 17 public entries, 0 new unnameable' (its own trailing line: 'NOT MEASURED: no callable export on @objectstack/spec/qa') / '✅ ... 44 bundle(s) scanned'", "check:empty-state / check:llms-txt / check:variant-docs / check:yaml-examples / check:skill-examples": "0 — '✓ all classified' / '✓ packages/spec/llms.txt: 97 claim(s) re-derived' / '✓ variant/doc gate: 18 discriminated union(s)' / pass / '✅ 257 prose examples type-check across 3 surface(s)' (after building client-react)", "dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths, from merge base)": "0 — 85 commands derived; all run with per-command exits captured by redirect. First pass 75 green; 6 dist-reading spec gates exit 1 'dist ... predate the sources' (mtime precheck tripped by the ablation restore, content byte-identical) => green after spec rebuild (quoted above); 2 lint doc gates exit 3 'PREREQUISITE NOT MET — @objectstack/formula is not built' => green after building formula ('✅ 27 ObjectSchema.create example(s) ... carry an os validate-clean security' / doc-formula-expressions pass); 2 NOT MEASURED locally, exit 3 by their own text and CI-owned: 'pnpm check:dual-build-cjs-loads' ('PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ ... 77 more') and 'pnpm check:type-check-debt' (same class, whole-repo build). Named families newly derived and run beyond the dispatch list: check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage, docs-redirects, docs-single-h1, docs-audit-scope and the rest of the 85 — all exit 0.", "ci": "in_progress at report time (draft PR just opened; PM owns convergence)" }, "labels": { "channel": "repo-scoped REST 403 for this session (gh absent; container curl token refused) => MCP union-write per fallback, compare-read-back done", "issue 14414 read-back": "priority:p2, pm:dispatched, domain:spec, needs:contract-review (4 = union of read set + target)", "pr 15227 read-back": "documentation, size/m, tests, tooling, needs:contract-review (union grew by the auto-labeler's three; nothing stripped)" }, "mcp_calls": "12 — 10 before this comment (1 get_comments, 1 claim comment, 2 get_labels on the issue, 1 create_pull_request, 2 issue_write label unions, 1 get_labels on the PR number which errored 'Could not resolve to an Issue', 2 pull_request_read), plus this comment and its read-back", "open_questions": [], "out_of_scope_findings": [ "not filed (inside the follow-on tagger card the seat files at ACCEPT, Blocked-by PR 15227): packages/services/service-automation/src/engine.ts StepLogEntry.iteration doc comment still reads 'Zero-based loop iteration or parallel branch index'; the runRegion tagger writes innermost region only and never writes branch", "not filed (same card): docs/qa/platform-checklist/areas/automation.json item automation.flow-run-step-nesting has no loop { parallel } clause — writable only once the engine writes branch", "not filed (ui seat's card per ruling): objectui FlowRunsPanel.tsx:156 grouping key regionKind#iteration must read branch for parallel-branch steps" ], "cleanup": "worktree /home/user/objectstack-issue-14414 removed without force after node_modules removal (git worktree remove exit 0); no dev server or background monitor started" }
Generated by Claude Code
Contract review (Clause ②) on PR #15227, head
a87a736b— PASS · ACCEPT, 2026-09-04T06:43Z, by thedomain:specPM seat (in-seat review per the 2026-08-31 ruling; seat post #6017). Tier fuse read 06:41Z viaget_session:session_context.modelandlast_served_modelbothclaude-fable-5-1=CONTRACT_REVIEW_TIER.Implemented-by: claude/issue-14414-step-record-branch-key (mode:subagent dev under session_01H2oQebDDxYKfWZusyd8GXk)
Reviewed-by: session_01H2oQebDDxYKfWZusyd8GXk① Derived judgments (from the diff): (a)
ExecutionStepLogSchemagains one optional memberbranch: z.number().int().min(0).optional()besideiteration; theiterationdescribe now states the single meaning (enclosingloopiteration through any nesting) with the try/catch sentence byte-identical;regionKind's describe points theparallel-branchindex atbranch. The accept set widens by one optional key and nothing else; the record's meaning forparallel-branchsteps moves as ruled (5519397028, option A; B not taken). Correct. (b) No refinement refusingbranchoutside a parallel branch — verified reasoning: no runtime parse site forExecutionStepLogSchemaoutsidepackages/spec(the dev'sgit grep; the engine's ownStepLogEntryinterface is not derived from the spec type), so the describe sentence is the ceiling — the dispatch's assumption (b) in its "nothing parses at runtime" direction. Accepted. (c) ADR-0087 disposition: a D3 semantic entryexecution-step-iteration-single-valued(hand-migration prescription: a consumer-side read move, not a data rewrite — a stored record predating the engine follow-on cannot be rewritten mechanically),registry.tsregenerated; prose without issue ids or call parentheses. Correct — the only fitting disposition, as the PR body argues (runtime-interface-onlyclosed by the*.zod.tsdeclaration site,no-migration-prescriptionclosed by the FROM → TO block). (d) Pins:loop { parallel }branch step carriesiteration: 3+branch: 1; the try-in-loop pin now also assertsbranchabsent (not an own property);branch: 0survives;-1→too_small,1.5→invalid_type, each exactly one issue at["branch"]. Ablation: exactly the fourbranchpins red with the member removed. (e) Generated followers:references/automation/execution.mdx(both tables),authorable-surface/automation.json+1; json-schema manifest and strictness counts not stale (gates say so);api-surfaceunchanged.② Semver:
@objectstack/specminor+ BREAKING banner +<!-- adr-0087: registered … -->marker + FROM → TO — the launch-window lockstep convention the ruling named;check-adr-0087-registrationandcheck-changeset-no-majorgreen. Consistent.③ Boundary flags:
open_questionsnone; deviations from the claim surface none. Out-of-scope items the dev did not file are the follow-on cards this seat files now: theservice-automationtagger (runRegionwritesbranchand carries the outer loop iteration through nesting; theStepLogEntry.iterationcomment; the QA checklist itemautomation.flow-run-step-nestinggains itsloop { parallel }clause), and the objectuiFlowRunsPanelgrouping key. Consumer readings present with direction and closure:@objectstack/service-automationclosure built upstream, directtscshows only the 3 ledgeredTS2341(not this diff's), 10 region/observability test files 152 green.Carrier stroke:
needs:contract-reviewcleared on both carriers (PR #15227 and this card) in this stroke — read-modify-write, read back below.check-clause2-carriers --paircannot run in this session (REST 403 on the injected token, see the seat post); the pair reading is taken through MCP on both carriers.Landing: CI on
a87a736bstarted 06:38Z.⚠️ packages/spec/src/migrations/registry.tsmoved onmainat50d6c924(#14180's entries) after this branch's base919beca—mergeable_statereadsblocked, notdirty, so git merges the hunks; if the queue dequeues on that file, the os-regen driver regenerates (no hand merge). On all-green: ready → squash auto-merge → queue; on MERGED this card closes viaFixes,pm:dispatchedstrips, landing note here with theexecution.zod.tsprobe.
Generated by Claude Code
Follow-ups filed at ACCEPT (2026-09-04T06:45Z): the engine tagger half is #15230 (
packages/services/service-automation,Blocked-by: #14414,pm:blocked,domain:*triage's); the objectui grouping-key half is objectui#7614 (pm:on-holdwith an installabilityRestart-when:, the same gate as objectui#7597). Carrier read-back: this cardpriority:p2·pm:dispatched·domain:spec; PR #15227documentation·size/m·tests·tooling—needs:contract-reviewgone on both, nothing else stripped.
Generated by Claude Code
Landing provenance (2026-09-04T07:07Z,
domain:specseat,session_01H2oQebDDxYKfWZusyd8GXk): PR #15227 heada87a736b— all 41 check runssuccessorskipped(Test Core rollup 07:03:39Z, Type Check rollup 06:52:39Z, Lint & Repo Gates green, Spec property liveness green), governed-surface predicate 0 of 7. Clause ② PASS · ACCEPT 5536760564 is the review of record; both carriers clear (read back 06:45Z). Flipped ready → squash auto-merge → merge queue; on MERGED:pm:dispatchedstripped, landing note here with theexecution.zod.ts+registry.tsprobes onorigin/main, #15230 unlocks via itsBlocked-by: #14414.
Generated by Claude Code
Landed —
domain:specseat,session_01H2oQebDDxYKfWZusyd8GXk, 2026-09-04T08:34Z. PR #15227 MERGED 08:14:18Z through the merge queue as8ab926be(squash);Fixesclosed this card at 08:14:19Z. Probed onorigin/main4dd5041b(08:32Z):packages/spec/src/automation/execution.zod.ts:151carriesbranch: z.number().int().min(0).optional()with the single-meaning describe, andpackages/spec/src/migrations/entries/semantic/18.execution-step-iteration-single-valued.tsis present.pm:dispatchedstripped (read-modify-write:priority:p2·domain:speckept). Clause ② carriers were cleared at PASS (5536760564). Follow-on: #15230 (theservice-automationtagger half,Blocked-by: #14414) is unlocked by this merge — note posted there; the objectuiFlowRunsPanelgrouping key is objectui#7614 (pm:on-hold, installabilityRestart-when:).
Generated by Claude Code
- added a commit that references this issue
on Sep 6, 2026
Observation (measured on
origin/main, read-only)ExecutionStepLogSchema.iteration(packages/spec/src/automation/execution.zod.ts) is declared as "Zero-based loop iteration OR parallel branch index of the enclosing region" — one field, two meanings. The engine'srunRegiontagger (packages/services/service-automation/src/engine.ts, thetag()closure) fillsparentNodeId/iteration/regionKindonly on steps that do not already carry aparentNodeId, so the innermost region wins and the outer one is never recorded:loop { body: [ parallel { branches: [A, B] } ] }— a step inside branch B of loop iteration 3 is taggedparentNodeId: parallelId, regionKind: 'parallel-branch', iteration: 1(the branch index). The loop's iteration3appears on no step of that branch. A per-row failure or write inside a branch is attributable to a branch, not to a row.This is the same class of gap #13681's retriage measured for
try_catch(gap ii, "iteration attribution"), one construct over. That card's spec PR states the try/catch rule — a try/catch region has no index of its own, so it carries the enclosing loop'siteration— and deliberately does not decide theparallelcase, because there the branch index and the loop iteration genuinely compete for one field:parallelDOES have an index of its own.Why it is a contract question, not an engine patch
Two readings are possible and they shape different records:
iterationalways means the enclosing LOOP's iteration; the branch index moves to a new optionalbranchkey on the step record (additive). Row attribution works through any nesting; consumers that readiterationas "branch index" forregionKind: 'parallel-branch'would need to move tobranch.loopIteration), leavingiterationas declared today. No existing reader moves; the record gains a second index whose presence depends on nesting.Either way the engine's tagger has to stop discarding the outer region's index for steps the inner region already tagged. The spec half (which key, which describe) lands first under the three-surface rule; the engine half follows it.
Scope note
Context: #13681 (the
loop { try_catch }containment card — this finding is not addressed there and #13681 stays open for its own limbs). Filed unassigned from the spec seat while implementing that card's contract half; no fix attempted.<!-- os-decision-facets -->⚠️ 但 A 会动到现有读者,这一点不能被①的方向掩盖。⚠️ 未测,而且这是本卡最关键的空白 —— 不知道真实 flow 语料里有几处
① 项目长远合理性(权重 ≥50%):一个字段两种含义,本身就是缺陷形状。A 让
iteration恒指「哪一行」,把分支序号搬到自己的键上 —— 这是收窄:读者不再需要先判断regionKind才知道手里的数字是什么。B 把重载固化成永久契约,再加第二个索引,而且第二个索引的存在与否取决于嵌套 —— 按本轴本义读,这是特例增生。② 实际业务拉动:
loop { parallel }嵌套,也不知道平台内外有几个消费者在读iteration。这两个数直接决定 A 的代价是零还是很大。③ 防 AI 犯错:今天这个字段是 AI 写消费代码时最容易读错的形状 —— 它得按
regionKind分支才知道数字的含义,而分支写错时不会响亮拒绝,只会静默把某一行的失败记到错误的行上。A 让它读不错;B 要求每个读者永远记住规则,而规则本身依赖嵌套形状。④ 创业阶段不扩散:两支都 +1 个声明键。差别是 A 用这一个键换来
iteration单义(净减一个隐含规则),B 用这一个键换来「不动现有读者」(净加一个隐含规则)。推荐:先量一格再裁 —— 请让 spec 席先答两个数:(1) 真实 flow 语料里
loop { parallel }嵌套有几处;(2) 读ExecutionStepLogSchema.iteration的消费者有几个(平台内 + 已发布客户面)。两数皆零 ⇒ A 的破坏面为零,荐 A(①③ 同向,且换来单义);非零 ⇒ 破坏面是真的,由维护者在「A 的一次性迁移」与「B 的永久重载」之间裁。⛔ 席位不代裁:两支都改已发布记录形状,属协议/公开契约变化,恒人工。置信缺口(本分析看不见什么):看不见
iteration的既有消费者 —— 而 A 的全部代价恰好只在那里;也看不见有没有真实部署已经在跑loop { parallel },所以「今天谁撞上」这一轴是空的,不是零。Generated by Claude Code