Skip to content

ConditionBuilder row mode quotes a reference typed in the value box into a string literal — every field-to-field / field-to-previous comparison compiles to a comparison against text #6293

Description

@os-litant

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

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions