Skip to content

Commit 6445eab

Browse files
committed
wip(spec): regenerate the migration registry
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 065ea98 commit 6445eab

1 file changed

Lines changed: 74 additions & 0 deletions

File tree

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9058,6 +9058,80 @@ const step18: MigrationStep = {
90589058
+ "slot phrase. A flow that boots without that warn is unaffected; every structural "
90599059
+ 'condition carrying a non-blank `source` parses byte-identically to before.',
90609060
},
9061+
// The ledger `predicate` slots' half of the blank-predicate rule. A SEPARATE
9062+
// entry from `flow-edge-condition-evaluated-slot-source-required` on purpose:
9063+
// that one carries the structural slots (`edges[].condition`,
9064+
// `config.condition`), refused by the evaluated-slot rule under
9065+
// EVALUATED_EXPRESSION_SOURCE_REQUIRED, where removing a blank condition
9066+
// INVERTS the edge. These slots are declared `z.string()`, are refused under
9067+
// PREDICATE_SLOT_STRING_REFUSAL, and removing the blank is behaviour-
9068+
// preserving — a different prescription, which one entry cannot carry for both.
9069+
//
9070+
// No backticks in `surface` — build-upgrade-guide.ts renders it inside a code
9071+
// span already, and a nested backtick would close it.
9072+
{
9073+
id: 'flow-predicate-slot-blank-string-refused',
9074+
surface:
9075+
'the two ledger predicate slots on a flow node — config.conditions[].expression on a decision '
9076+
+ 'node (a branch predicate) and config.fields[].visibleWhen on a screen node (a field visibility '
9077+
+ 'predicate) — authored as a string that is blank after trimming (\'\', \' \', a tab or a '
9078+
+ 'newline), at any depth including an ADR-0031 region body. Reachable wherever a flow is '
9079+
+ 'authored or stored: defineStack({ flows }) sources, defineFlow(), an exported stack passed to '
9080+
+ 'objectstack validate, a POST /flows body, and a flow row already sitting in sys_metadata',
9081+
replacement:
9082+
'the predicate the branch or field was meant to test, as non-blank bare CEL text '
9083+
+ '(`expression: \'record.amount > 10\'`, `visibleWhen: \'amount > 0\'`); or REMOVE the blank. '
9084+
+ 'On a screen field, drop the `visibleWhen` key: an absent `visibleWhen` shows the field '
9085+
+ 'unconditionally, which is what a blank one already did at run time (the resume contract '
9086+
+ 'treated it as absent, and the renderer fell back to showing the field). On a decision '
9087+
+ 'branch, drop the whole `conditions[i]` element: `expression` is required by '
9088+
+ '`DecisionConditionSchema`, so a branch cannot keep its label without it, and a branch whose '
9089+
+ 'predicate was blank was never taken, so dropping it changes no run. ⚠️ Removal is '
9090+
+ 'behaviour-preserving HERE, unlike on a structural condition, where dropping a blank '
9091+
+ '`condition` turns a never-firing edge into an always-firing one '
9092+
+ '(`flow-edge-condition-evaluated-slot-source-required`)',
9093+
reason:
9094+
'Card #17493, ruling A (5651023407). Both slots are declared bare CEL text (`z.string()`) '
9095+
+ 'and both admitted a blank string at every door: the expression ledger resolver skipped it '
9096+
+ 'as "not authored", and `AutomationEngine.evaluateCondition` answered it `false` — so a '
9097+
+ 'decision branch carrying it was never taken, with nothing said at any layer, and a screen '
9098+
+ 'field carrying it was shown with its predicate ignored. #15572 had pinned that '
9099+
+ 'admission as correct because the two sides agreed. The ruling is that self-consistency '
9100+
+ 'between parser and evaluator is not a defence when the author\'s intent is silently '
9101+
+ 'dropped — the third instance of one rule, after #17322 (the structural `config.condition`) '
9102+
+ 'and #15811 (a blank evaluated `source`). The blank is now refused at `FlowSchema.parse`, '
9103+
+ 'at `AutomationEngine.registerFlow` (which parses first) and at `objectstack validate`, all '
9104+
+ 'three through `predicateSlotRefusal`, leading with `PREDICATE_SLOT_STRING_REFUSAL`. '
9105+
+ '⚠️ No D2 conversion, and the reason is the judgment this entry delegates: the blank is '
9106+
+ 'where an author meant to write a rule, and the platform cannot tell a predicate somebody '
9107+
+ 'forgot from one they meant to delete. Removing it preserves what ran; writing it is what '
9108+
+ 'the author intended; only the author knows which. '
9109+
+ '⚠️ A flow ALREADY STORED with such a blank no longer registers at all, not just that '
9110+
+ 'branch or field: `registerFlow` parses through `canonicalizeStoredFlow` → '
9111+
+ '`FlowSchema.parse`, and each boot path in `service-automation/src/plugin.ts` logs one '
9112+
+ '`warn` naming the flow and continues — its trigger is never armed. '
9113+
+ 'ADR-0087, ADR-0032.',
9114+
acceptanceCriteria:
9115+
'Grep every flow node in `defineStack({ flows })` sources, exported stacks, `POST /flows` '
9116+
+ 'bodies and every flow row in `sys_metadata` — including nodes inside a `loop` / '
9117+
+ '`parallel` / `try_catch` region body — for a `decision` node whose '
9118+
+ '`config.conditions[i].expression`, or a `screen` node whose `config.fields[i].visibleWhen`, '
9119+
+ 'is a string that is empty after trimming. Each refusal names the node and the branch or '
9120+
+ 'field, which is the TODO\'s locator: `FlowSchema.parse` anchors a `custom` issue at '
9121+
+ '`nodes.N.config.conditions.I.expression` (or `…config.fields.I.visibleWhen`, or the region '
9122+
+ 'path `nodes.N.config.body.nodes.M.config…`), and `objectstack validate` prints the same '
9123+
+ 'path; `validateStackExpressions` phrases it as '
9124+
+ '`node \'check\' (decision) decision branch expression at config.conditions[0].expression`. '
9125+
+ 'For each hit decide, per the `replacement` note, whether to write the predicate or to '
9126+
+ 'remove it — and on a decision branch removing means the whole branch. Two proofs. (1) For '
9127+
+ 'a stack authored in config files, `objectstack validate` is clean. (2) Boot the stack and '
9128+
+ 'confirm each flow REGISTERS: no `failed to register flow` warn for it (the three boot '
9129+
+ 'paths spell it `[Automation] failed to register flow`, `[Automation] flow re-sync: failed '
9130+
+ 'to register flow` and `[Automation] cold-boot flow bind: failed to register flow`) — that '
9131+
+ 'warn line is the locator for a row that exists only in `sys_metadata`. A non-blank '
9132+
+ 'predicate parses and registers byte-identically to before, and a non-string in these '
9133+
+ 'slots keeps its own earlier refusal (at `registerFlow` and `objectstack validate`).',
9134+
},
90619135
{
90629136
id: 'hook-register-undispatched-lifecycle-event-refused',
90639137
surface:

0 commit comments

Comments
 (0)