Filing as a finding — unassigned, not claimed. Found while taking the "first measure whether the current ConditionBuilder can express the flow-entry idioms" reading that the batch-4 ruling on #6226 ordered. This defect is not specific to the flow designer: it is in ConditionBuilder itself and is live today at all five of its existing consumers.
What
ConditionBuilder's row mode compiles [subject][operator][value] rows to CEL through fmtValue, in packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.tsx:
/** Quote a raw value for CEL unless it is a number / boolean / null. */functionfmtValue(v: string): string{constt=v.trim();if(t==='true'||t==='false'||t==='null')returnt;if(t!==''&&!Number.isNaN(Number(t)))returnt;return`'${t.replace(/'/g,"\\'")}'`;}The value box is free text. Anything that is not a number, true, false or null is wrapped in quotes — including a value that is plainly a reference. So an author who builds "this field differs from its prior value" in the row builder gets a predicate that compares the field to the literal text of the reference.
Measured, on the real component
Mounted the real ConditionBuilder in jsdom on origin/main (c1a91b842), opened it on previous == null (which opens in row mode), and typed into its live value box. Captured what onCommit emitted:
positive probe — opened in ROW mode: true
positive probe — value input present, current value: "null"
author types a REFERENCE into the value box: "previous.status"
onCommit emitted: "previous == 'previous.status'"
CONTROL — author types a literal "done"
onCommit emitted: "previous == 'done'"
CONTROL — author types a number "42"
onCommit emitted: "previous == 42"
The number control is there to show the quoting is value-dependent rather than unconditional, so the reading reflects the input rather than a constant.
previous == 'previous.status' parses, registers, and evaluates. It is simply always false. Nothing at any layer objects: it is syntactically valid CEL, previous is a declared root, and a string literal's contents are deliberately not scanned for references by flow-ref-check or by the server-side expression validator.
Why it matters
Field-to-field comparison is not an exotic idiom at these surfaces — it is the defining shape of a change-detection predicate. The hook condition shipped in the showcase app is previous.done != true && record.done == true; every record-change flow entry gate in the example apps compares a field to its previous counterpart.
The five surfaces that mount ConditionBuilder today:
views/studio-design/ObjectValidationsPanel.tsx (validation rules)views/metadata-admin/inspectors/PageBlockInspector.tsx (visible-when)views/metadata-admin/inspectors/HookDefaultInspector.tsx (hook condition — the surface whose canonical idiom is exactly this)views/metadata-admin/inspectors/ActionDefaultInspector.tsx (visible-when / disabled-when, two mount sites)views/metadata-admin/widgets.tsx (condition widget)
The failure is silent in both directions: the compiled string looks plausible in the preview strip under the rows, and the predicate never fires. There is no author-time signal at all.
Worth noting on the "hard to get wrong" axis: this is the shape where a consumer-side tolerance would be the wrong repair. The fix belongs at the authoring surface — either a value control that can express "another field" as a first-class choice, or a refusal to silently quote a reference-shaped value.
Not filed as part of #6226
#6226 is about wiring this component into the flow designer; that card's own measurement is reported on it. This defect predates that wiring and lands at the five surfaces above regardless of what #6226 decides, so it is filed on its own rather than as a rider. #6226 remains open and is not addressed here.
Generated by Claude Code
Filing as a finding — unassigned, not claimed. Found while taking the "first measure whether the current ConditionBuilder can express the flow-entry idioms" reading that the batch-4 ruling on #6226 ordered. This defect is not specific to the flow designer: it is in
ConditionBuilderitself and is live today at all five of its existing consumers.What
ConditionBuilder's row mode compiles[subject][operator][value]rows to CEL throughfmtValue, inpackages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.tsx:The value box is free text. Anything that is not a number,
true,falseornullis wrapped in quotes — including a value that is plainly a reference. So an author who builds "this field differs from its prior value" in the row builder gets a predicate that compares the field to the literal text of the reference.Measured, on the real component
Mounted the real
ConditionBuilderin jsdom onorigin/main(c1a91b842), opened it onprevious == null(which opens in row mode), and typed into its live value box. Captured whatonCommitemitted:The number control is there to show the quoting is value-dependent rather than unconditional, so the reading reflects the input rather than a constant.
previous == 'previous.status'parses, registers, and evaluates. It is simply always false. Nothing at any layer objects: it is syntactically valid CEL,previousis a declared root, and a string literal's contents are deliberately not scanned for references byflow-ref-checkor by the server-side expression validator.Why it matters
Field-to-field comparison is not an exotic idiom at these surfaces — it is the defining shape of a change-detection predicate. The hook condition shipped in the showcase app is
previous.done != true && record.done == true; every record-change flow entry gate in the example apps compares a field to itspreviouscounterpart.The five surfaces that mount
ConditionBuildertoday:views/studio-design/ObjectValidationsPanel.tsx(validation rules)views/metadata-admin/inspectors/PageBlockInspector.tsx(visible-when)views/metadata-admin/inspectors/HookDefaultInspector.tsx(hook condition — the surface whose canonical idiom is exactly this)views/metadata-admin/inspectors/ActionDefaultInspector.tsx(visible-when / disabled-when, two mount sites)views/metadata-admin/widgets.tsx(condition widget)The failure is silent in both directions: the compiled string looks plausible in the preview strip under the rows, and the predicate never fires. There is no author-time signal at all.
Worth noting on the "hard to get wrong" axis: this is the shape where a consumer-side tolerance would be the wrong repair. The fix belongs at the authoring surface — either a value control that can express "another field" as a first-class choice, or a refusal to silently quote a reference-shaped value.
Not filed as part of #6226
#6226 is about wiring this component into the flow designer; that card's own measurement is reported on it. This defect predates that wiring and lands at the five surfaces above regardless of what #6226 decides, so it is filed on its own rather than as a rider. #6226 remains open and is not addressed here.
Generated by Claude Code