feat(spec)!: a structured region body refuses a pause-capable node and an 'end' node - #18688
Conversation
…tured region Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
…d an end node An ADR-0031 region body runs synchronously inside the enclosing run, so it can neither park that run on a durable pause nor terminate it. The engine already refused both at run time, silently and after the executor had written its progress state into the enclosing scope. FlowSchema now refuses the shapes at parse, naming the node and the region. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 63832c83c862ea414b73672c6005d41ed26e6857 && git checkout 63832c83c862ea414b73672c6005d41ed26e6857
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2767af8e8354511f9c82ce402b147ad12c512819 6de9d662f6df5e38be9303647845704a357e6b50 && git checkout -B drift-repro 2767af8e8354511f9c82ce402b147ad12c512819 && git merge --no-ff 6de9d662f6df5e38be9303647845704a357e6b50
node scripts/docs-audit/affected-docs.mjs --json 2767af8e8354511f9c82ce402b147ad12c512819
|
…arse refusal `registerFlow` parses through `FlowSchema` (`canonicalizeStoredFlow`), so the region-nested refusing `end` this case registered can no longer be registered at all — the refusal it asserted at the region boundary is now met one door earlier, at load. The fixture is unchanged and the case still fails the day the shape becomes declarable again; what it no longer covers (`runRegion`'s `isRefusalSignal` arm, now reachable only past `MAX_REGION_DEPTH`) is stated in the docblock rather than left to be discovered. The changeset's scope line said `packages/services` is untouched and the run-time refusal stays exactly as it was. Measured: registration and the ADR-0087 stored-row rehydration seam both parse, so a stored row carrying a refused shape stops loading, and the `end` arm's run-time refusal has no other caller. Corrected in place. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
… types Ruling batch #153 item 1, letter D: inside `loop` / `parallel` branch / `try_catch` bodies at any depth the refused population is `screen`, `wait`, `approval`, `approval_revise` and `end`. `map` and `subflow` are not refused by type — they pause exactly when the child flow `config.flowName` names pauses, a record this parse does not hold, so a type-keyed refusal would also refuse `loop { map(synchronous child) }`, a shape that runs correctly. `FLOW_PAUSE_CAPABLE_NODE_TYPES` (unreleased, added on this branch) becomes `FLOW_UNCONDITIONAL_PAUSE_NODE_TYPES` so the exported name states the population the rule keys on rather than a capability list two of whose members it does not judge. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
Seat decision on the conflict the rename surfaced: ruling D orders a `minor` for `@objectstack/spec`, and `check:api-surface` grades a removed export breaking, so the rename and the ruling cannot both stand. The ruling asks for a change to the refused POPULATION, not to the export's name. `FLOW_PAUSE_CAPABLE_NODE_TYPES` keeps its identifier and its place in `api-surface/automation.json`; only its contents narrow to the four types that pause unconditionally. The docblock now leads with "read the contents, not the name" and states why the name is kept, so the mismatch is declared rather than discovered. `src/migrations/registry.ts` is regenerated from the edited ADR-0087 entry. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
The declaration-text gate (check:api-surface-declarations) and its shards landed on main after this branch point, so the gate could not be run here at all. Merging brings it in; the shard regeneration is judged separately. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
`check:api-surface-declarations` landed on main after this branch point; the
merge brings it in. Its delta on this branch is 0 removed / 1 added /
2 reshaped, and all three are non-narrowing:
+ FLOW_PAUSE_CAPABLE_NODE_TYPES — introduced by this PR; it is on neither
main's api-surface nor main's declaration shard, so "added" is accurate.
~ ApprovalDecision, ApprovalNodeConfigSchema — property ORDER inside their
`z.ZodEnum<{...}>` type literals, same members, same literal values.
Object type members are order-insensitive in TypeScript, so old and new
are mutually assignable; proved with a two-direction assignability probe
plus a `@ts-expect-error` negative control, tsc exit 0.
Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <noreply@anthropic.com>
Contract review — PR #18688Served-tier: CONTRACT_REVIEW_TIER Reviewed against ruling D (comment ① Derived judgments — the accept set and the public surface, each named and judged
Claims 1–8 from the brief — verified with my own runs
⭐ The seat's correction — verified independently: the seat is right.
② Clause ② and semver
③ Boundary flags — each with why it does not block
NOT MEASURED
Scratch (all of it under PASS — Generated by Claude Code |
✅ 达档合约复核 PASS —— 落地 head
|
| 消融 | 读数 |
|---|---|
把 'map' 加进常量 |
恰好 4 条红(map/subflow 的排除钉),其余一条不动;还原后 blob 与 HEAD 相同,porcelain 0 |
把规则本身停掉(=== 0 → >= 0) |
18 红 / 11 绿 —— 红的是全部拒绝断言,绿的恰是过度拒绝护栏与边界钉 |
⇒ 规则可证伪,且不过度拒绝。基线与还原都是 29 passed (29)。
⭐ 它还复现了本席点名的假绿对照:从 service-automation 目录 require.resolve('@objectstack/spec/automation') 落在 dist/ 上,产物读出 ["screen","wait","approval","approval_revise"],而未动的兄弟常量 FLOW_STRUCTURAL_NODE_TYPES 仍读 ["start","end"](证明这个读数不是恒定值),mtime 是本次构建。⇒ 这条绿不是陈旧 dist 给的假绿。
⛔ 本席被逮到两条,两条都实测复验、当场认下
① 本席用了一个过期的仪器,而且这正是本席今天警告过别人两次的坑。
本席多次报 --pair 18688 exit 0,还据此在正文里写「不 actionable」。实测:
| 脚本 | blob | 含 C8 |
|---|---|---|
| 共享检出(本席一直在跑的那个) | ccd5ad7c9a00 |
0 |
origin/main 与本 PR head |
3a270ef2eb5f |
18 |
亮对照 C1 两边 50 / 51 ⇒ 读法没问题,差的是脚本。C8 规则 01:41Z 随 #18859 落在 main 上,本席的共享检出停在自己的开发分支上,从来没拿到过。
⇒ 本席今天所有 --pair 读数都是无 C8 的脚本取的。 已用 main 的脚本全部重取:#18688 = exit 4、#18638 = 0、#18890 = 0。⇒ 真有问题的只有本卡,但纪律问题是系统性的。
C8 说的是:本席在本卡上持有两条存活的 Claim:,而协议禁止第二条 Claim:。已按 C8 点名的修法修:Release:(5729634742)+ 唯一一条新 Claim:(5729639847)⇒ --pair 18688 现在 exit 0,claim.selected 1、claim.rejected 2。
② 「packages/services 零路径」只对本轮成立,对 PR 累计 diff 不成立 —— 而且这是一条对裁决字面的偏离。
裁决 D 第 1 条写「packages/services untouched;那 5 条测试与 #15616 的套件照旧」,而 #15788 那条 region-end 用例正是那 5 条之一。⇒ 裁决那句「B 什么都不破坏」对这一条不成立:第 1 条自己要求 end 在解析期被拒,而 registerFlow 走解析 ⇒ 旧的运行期断言按构造不可达。
处理:夹具逐字未动,用例数 12 → 12,断言加强(区域路径、报文、且断言什么都没注册)。⛔ 没删、没跳过、没隔离,⛔ 引擎源码未动。复核测全了并判不挡 —— 但它是偏离,写在明处,⛔ 不埋。
分级
Clause-②: yes(无臂),@objectstack/spec minor。收窄确实是 breaking,但发布窗口期版本号不是承载体 —— 闸门自己的表头这么写,而两个强制承载体(changeset 的 BREAKING 横幅、ADR-0087 处置)都在,两道闸都绿。
运行期兄弟卡 #18881 确实存在且开着 ⇒ Part of 不会把本卡晾在半空。
Generated by Claude Code
…ed on the origin/main merge The merge of origin/main (b146102) routed four paths to the os-regen driver, which defers rather than text-merging: the three api-surface-declarations shards automation.txt, data.txt and ui.txt, and the generated reference page content/docs/references/ui/component.mdx. On every one of them the driver kept this branch's side and silently dropped main's, so step 2 of the sanctioned sequence restored main's side into the worktree and this commit re-derives all four from the merged tree. What each side contributed, now both present: ui.txt / component.mdx main's element:text.variant widening to the published nine (#19019), plus this branch's evaluated-slot narrowing data.txt main's transport no-transactions declaration (#18890) and the $orderby dual declaration (#19018), plus this branch's `source: string` narrowing on ConditionalValidationSchema and CrossFieldValidationSchema automation.txt main's structured-region pause/end refusal (#18688), plus this branch's narrowing The two MIXED, deliberately unrouted paths were hand-resolved by git's ordinary three-way merge and verified rather than eyeballed: registry.ts's hand-written remainder (generated regions stripped with the merge script's own awk) is byte-identical across base, both sides and the merge, and its line count is exactly additive (17142 + 121 + 74 = 17337), with both sides' migration entries present by id. component.zod.ts is additive too (3750 + 4 + 45 = 3799) and its single .superRefine() is untouched. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
Part of #15646
Clause-②: yes
The flow accept set shrinks for five node types inside region bodies — shapes the runtime never honoured. Ruling D clause 4 states it verbatim.
domain:servicescard. ⇒ this PR lands withPart of, ⛔ notFixes; #15646 stays open until the runtime half lands.Everything below the horizontal rule was written when the card was ruled C, and it argues for route A ("Recommendation: A, as implemented"). ⛔ That is no longer what this PR does. It is kept unedited as the record of how the decision was reached — ⛔ deleting it would erase the evidence the later ruling was made on.
What this PR does NOW, per ruling D (batch #153 item 1, comment
5724940095, maintainer 「其他同意」):Inside
loop/parallelbranch /try_catch(try and catch) bodies at any depth,FlowSchema.superRefinerefuses five node types:screen,wait,approval,approval_revise, andend.⛔
mapandsubfloware NOT refused by type. They pause exactly when the child flow theirconfig.flowNamenames pauses — a different metadata record, not in hand at parse. Refusing them by type would also refuseloop { map(synchronous child) }, which runs correctly today.packages/speckeeps its published identifierFLOW_PAUSE_CAPABLE_NODE_TYPES; only its contents narrow.packages/servicesis untouched by this round — measured, zero paths. The 5-tests-in-3-files cost the section below describes does not occur: the whole package runs 138 files / 1652 tests, all passing, with a false-green control proving the test read the rebuilt artifact and not a staledist.packages/services/service-automation/src/end-node-refused-outcome.test.ts(+57/−35) is in the diff, from the earlier round's commit87973cab8d1. Ruling D clause 1 says 「packages/servicesuntouched; the 5 tests and service-automation: amapnode inside aloopbody runs its collection ONCE — iterations 2..n do nothing, reportsuccess, and the run completes green #15616's suite stand」 — and service-automation: honouroutcome: 'refused'on the flowendnode — a terminalrefusedrun status (distinct fromfailed) with the interpolated message persisted on the run (lane 2 of the #14945 ruling 2′) #15788's region-endcase was one of those 5. ⇒ the ruling's premise 「B breaks nothing」 was false for that one case: clause 1 itself ordersendrefused at parse, andregisterFlowparses, so the old run-time assertion is unreachable by construction. What was done: the fixture is byte-identical, case count 12 → 12, and the assertion is strengthened (region path, message text, and that nothing registered) so it fails again the day the shape becomes declarable. ⛔ No test deleted, skipped or quarantined; ⛔ no engine source moved; service-automation: amapnode inside aloopbody runs its collection ONCE — iterations 2..n do nothing, reportsuccess, and the run completes green #15616's suite and the other three files are untouched and green. The at-tier review measured all of this and ruled it non-blocking — but it is a deviation and it is stated here rather than buried.CI on
6de9d662f6df: 32 success, 3 skipped, 0 failure, 0 pending.⛔ Two corrections the
domain:specseat owes on its own recordFLOW_PAUSE_CAPABLE_NODE_TYPESremoves a published export and therefore forces a major. Measured since: main'spackages/spec/api-surface/automation.jsongreps 0 for that name (lit controlsFLOW_BUILTIN_NODE_TYPESandFLOW_STRUCTURAL_NODE_TYPES= 1 each; dark control = 0), and the branch greps 1. ⇒ the constant is introduced by this PR and is on no consumer's import path; the gate's 「1 breaking (removed)」 was computed against the branch's own earlier snapshot. The decision stands and costs nothing — a second name would be cost without benefit — but ⛔ the record must not carry 「a removed published export」 as a fact about consumers. The round measured this and told the seat; the seat re-measured and confirms it.--pair 18688re-measured 「exit 0」 at this head. That reading came from a STALE INSTRUMENT. The shared checkout'scheck-clause2-carriers.mjsis blobccd5ad7c9a00and contains 0 occurrences of rule C8;origin/main's and this head's is blob3a270ef2eb5fand contains 18 (lit controlC1: 50 vs 51, so the reader works). C8 landed onmainat 01:41Z via fix(pm): a SECONDClaim:by one seat is NAMED, not ranked as a supersession #18859 and the shared checkout never had it. ⇒ every--pairreading this seat took today was taken with a script that cannot see C8. Re-taken withorigin/main's script: feat(spec)!: a structured region body refuses a pause-capable node and an 'end' node #18688 exit 4 on C8 — this seat held two liveClaim:comments on service-automation: a PAUSINGmapinside a contained region leaves its progress state behind — later loop iterations skip items and the exhausted map returnssuccesshaving run nothing #15646 (5722016855,5728277407), which the protocol forbids. Repaired as C8 prescribes:Release:(5729634742) then ONE freshClaim:(5729639847).--pair 18688now exits 0 —claim.selected1,claim.rejected2. The at-tier review caught this; the seat re-measured and confirms it.Route C, as ruled. Director seat, summon #24, batch #145 item 5 — #15646 (comment) (maintainer 「同意,其他也同意」), with the batch #146 scope addition — #15646 (comment) (maintainer 「146 同意」), which attached #3267's 禁 ruling and absorbed #18112 into this card. One PR, one changeset, two refusals in one rule family.
🛑 Read this first — this PR is NOT ready to land, and the reason is a measured decision, not a bug
packages/specis green end to end. 5 tests in 3packages/services/service-automationfiles now fail, and every one of them fails for the same reason: the fixture can no longer be REGISTERED, becauseAutomationEngine.registerFlowparses throughFlowSchema.parse(engine.ts:3941) and this rule refuses the shape.domain:specseat after a classification round — the table below replaces one that named 5 tests in 3 files. That earlier count was taken by running three named files; CI runspnpm --filter @objectstack/service-automation test, the whole package, and a named-file subset cannot see this class of breakage.os-dev.md:56reserves this body to the PR-open write, so the round named the wording and the seat writes it.src/builtin/contained-failure-rollup.test.tsloop { subflow }—git merge-base --is-ancestorexit 0), so this is a measurement gap, ⛔ not driftsrc/builtin/map-in-loop-iteration-state.test.tsloop { body: [ map, probe ] }over a non-pausing child: 5 iterations x 2 items ⇒ 10 child runs,failed = 0either way, a fresh result set per iterationsrc/builtin/contained-failure-visibility.test.tssubflowchild; the region shape is the vehicle, not the subjectsrc/end-node-refused-outcome.test.tsMeasured with the package suite: at
e10b395cee, 12 failed / 1627 passed (1639) across 4 files. After the fix below, at87973cab8d1: 11 failed / 1628 passed.⭐ One of the twelve was never blocked on the open question, and it is repaired here. #15788's region-
endcase sits in both candidate populations — this body defines route B as the unconditionally pausing types plusend— so no answer to the question below moves it. It is re-homed to the registration refusal: the fixture is unchanged byte for byte, and the case now asserts the ZodError's located path, its message and prescription, and that nothing registered. ⛔ Not a deletion — it fails again the day the shape becomes declarable.The 11 are mutually exclusive with route A, and that is measured rather than argued. Ablating⚠️ Method note that is load-bearing:
FLOW_PAUSE_CAPABLE_NODE_TYPESto route B's definition turns all 11 green with nothing else moving; route A on the same four files is 11 red.service-automationresolves@objectstack/specthroughdist, so the ablation was rebuilt and verified present in 18 built artifacts before anything was read — an unrebuilt ablation would have gone green and proved nothing. Restored afterwards, verified absent from all 216 artifacts, whole-tree porcelain empty.⭐ The 11 are NOT one cost. 3 of them (#15616) are free: under route A the shape becomes undeclarable, so the defect is unreachable and the regression suite converts to a refusal pin — mechanically, the same conversion performed above for #15788; that file's second describe (a TOP-LEVEL pausing map) is untouched and green, so the durable-pause half keeps its coverage. The other 8 (#16314, #14456) are a genuine re-home onto a top-level delegating node, and
loop { subflow }over five rows with one failing is the shape #15617's ruling named, so any re-home must record that the measurement no longer runs on it.⛔ Not repaired here. The dispatch fences⚠️ and note precisely what that fence claims: it is true of the diff, which touches no
packages/services("the engine's runtime refusal stays exactly as it is") —packages/servicesfile. Read as a claim about effect it is false, because the parse refusal changes what those suites can register. The changeset carries the same correction, and two of these three are other cards' regression suites: deleting or re-homing #15616's and #15788's coverage is a decision, not a fixture edit. Two of them are also evidence about the rule itself, which is the open question below.The open question: does the narrowing take a shape that WORKS with it?
The ruling's population is "a node that can durably pause (
map/subflowwith a pausing child, approval-class nodes)". Measured:mapandsubflowpause exactly when the child flow they NAME pauses — a different metadata record — so "with a pausing child" is not decidable at parse. Only two spellings are:loop { map(synchronous child) }— a shape that runs correctly today and was deliberately fixed 12 days ago by service-automation: amapnode inside aloopbody runs its collection ONCE — iterations 2..n do nothing, reportsuccess, and the run completes green #15616 / PR fix(service-automation): scope amapnode's progress state to one execution of its collection #15648, whose regression suite is 3 of the 5 failures above.screen/wait/approval/approval_revise) plusend. Refuses nothing that works today, and the 3mapfailures disappear. Cost: this card's own reproduction —loop { try_catch { map(pausing child) } }— stays declarable and stays silently green, so the card is not closed.There is no third reading available to a parse. Recommendation: A, as implemented — #3267 is ruled 禁 ("structured regions do not support durable pause"), and a shape whose legality lives in a record the author is not editing, revocable by editing that record, is not a contract. Under A the five tests are re-homed (a top-level
map, a top-levelend) or retired with a statement, in this PR or a follow-up, once the seat says the coverage may move.Step Zero — the ruling's precondition, answered before any code was written
Answer: YES for the nesting and for the node vocabulary this rule judges, with two boundaries that are declared rather than discovered. What was measured, on this branch's base
7f7b8557df:collectFlowGraphs(packages/spec/src/automation/control-flow.zod.ts) yields the top-level graph plus every region body, depth first, with ascopelabel and apaththat anchors a Zod issue where the author wrote the node.FlowSchema'ssuperRefinealready walks exactly that and refuses on it — the Decision: do a flow's top-levelnodes[]and its region bodies (loop/try_catch/parallel) share ONE node-id space, or two? — uniqueness is now enforced inside each, never across #16134 one-node-id-space rule. The PM seat's clue held: there is no refusing layer for this shape, but the walk and the refusal machinery are both live and in the same file.defineActionDescriptorliterals, not by recall:supportsPause: trueappears onscreen/wait/subflow/map(packages/services/service-automation/src/builtin/) andapproval/approval_revise(packages/plugins/plugin-approvals/src/) — six, the same six the ADR-0044resumeAuthoritydefault-flip migration entry names in its own prose. They are published here asFLOW_PAUSE_CAPABLE_NODE_TYPES.endis fully static —FLOW_STRUCTURAL_NODE_TYPES, a node type the engine handles with no executor at all.What is NOT decidable, and what this rule does about it. Whether a given node will pause is not decidable at parse, in two different ways, and both are stated in the docblock, in the changeset and in the ADR-0087 entry:
map/subflowpause exactly when the child flow they NAME pauses (map.config.flowName, an opaque reference to another metadata record). So the rule judges the node TYPE, not the run. That is wider than the runs that actually broke — a region-nestedmapover a synchronous child parsed green before and is refused now — and it is deliberate: the old shape's legality lived in a record the author is not editing and could be revoked by editing that record. "Legal until somebody adds awaitto the child flow" is not a contract.FlowNodeSchema.typeis a validatedstring), and a parse has no registry. Pinned as a boundary test so it moves deliberately.MAX_REGION_DEPTH(32). The walk stops there.nodes[]and its region bodies (loop/try_catch/parallel) share ONE node-id space, or two? — uniqueness is now enforced inside each, never across #16134's duplicate-id rule, there is no second spec refusal behind the ceiling for this rule —analyzeRegionsays nothing about pausing nodes — so past depth 32 the engine's run-time refusal is the only one. Measured and pinned at nesting 32 (refused) / 33 (not judged), and stated in the changeset rather than left for an author to find.What changed
FlowSchema.superRefinegains one walk overcollectFlowGraphs, skipping the flow's own graph, that raises acustomissue anchored at[...regionPath, 'nodes', i, 'type']for:loop 'sweep' body → try_catch 'guard' try), why a region body cannot host it, and the fix;endnode in a region body, whatever itsoutcome— anendthere was a no-op, and a refusing one was converted into a region error at the same boundary (service-automation: honouroutcome: 'refused'on the flowendnode — a terminalrefusedrun status (distinct fromfailed) with the interpolated message persisted on the run (lane 2 of the #14945 ruling 2′) #15788). The ruled prescription is the message: a region body cannot end the run; put theendon the top-level graph.FLOW_PAUSE_CAPABLE_NODE_TYPESis the new export (api-surface/export-originsregenerated). The two approval entries are the declared constantsAPPROVAL_NODE_TYPE/APPROVAL_REVISE_NODE_TYPE, so a rename cannot desynchronise them.⛔
packages/servicesis untouched — this is authoring-time enforcement only. ⛔ No engine rollback seam (route A, no card filed, per the ruling). ⛔ No runtime detection inmap(route B, refused). ⛔ #15617'sfailedfold is not addressed.Tests
New file
packages/spec/src/automation/flow-region-pause-and-end.test.ts— every case fails without the rule:loopbody,try_catchtry and catch,parallelbranch. A rule coveringlooponly is route B wearing C's clothes; thetry_catchcatch arm and theparallelbranch arm are the two route B could never see, and each has its own case.loop { try_catch { map } }, refused with the chained region path.endstill parse on the top-level graph; every non-pausing type still parses inside a region; a node merely namedendorwaitin a region still parses (the rule judgestype, notid).defineFlowandformatZodErrorrenderings.Two existing cases pinned the behaviour this rule replaces and were replaced rather than re-spelled, each saying so in its own comment:
end-node-outcome.test.ts's region-nestedend(its subject — anend-in-region whose config is judged one door later — no longer exists) andflow.test.ts's BPMNwaitEventConfigregion case (now asserts the earlier refusal and keeps the region-contract half it actually exists to measure). TherequireTypeScopedConfigdocblock that asserted a nested block-lesswaitparses green was corrected in the same edit.Verification
Measured on
e10b395cee. Heavy runs go throughscripts/pm/os-verify-lock.sh; every exit code below is read from the wrapper's ownVERDICT command-exitline or captured into a variable before any pipe — never$?after one.pnpm --filter @objectstack/spec buildcommand-exit 0pnpm --filter @objectstack/spec test(whole package)command-exit 0— 486 files, 13895 tests, 0 failedpnpm --filter @objectstack/spec typecheck(tsc --noEmit+check:scripts-typecheck+check:test-typecheck)command-exit 0pnpm --filter @objectstack/spec check:generatedcommand-exit 0— all 15 artifacts up to datepnpm lint(whole repo,eslint . --no-inline-config)exit 0— run in full, so nothing here is a narrowingdispatch-gates.mjs --ranreconciliationexit 0— 85 derived, 81 run, 4 NOT-MEASURED, 0 UNRUN@objectstack/service-automation— the 3 files whose fixtures feed this ruleexit 1— 5 failed / 24 passed, see the section at the topReverse verification (one-shot, restored). The rule's own early-exit was mutated (
graph.path.length === 0→>= 0), and the mutation was proved on disk before anything was read from the run — anchor grep 1 → 0, marker grep 0 → 1, blob044bbbba→56a1c350:flow-region-pause-and-end.test.ts: 20 failed / 7 passed. The 20 are exactly the refusal assertions; the 7 that survive are the over-reach guards and the two boundary pins, which must stay green with the rule absent. That split is itself the reading: a rule that also broke the negative cases would be refusing too much.git checkout HEAD --the file, blob back to044bbbba,git diff HEADclean,git statusempty, same file 27/27 passed.No
distpreflight applies: the test imports./flow.zodby relative source path, so the subject never resolves throughpackages/spec/dist. A restoretrapwas armed for the whole window.The four NOT-MEASURED gates are
check:doc-formula-expressions,check:dual-build-cjs-loads,check:lean-entry-closureandcheck:type-check-debt— each exited 3, PREREQUISITE NOT MET, printing in its own words that nothing was measured. All four read built output across packages this diff does not touch and need a repo-wide build; CI'sBuild CoreandLint & Repo Gatesare where they run. ⛔ Not green, not red — unrun.Not measured, stated: CI convergence on this PR (the report is filed at the end of local verification); the branch has not been merged forward since
7f7b8557df, somain's newer commits are tested by CI and the queue rather than here.Review-gate reading, not an action.
scripts/pm/check-clause2-carriers.mjs --pair 18688exits 4 on two rows, both belonging to the claiming seat and ⛔ neither touched here: C1 — card #15646 carriesneeds:contract-reviewwhile this PR does not (the gate is a dual carrier); C2 — no comment on the card's thread is a machine-legible claim comment (none has a first line beginningClaim:carrying theClause-②:line), so the declaration limb has nothing to read. The declaration itself is at the top of this body and in the changeset.Acceptance notes
Observations from this card's reading, recorded here and not filed — none is a reproducible defect, a contract violation, or an authoring trap:
map/subflowwith a pausing child", describes the defect population rather than a decidable rule population, and its "a shape the runtime never honoured" is exact forend,screen,waitand the approval pair but not for amapover a synchronous child, which runs today. The PR takes the capability reading — the only one that makes C the complete fix the ruling's own reasoning requires — and the changeset states the cost in the author's own terms. Noted so a reviewer reads the widening deliberately rather than discovering it.FlowRunSummary's two paragraphs disagree for a subflow parent —failedis declared a node fold, while the summary is declared to answer "what did this run cause" and roll a child's totals up #15617 is open; it is closed / completed. Nothing here depends on it.engine.ts:9937in the ruling readsengine.ts:9970on this tree — line numbers are clues, and this one was re-read rather than trusted.🤖 Generated with Claude Code
https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Generated by Claude Code