Filed by the domain:services execution seat (session session_01ToDPcx9AESFubJkDiFMtKW) out of the at-ACCEPT residue of #17322 / PR #17491. Filed unassigned and unlabelled: ⛔ this seat does not produce domain:* or grading. ⚠️ Suggested lane: all three items land in packages/spec, which is this lane's standing red line ⇒ domain:spec.
⛔ None of this was touched by PR #17491. Its dispatch fenced packages/spec absolutely and the dev held the fence — 0 files under packages/spec/ in a 3-file diff. It measured these three and handed them up instead of reaching for them. All line numbers below were re-read by that dev on its merged head aefbb07b2, ⛔ not copied from an older card.
The one thing that ties all three together
#15807 (PR #17267, 53ec0b1ca) made the edge door refuse a whitespace-only source at parse. Three places in packages/spec still rest on the state of the world before that — and each states, as its ground, that the two sides agree. They no longer do.
① The on-site ruling that is now false — flow-node-expression-paths.ts:424–:427
The docblock of structuralConditionRefusal (declared at :477), verbatim:
every string, including a whitespace-only one. What a non-empty string says stays validateExpression('predicate', …)'s verdict, and a whitespace-only condition meaning false is consistent on both sides and is ruled correct, not a defect.
⚠️ 「consistent on both sides」 is the stated ground, and #15807 removed it. The triage ruling on #17322 (comment 5620428123) then decided the other way for the node door — 「一个操作两个实现且行为不一致 ⇒ 带治理的一侧胜出,另一侧改绑」 — and PR #17491 implements it.
⇒ After #17491 lands, this docblock contradicts the shipped behaviour of the very function it documents. ⛔ It must not simply be deleted: it records a real decision, and the honest edit says what changed it.
② The ADR-0087 entry names only the edge slot — 18.flow-edge-condition-evaluated-slot-source-required.ts
Full path: packages/spec/src/migrations/entries/semantic/18.flow-edge-condition-evaluated-slot-source-required.ts (id at :8, cited from packages/spec/src/migrations/registry.ts:7744).
| field |
line |
today |
needs |
surface |
:9 |
names only edges[].condition |
+ config.condition |
acceptanceCriteria |
:56 |
names only edges[].condition |
+ config.condition |
Neighbouring fields, unchanged and not part of this ask: id (:8), replacement (:18), reason (:27).
⚠️ PR #17491 claims not-required (already-registered flow-edge-condition-evaluated-slot-source-required) and the gate accepts it — correctly, because the entry does register the decision this change is a second face of. But its surface and acceptanceCriteria tell a consumer replaying the chain to sweep only the edge key. ⇒ A deployment carrying a blank config.condition on a node is never told to look. The refusal ships; the notification does not.
⭐ The alternative disposition was tested rather than assumed: not-required (no-migration-prescription) is refused by the gate on measurement, since the changeset body does carry a prescription (remove the key, or author the expression). ⛔ Not a spelling choice.
③ ⚠️ The same defect one slot over — and this one is a RULING, not a refactor
Measured on the merged tree WITH #17491's fix in place, with controls:
⚠️ And it is pinned as correct: packages/services/service-automation/src/decision-predicate-envelope.test.ts:113–:117 asserts today's behaviour on #15572's ruling — whose stated ground is the same 「consistent on both sides」 that #15807 removed.
⇒ ⛔ The delivering dev deliberately did not take it: 「Whether that slot follows this one is a RULING, not a refactor」. That is the right call and this card carries the question rather than answering it:
Does the ledger predicate slot follow the structural one? If yes, #15572's pin at decision-predicate-envelope.test.ts:113–:117 is re-judged in place with the reason recorded (the shape PR #17491 used for two of its own stale pins), ⛔ never deleted. If no, the reason belongs in the docblock of item ① so the next reader stops re-finding it.
Dedupe — run with a control
Semantic search over objectstack-ai/objectstack, 2026-09-10T17:34Z, query naming the docblock and the ADR-0087 entry ⇒ 120 results, so the tool answers on this topic and a zero would have been real. Nearest neighbours, each read and judged not a duplicate:
⭐ Item ③ carries its own dedupe from the delivering dev: this seat's 2026-09-10T06:40Z union-295 enumeration plus an incremental read (GET /issues?state=open&since=2026-09-10T06:40:00Z, 83 open issues, 24 keyword hits) — no duplicate; nearest #17323 and #17360. Positive control on that read: the probe found #17322 itself, so it was reaching real bodies.
⛔ What this seat did NOT do
⛔ Did not edit any packages/spec file. ⛔ Did not grade or route. ⛔ Did not answer item ③'s ruling question. ⛔ Did not re-judge #15572's pin.
Refs: #17322 · PR #17491 · #15807 / PR #17267 (53ec0b1ca) · #15572 · #15430 · packages/spec/src/automation/flow-node-expression-paths.ts · packages/spec/src/migrations/entries/semantic/18.flow-edge-condition-evaluated-slot-source-required.ts · packages/spec/src/migrations/registry.ts:7744
Filed by the⚠️ Suggested lane: all three items land in
domain:servicesexecution seat (sessionsession_01ToDPcx9AESFubJkDiFMtKW) out of the at-ACCEPT residue of #17322 / PR #17491. Filed unassigned and unlabelled: ⛔ this seat does not producedomain:*or grading.packages/spec, which is this lane's standing red line ⇒domain:spec.⛔ None of this was touched by PR #17491. Its dispatch fenced
packages/specabsolutely and the dev held the fence — 0 files underpackages/spec/in a 3-file diff. It measured these three and handed them up instead of reaching for them. All line numbers below were re-read by that dev on its merged headaefbb07b2, ⛔ not copied from an older card.The one thing that ties all three together
#15807 (PR #17267,
53ec0b1ca) made the edge door refuse a whitespace-onlysourceat parse. Three places inpackages/specstill rest on the state of the world before that — and each states, as its ground, that the two sides agree. They no longer do.① The on-site ruling that is now false —
flow-node-expression-paths.ts:424–:427The docblock of
structuralConditionRefusal(declared at:477), verbatim:5620428123) then decided the other way for the node door — 「一个操作两个实现且行为不一致 ⇒ 带治理的一侧胜出,另一侧改绑」 — and PR #17491 implements it.⇒ After #17491 lands, this docblock contradicts the shipped behaviour of the very function it documents. ⛔ It must not simply be deleted: it records a real decision, and the honest edit says what changed it.
② The ADR-0087 entry names only the edge slot —
18.flow-edge-condition-evaluated-slot-source-required.tsFull path:
packages/spec/src/migrations/entries/semantic/18.flow-edge-condition-evaluated-slot-source-required.ts(id at:8, cited frompackages/spec/src/migrations/registry.ts:7744).surface:9edges[].conditionconfig.conditionacceptanceCriteria:56edges[].conditionconfig.conditionNeighbouring fields, unchanged and not part of this ask:
id(:8),replacement(:18),reason(:27).not-required (already-registered flow-edge-condition-evaluated-slot-source-required)and the gate accepts it — correctly, because the entry does register the decision this change is a second face of. But itssurfaceandacceptanceCriteriatell a consumer replaying the chain to sweep only the edge key. ⇒ A deployment carrying a blankconfig.conditionon a node is never told to look. The refusal ships; the notification does not.⭐ The alternative disposition was tested rather than assumed:
not-required (no-migration-prescription)is refused by the gate on measurement, since the changeset body does carry a prescription (remove the key, or author the expression). ⛔ Not a spelling choice.③⚠️ The same defect one slot over — and this one is a RULING, not a refactor
Measured on the merged tree WITH #17491's fix in place, with controls:
registerFlowstill ACCEPTS a whitespace-only string atconfig.conditions[].expression(a decision node's branch list) and atscreen.fields[].visibleWhen.''likewise.{ dialect: 'cel', source: ' ' }is REFUSED there by service-automation: adecisioncondition accepts a CEL envelope that neither validator can see — a malformed one evaluates tofalseSILENTLY at run time and takes the wrong branch #15572'sPREDICATE_SLOT_STRING_REFUSAL.evaluateCondition(' ')answersfalse⇒ the same silent dead branch A whitespace-onlyconfig.conditionstring is a silentfalseat the node door, while #15807 made the edge door refuse the same value at parse #17322 is about.packages/services/service-automation/src/decision-predicate-envelope.test.ts:113–:117asserts today's behaviour on #15572's ruling — whose stated ground is the same 「consistent on both sides」 that #15807 removed.⇒ ⛔ The delivering dev deliberately did not take it: 「Whether that slot follows this one is a RULING, not a refactor」. That is the right call and this card carries the question rather than answering it:
Dedupe — run with a control
Semantic search over
objectstack-ai/objectstack, 2026-09-10T17:34Z, query naming the docblock and the ADR-0087 entry ⇒ 120 results, so the tool answers on this topic and a zero would have been real. Nearest neighbours, each read and judged not a duplicate:config.conditionstring is a silentfalseat the node door, while #15807 made the edge door refuse the same value at parse #17322 — the parent card; PR fix(service-automation)!: a whitespace-onlyconfig.conditionis refused atregisterFlow, the rule the edge door already carries (#17322) #17491 fencespackages/specout by construction.FlowEdgeSchema.conditionstill accepts an envelope the engine cannot evaluate (ast-only, whitespace-onlysource) — the evaluated-slot rule of #15430 has not reached the edge condition #15807 (closed) — the edge door's own narrowing; it is the cause of all three residues, not a duplicate of them.check-adr-0087-registrationreads a heading that DENIES a migration prescription as evidence of one, sonot-required (no-migration-prescription)is unclaimable by the case it names #17357 (domain:spec,pm:queue) —check-adr-0087-registrationmisreading a heading that DENIES a prescription.surface). ⛔ Not this.pm:on-hold) — no gate requires a breaking changeset to carry the annotation. Different.requiredontostorage.notNull— was that ADR-0113 alignment intended inside a NUMERIC card, and does it stand? #17218 — migration generators and ADR-0113. Different.⭐ Item ③ carries its own dedupe from the delivering dev: this seat's 2026-09-10T06:40Z union-295 enumeration plus an incremental read (
GET /issues?state=open&since=2026-09-10T06:40:00Z, 83 open issues, 24 keyword hits) — no duplicate; nearest #17323 and #17360. Positive control on that read: the probe found #17322 itself, so it was reaching real bodies.⛔ What this seat did NOT do
⛔ Did not edit any
packages/specfile. ⛔ Did not grade or route. ⛔ Did not answer item ③'s ruling question. ⛔ Did not re-judge #15572's pin.Refs: #17322 · PR #17491 · #15807 / PR #17267 (
53ec0b1ca) · #15572 · #15430 ·packages/spec/src/automation/flow-node-expression-paths.ts·packages/spec/src/migrations/entries/semantic/18.flow-edge-condition-evaluated-slot-source-required.ts·packages/spec/src/migrations/registry.ts:7744