Repository navigation
[finding] A decision branch with no expression key registers and validates clean, although DecisionConditionSchema declares it required and the executor throws on the source-less envelope #19961
Description
Activity
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsBlocked-by: #19960
分诊首次定级:
priority:p2·bug·domain:spec·pm:blocked—— 缺少expression的决策分支能通过构建,运行时才报错Path:
packages/spec/src/automation/flow.zod.ts(FlowSchema的谓词槽精化)+packages/spec/src/automation/flow-node-expression-paths.ts(predicateSlotRefusal)—— 两处都正被 PR #19960 修改Triage: lands in the flow predicate-slot refusal ⇒
domain:spec,bug,priority:p2,pm:blockedBlocked-by #19960; rationale:DecisionConditionSchemadeclaresexpressionrequired, but nothing parses adecisionnode's open config against it, so a branch with noexpressionpassesFlowSchema.parse,registerFlowandobjectstack validateand then throws in the executor — and objectui's Studio flow editor (FlowObjectListFieldrowsToList) writes exactly that shape when the expression cell is left blank (NORTH-STAR rule 4: wrong metadata must be refused at build time, with a remedy).分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ,座位贴 #6015),2026-09-24T09:18Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在main(3b5607019f)和 objectui62597c5上核对了前提。本席核对
schemaless-node-config.zod.ts里DecisionConditionSchema的expression是z.string(),也就是必填。flow-node-expression-paths.ts:284-291的注释写明:decision没有发布configSchema,DecisionConditionSchema从不对节点的开放 config 做解析。所以这个"必填"没有任何地方在执行。- objectui 的
packages/app-shell/src/views/metadata-admin/inspectors/FlowObjectListField.tsx:62就是卡面说的rowsToList。
定级说明
- p2:用户在 Studio 里给决策分支留空表达式,就能造出这种流程。保存和校验都会通过,流程运行到这个分支时才报错。这不是安全问题,也不会静默算错(运行时会报错),但报错太晚,作者(常常是 AI)在构建期拿不到任何提示。
- 同一类问题中的"空白字符串"情形,正由 PR fix(spec)!: refuse a blank string in a flow node's predicate slot — decision branch
expression, screen fieldvisibleWhen(#17493) #19960(spec/automation: three residues of #17322's node-door refusal — a stale on-site ruling, an ADR-0087 entry that names only the edge slot, and two sibling predicate slots still admitting a blank string #17493 裁定 A)在构建期拒绝。本卡是它相邻的"根本没有这个键"的情形。
为什么挂
pm:blocked修复点就是 PR #19960 正在改的
FlowSchema精化和predicateSlotRefusal。在它合并之前另开 PR,会改同一个文件、同一段逻辑。⇒ Blocked-by #19960。PR #19960 合并后本卡立即可派发。PR #19960 已写明决策分支的处方是"删掉这个分支",本卡沿用。方向(默认)
在同一个构建期拒绝里覆盖"缺少
expression"的决策分支,三道门(FlowSchema.parse、registerFlow、objectstack validate)都要报出同样的处方。objectui 编辑器那一侧不需要单独开卡:构建期拒绝上线后,编辑器保存时就会看到这条报错。
Generated by Claude Code
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 24, 2026 objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsUnblock re-derivation:
pm:blocked→pm:queue· 2026-09-24T23:58ZActing on the maintainer's instruction. Provenance: who — the maintainer; verbatim — 「帮我排查一下 blocked 卡片,哪些需要更新状态帮我更新。」; where — the maintainer's chat with session
session_013RWUA7bNq5bRhehLPqXwMg, 2026-09-24. ⛔ Not a claim and not a dispatch: the card returns to its lane's take order. Candidate surfaced bycheck-half-states.mjsH19 on this sweep.- Blocker: fix(spec)!: refuse a blank string in a flow node's predicate slot — decision branch
expression, screen fieldvisibleWhen(#17493) #19960 closedcompletedon 2026-09-24 (blank-string predicate refusal). - Double-check ① — latest conversion comment
5811370954names exactly that condition (same two files). - Double-check ② — PR fix(spec)!: refuse a blank string in a flow node's predicate slot — decision branch
expression, screen fieldvisibleWhen(#17493) #19960 merged after that comment, but it is the named condition itself. It covers the blank-string case, not this card's absent-expression-key case, and it does not name [finding] Adecisionbranch with noexpressionkey registers and validates clean, althoughDecisionConditionSchemadeclares it required and the executor throws on the source-less envelope #19961. No commit onmaindoes either. - Re-derived, still owed: the absent-key arm of the decision-branch refusal.
State:
pm:blocked→pm:queue;domain:spec,priority:p2unchanged; no assignee.
Generated by Claude Code
- Blocker: fix(spec)!: refuse a blank string in a flow node's predicate slot — decision branch
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsSerial note from
domain:specseat 2 (session_01QcAS3qiYYZNezaxZxaUdMV), 2026-09-27T03:20Z. ⛔ Not a claim; the state is unchanged (pm:queue).Passed over in this seat's round 1 because of a hot file. The fix lands in
packages/spec/src/automation/flow-node-expression-paths.ts(predicateSlotRefusal) andflow.zod.ts. The first of those is named by the live claim on #19938 (seat 5, dispatched 2026-09-24, no open PR at this reading). Seat 1's #19867 holds the adjacentDecisionConfigSchemablock inschemaless-node-config.zod.ts. Whoever takes this card re-reads both claims first. If #19938 has landed, mergemainand build on its ledger shape.objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-09-27T16:51Z
Session:session_01Rjy9MeetSfq34PKn81CRiN
Account:os-zhuang
Branch:claude/issue-19961-decision-branch-expression-absent
Worktree:objectstack-issue-19961
Domain:domain:spec
Seat:domain:spec#1
File surface:packages/spec/src/automation/flow-node-expression-paths.ts(predicateSlotRefusal) and theFlowSchemapredicate-slot refinement inpackages/spec/src/automation/flow.zod.ts, with tests; one ADR-0087 semantic entry +packages/spec/src/migrations/registry.ts; generated artefacts;.changeset/. ⛔ Notschemaless-node-config.zod.ts'sDecisionConfigSchemaregion or itsmoderefusal helper (seat 4's #20168);DecisionConditionSchemais read-only unless the refusal cannot live in the slot walk, which the dev must then report. (stop on breach; explain in the report)
Container & model:S,mode:subagent,model: default judgment tier(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable). The diff hitspackages/spec/src/**, so it is reviewed atCONTRACT_REVIEW_TIERbefore enqueue.
Clause-②: no
Thread-read: 5852253854
Serial constraints cleared:#19938 (PR #20205) has landed, so the hot file flow-node-expression-paths.ts is free; build on its ledger shape (seat 2's note 5852253854). Seat 4's #20168 (claim 5857419814) holds schemaless-node-config.zod.ts DecisionConfigSchema only and names this card as next and out of its surface; region-fenced above. No open PR touches flow-node-expression-paths.ts or automation/flow.zod.ts (15 open PRs' file lists read this act); PR #20258 (devx #12238) touches only the generated schemaless-node-config.mdx reference. Triage 5811370954 set the direction: the same build-time refusal and prescription at all three doors.objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 19961, "status": "done", "branch": "claude/issue-19961-decision-branch-expression-absent", "pr": "https://github.com/objectstack-ai/objectstack/pull/20315", "session": "session_01Rjy9MeetSfq34PKn81CRiN (mode:subagent; identity = the branch)", "premise_still_valid": true, "summary": "Reproduced on origin/main a9fb83ef at all three doors. A decision branch with no expression ({label}), expression: null, or the predicate under condition was ACCEPTED by FlowSchema.parse, registerFlow and objectstack validate (valid: true, exit 0). The blank control was refused, and true was accepted. At run time evaluateCondition({dialect: cel, source: undefined or null}) throws, and the run fails at the branch. Fix via Zone 2 option (a), one walk and one judge. FLOW_NODE_EXPRESSION_PATHS gains required?: true on decision conditions[].expression only. For a required predicate slot, resolveFlowNodeExpressions emits the absent or null value on a branch that exists. predicateSlotRefusal(undefined|null) gets its own detail and prescription under the unchanged PREDICATE_SLOT_STRING_REFUSAL lead. The FlowSchema refinement admits that value next to strings. Lint checkDeclaredPredicate drops its raw == null early return; that site is outside the claimed file surface, and ablation B shows the lint door needs it. The ledger ratchet now reconciles required against each channel JSON-Schema required list, predicate role only. After the fix, all three doors give one byte-identical message at the same path. Premise note: triage 5811370954 paraphrases the #19960 prescription as delete the branch. The landed #19960 text prescribes expression false and warns NOT to drop a decision only branch. I followed the landed wording and dropped its keep-what-ran half, because an absent predicate never ran. Producer census: examples 0, packages 0, cloud main 96eb092f 0, docs 0. objectui pin f8a9d0fb rowsToList still writes {label} for an empty expression cell; it is the known writer and triage accepted it. PR #20279 (#20168) landed mid-run and is merged into the final head 7534fd7e; the two do not meet.", "tests": "Final head 7534fd7e, all through os-verify-lock with spec rebuilt: spec src/automation+src/migrations 1005/1005 (including #20279 schemaless-node-config.test.ts); service-automation decision-branch-expression-absent + config-expression-ledger + decision-predicate-envelope + predicate-slot-blank 50/50; lint validate-expressions.test.ts 344/344. Full suites on first-merge head 266cd043: spec 583 files, 16855 passed (1 skipped, 1 todo); service-automation 147 files, 1767/1767; lint 110 files, 4269/4269. typecheck (tsc --noEmit + check:test-typecheck) for spec, lint and service-automation: exit 0 on 266cd043; no file of this diff changed after that. Door readings after the fix (e702ebd4, spec dist rebuilt): real CLI objectstack validate --json gives absent / null / alias valid:false exit 1 with a custom error at flows.0.nodes.1.config.conditions.0.expression, absent and alias byte-identical; true gives valid:true exit 0. registerFlow refuses the same three with a custom issue at nodes.1.config.conditions.0.expression and registers true. Ablation A, committed state: scripts/ablation-replace.mjs replaced required: true, by a globalThis-gated spread (anchor 1 to 0, blob 44394d22 to e577ad3a). spec rebuilt; ablation-dist-preflight found ABLATION_19961 in 20 built files. RED as expected: spec 7 failed, service-automation 6 failed, lint 4 failed; blank and true controls green at every door. Restore: blob == HEAD 44394d22, git diff HEAD empty, rebuild, preflight --absent (marker gone from 222 files), whole-tree status clean, then spec 52/52, service-automation 34/34, lint 344/344. The first A attempt was a no-op and its reading was discarded: the replacement was invalid TS, the transform failed and the build did not run. Ablation B: the lint raw == null early return put back (anchor 1 to 0, blob 716388d9 to bfb4521e). Lint 4 failed (absent, null, alias, index 1), blank and true green, restored to blob == HEAD. The first B attempt was refused by the tool before running, because the anchor matched its replacement. eslint --no-inline-config --format json over the 12 changed .ts files on 7534fd7e: 12 files, 0 errors, 0 warnings. Population: eslint.config.mjs files glob (all ts/js), no file ignored. Invariance: the config never enables type-aware linting (no parserOptions.project).", "gates": "Head 7534fd7e: dispatch-gates --commands --repo objectstack-ai/objectstack re-derived 90 families; 89 ran, all exit 0; dispatch-gates --ran reads 90 derived / 89 run / 1 NOT-MEASURED / 0 UNRUN. NOT MEASURED: pnpm check:type-check-debt. Its --re-measure runs a whole-tree turbo build outside os-verify-lock (coordinator instruction); this diff touches no DEBT-ledger package (cloud-connection, hono, observability, spec-monorepo). On earlier heads: check:dual-build-cjs-loads gave PREREQUISITE NOT MET (12 unrelated packages unbuilt, exit 3) until they were built, then 0. check:dts-closure went red on local state: 6 packages lost their .d.ts to my own interrupted --re-measure build; they were rebuilt, then 0. spec check:generated: all 15 artifacts up to date on 266cd043; the migration registry is current on every merge head. CI on PR #20315 at 7534fd7e when read: 12 success, 17 in_progress, 3 skipped, 0 failure. in_progress is the honest value.", "line_budget": "The diff vs origin/main is 13 files, +777/-23 = 800 changed lines, under the 5000 human-merge threshold. No skills/** and no governed surface are touched, so no SKILL line budget applies.", "files_changed": [ ".changeset/19961-decision-branch-expression-absent-refused.md", "packages/spec/src/automation/flow-node-expression-paths.ts", "packages/spec/src/automation/flow.zod.ts", "packages/spec/src/automation/flow-node-expression-paths.test.ts", "packages/spec/src/automation/flow-decision-branch-expression-absent.test.ts", "packages/spec/src/automation/flow-predicate-slot-blank.test.ts", "packages/spec/src/migrations/entries/semantic/18.flow-decision-branch-expression-absent-refused.ts", "packages/spec/src/migrations/registry.ts", "packages/lint/src/validate-expressions.ts", "packages/lint/src/validate-expressions.test.ts", "packages/services/service-automation/src/decision-branch-expression-absent.test.ts", "packages/services/service-automation/src/decision-predicate-envelope.test.ts", "packages/services/service-automation/src/builtin/config-expression-ledger.test.ts" ], "deviations": [ "Fix site outside the claimed file surface: packages/lint/src/validate-expressions.ts, where the raw == null early return is removed. Without it the lint door stays green on the absent value (ablation B). Also outside it: service-automation and lint test files. Named in the PR body.", "Bounded in-place extension of the same class: expression: null, and a predicate written under condition, both take the same arm through the same walk. The card title names only the absent key. Both throw identically at run time.", "The prescription follows the landed #19960 wording, not the triage paraphrase (delete the branch); see summary.", "My first gate sweep ran check:type-check-debt --re-measure, a whole-tree build, OUTSIDE os-verify-lock. It contended with a locked build, which the coordinator flagged. My runner timeout killed it, and it cost 6 packages their .d.ts locally, which I rebuilt. After that the gate was recorded NOT MEASURED.", "Two locked closure builds were lost: one to a 590s timeout (exit 124, 12/14 tasks done and cached) and two to queue-timeouts (exit 99). The re-verification on the merge heads was narrowed to spec rebuild + targeted suites + all gates, and this is declared in the PR body.", "origin/main was merged three times (266cd043, e702ebd4, 7534fd7e); the last one carries the landed #20279.", "Commit trailers: the harness reminder asked for a model-named Co-Authored-By. AGENTS.md requires the model-free pair (Claude-Session + Co-authored-by: Claude), and all pushed commits carry that pair. The first commit was amended before its push." ], "mcp_calls": "0 — no MCP GitHub tool was called", "api_writes": "3 relay writes, each one POST /repos/objectstack-ai/objectstack/dispatches (repository_dispatch) executed by fleet-write as objectstack-fleet[bot]. (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft, #20315). (2) label-write assign: POST /repos/objectstack-ai/objectstack/issues/20315/assignees [os-zhuang]; no labels written. (3) post-stamped: POST /repos/objectstack-ai/objectstack/issues/19961/comments (this report). Not REST writes: git push over the paced route, 6 pushes of the branch.", "open_questions": [], "out_of_scope_findings": [ "class: b · reach: objectstack validate --json gives valid:true exit 0 and registerFlow registers; named producer objectui FlowObjectListField rowsToList at pin f8a9d0fb writes {expression} when the label cell is blank · evidence: DecisionConditionSchema declares label z.string() required. A decision branch {expression: true} with no label ran with success:true, and traversal took EVERY out-edge (steps start,d,y,x: both the isDefault edge and the labelled one), because the matched branch reports branchLabel undefined. That is silent wrong routing. Seam: spec:DecisionConditionSchema.label -> runtime:service-automation builtin/logic-nodes.ts decision executor (branchLabel) | renderer:objectui FlowObjectListField rowsToList · same family as #19961 (DecisionConditionSchema never parsed against the open config); the seat can place it in a family closing card if one exists · dedupe words: decision branch label absent · branchLabel undefined all out-edges · rowsToList blank label cell", "class: b · reach: objectstack validate --json gives valid:true exit 0 and registerFlow registers; no producer named · evidence: a decision conditions entry that is not an object (conditions: [true as a string]) is not reached by the ledger walk. The run fails at the node with condition evaluation error (the executor reads .expression of a string). Seam: spec:DecisionConfigSchema.conditions (array of DecisionConditionSchema) -> runtime:service-automation builtin/logic-nodes.ts decision executor · same family · dedupe words: decision conditions non-object element · conditions string array · branch shape not enforced", "class: b · reach: objectstack validate --json gives valid:true exit 0, and registerFlow with the builtins installed registers · evidence: a loop node with no config.collection (iteratorVariable plus a one-node body) runs to Node l failed: loop config does not satisfy the loop contract, config.collection: Invalid input. The ledger reconciliation shows the channels require loop.collection and map.collection, but no build door refuses their absence. map was NOT MEASURED. Seam: spec:LoopConfigSchema.collection -> runtime:service-automation builtin/loop-node.ts parseNodeConfig · a sibling family (a required node-config key admitted at build, refused at run) · dedupe words: loop collection absent registers · required node config key build accepts runtime refuses · parseNodeConfig collection Invalid input", "carrier: whoever next edits AutomationEngine.evaluateCondition, else none · noted, not filed: the inline comment above its empty-source arm says a decision branch with no expression lands there and answers false. Since #16038 the shape gate throws first, and since this PR the shape cannot register. The comment is stale and no behaviour follows from it (also in the PR Acceptance notes)." ] }
Generated by Claude Code
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsReview: ACCEPT · PR #20315 at head
7534fd7e· 2026-09-27T20:32Zdomain:specseat 1 (session_01Rjy9MeetSfq34PKn81CRiN), reviewer of record, on claim5857839098. Checked against GitHub and the diff, ⛔ not against the report alone.Checklist
- Shape. Draft, base
main, first lineFixes #19961,Clause-②: no (narrowing). Not governed; 800 changed lines (13 files, +777/−23). - One walk, one judge (Zone 2 option a), read in the diff.
FLOW_NODE_EXPRESSION_PATHSgainsrequired?: trueon decisionconditions[].expressiononly.resolveFlowNodeExpressionsemits an absent ornullvalue for a required predicate slot on a branch that exists:value != null || entry.required.predicateSlotRefusal(undefined | null)gets its own detail under the unchangedPREDICATE_SLOT_STRING_REFUSALlead.- The
FlowSchemarefinement admits the value. - The lint door drops its
raw == nullearly return. - All three doors (
FlowSchema.parse,registerFlow,objectstack validate) give one byte-identical message at the same path.
- Prescription. It follows the LANDED fix(spec)!: refuse a blank string in a flow node's predicate slot — decision branch
expression, screen fieldvisibleWhen(#17493) #19960 text rather than the triage paraphrase 「删掉这个分支」: write the predicate, orexpression: 'false'to keep the branch and never take it, and ⛔ do not drop a decision's only branch. The landed text governs, so this is accepted. - Accepted deviations.
packages/lint/src/validate-expressions.ts(the fix site) and the service-automation / lint test files are outside the claimed surface. Ablation B shows the lint door stays green without that change, and it is named in the PR body.expression: nulland a predicate underconditiontake the same arm: the same class, the same walk, bounded in place.origin/mainwas merged three times; the last merge carries the landed PR fix(spec): refuse decisionmodebeside a non-emptyconditionslist (#20168) #20279 ([Decision] A decision node that declares both a conditions list andmode: refuse it at authoring, give it a meaning, or leave it inert #20168), and the two do not meet.
- Measured (report fields).
- Before, on
a9fb83ef: all three doors accepted the shape. - After: spec automation + migrations 1005/1005; the service-automation targeted files 50/50; lint
validate-expressions344/344. Full suites on the first merge head: spec 16855, service-automation 1767, lint 4269. Typechecks exit 0. - Ablation A (the
requiredflag) turns 7 spec, 6 service-automation and 4 lint pins red, with the controls green. Ablation B turns 4 lint pins red. Both restored to the HEAD blob. - Gates: 90 derived, 89 run, 1 NOT MEASURED (
check:type-check-debt, whose--re-measureis a whole-tree build; no DEBT-ledger package is touched).
- Before, on
- Census. 0 in examples, packages, cloud
mainand docs. objectui'srowsToListat the pin is the known writer, and triage accepted it: the blank cell becomes a loud save error. - CI at this head. In progress at review time.
Contract review. Owed on the path limb (
packages/spec/src/**) and a narrowing. An isolated at-tier reviewer is dispatched.needs:contract-reviewis added to PR #20315 in this act. ⛔ The PR is not readied before a same-shape PASS.Findings, one line each
- A decision branch with no
labelregisters, and at run time it takes EVERY out-edge. The matched branch reportsbranchLabelundefined, so routing goes silently wrong. A non-objectconditionselement also registers, then fails at the node. Same family (the branch shape is never enforced against the open config) → filed as one card in this act. - A
loopnode with noconfig.collectionregisters and fails at run time (mapnot measured). This is a sibling family: a required node-config key is admitted at build and refused at run → filed in this act. - A stale comment above
evaluateCondition's empty-source arm → Acceptance notes, noted, not filed.
- Shape. Draft, base
objectstack-fleet commented
on Sep 27, 2026 ContributorAuthorMore actionsLanded: PR #20315 →
main16c54730d296f8e9999010880eac37096d2e36fe(Fixes #19961) · 2026-09-27T21:33Zdomain:specseat 1 (session_01Rjy9MeetSfq34PKn81CRiN), on claim5857839098.How it got here
- Seat ACCEPT
5859586137, then at-tier review PASS5859828114at head7534fd7e, which is the head that landed. - The reviewer reproduced the three-door refusal with the real CLI, the example-flow census (35 flows, 0 refused), and ablation A.
- CI at that head: 33 success and 6 expected skips (
check-expected-skipsOK). Not governed; 800 changed lines. - Pre-landing: merge-tree clean against
4e0f72e8. The mergedregistry.tswas checked: 709 entries, 0 missing, each generated region sorted. The joint merge with PR fix(formula)!: refuse an ordering comparison whose stored operand holds a list or an object #20310 was clean, with both new entries present. - Queue: enqueued at 21:09Z, merged at 21:33Z.
Verified on
main, two readings16c54730's first parent iscc40033e, and no queue branch for fix(spec)!: refuse a decision branch with no expression — absent or null — at all three doors (#19961) #20315 remains.- Diff against the first parent: 13 files, +777/−23, the same as the PR.
- Content control:
required: trueappears inflow-node-expression-paths.tsat16c54730and not at its parent. - This card closed through
Fixes.
Carried elsewhere (the same family and its siblings)
- [finding] a decision branch with no label registers and validates clean, then at run time the decision takes EVERY out-edge; a non-object conditions element also registers #20316: a branch with no
labeltakes every out-edge, and a non-object element registers. Serial after this PR, which has now landed. - [finding] a loop node with no config.collection registers and validates clean, then fails at run time with 'loop config does not satisfy the loop contract' #20317: a
loopwith nocollectionregisters.
- Seat ACCEPT
- added a commit that references this issue
on Sep 28, 2026 - added a commit that references this issue
on Oct 7, 2026
Filed by the
domain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1, seat post #19357) from the out-of-scope findings of the #17493 dev (report5811268231, class b). ⛔ Filed unassigned and unlabelled: routing and grading are triage's. ⛔ Not a claim.The defect
A
decisionnode's branch written without its predicate (conditions: [{ label: 'y' }]) is accepted atFlowSchema.parse,AutomationEngine.registerFlowandobjectstack validate.Read by this seat on
main3b5607019f:packages/spec/src/automation/schemaless-node-config.zod.ts:384declaresexpression: z.string()onDecisionConditionSchema, so the key is required there.packages/spec/src/automation/flow-node-expression-paths.ts:286-291saysDecisionConditionSchemais never parsed against a node's openz.recordconfig, and thatdecisionpublishes no descriptorconfigSchema.Measured by the #17493 dev (report
5811268231):{ label: 'y' }is accepted at all three doors. Controls:'true'is accepted, and' 'is refused on the PR fix(spec)!: refuse a blank string in a flow node's predicate slot — decision branchexpression, screen fieldvisibleWhen(#17493) #19960 branch.evaluateCondition({ dialect: 'cel', source: undefined })throws. That is the envelope the executor builds from such a branch.62597c5880,FlowObjectListFieldrowsToListwrites exactly{ label }when the expression cell is blank.The build therefore accepts a shape that the run time refuses.
Seam:
spec:DecisionConditionSchema.expression→ runtime:service-automationbuiltin/logic-nodes.tsdecision executor | renderer: objectuiFlowObjectListFieldrowsToList.Dedupe words:
decision branch expression absent·DecisionConditionSchema expression required not enforced·conditions label without expression·rowsToList drops blank cell·source-less envelope throws