Skip to content

validate-expressions has no flow leg for bare identifiers — a bare field reference in a flow condition passes objectstack validate clean #14089

Description

@os-warren

Found while building an ObjectStack application in objectstack-ai/duly against published @objectstack/* 17.2.0. Filed here because the fix lands in packages/lint.

Measured

On a real record_change flow whose START node config binds objectName: 'duly_assignment':

Mutation to the START-node condition objectstack validate
P\status == "dispatched"`` — bare identifier exit 0, clean
P\record.needs_colection == true`` — typo'd field, same site exit 1, unknown field \needs_colection` on `duly_assignment` — did you mean `needs_collection`?`

Both mutations were confirmed on disk (injected literal present, removed literal absent, non-empty git diff --stat) before each run, and the tree restored after.

So the validator does resolve the bound object from the START node and does check field existence behind record. — the binding works. It skips only the bare-identifier case, and collectBoundRecordReads says so deliberately:

Deliberately NEVER a bare identifier: in a flattened flow scope a bare name may be a flow variable.

Why the exemption costs more than it saves

The reasoning is sound in isolation — a bare name in a flattened flow scope genuinely may be a flow variable, so a blanket rejection would produce false positives. But the current behaviour makes the documented guidance unenforceable exactly where it matters most:

  • Flow conditions are the surface where a wrong predicate is least visible. A view filter that matches nothing renders an empty grid someone notices. A flow condition that never fires produces no output at all — no record, no error, no log line. The dispatcher simply does not dispatch.
  • And in flows the failure is not even the documented one: a bare name does not evaluate to null, it either resolves through record flattening or throws (ADR-0032 §1c). So an author following the rule gets one failure mode, and an author breaking it gets a different, undocumented one.

Suggested direction

The false-positive concern is addressable rather than fatal, because the flow's own variable scope is authored metadata and therefore knowable at validate time:

  1. Collect the flow's declared variables (loop iteratorVariable/indexVariable, assignment targets, input/output variables) — including inside loop and region node bodies, where a hand-written predicate is easiest to miss.
  2. A bare identifier that matches none of them, but does match a field on the bound object, is a near-certain error and can be rejected with a corrective message (did you mean record.status?).
  3. A bare identifier matching neither is the genuinely ambiguous case and can stay a warning.

That keeps every legitimate flow-variable reference passing while catching the actual mistake, which is a field name written without its record. prefix.

Meanwhile

The consuming application is adding a local walk over its own flows as a stopgap. That is a workaround for a gap that belongs here — every ObjectStack application will need the same walk otherwise, which is the argument for fixing it once in packages/lint.

Related, filed separately: #14087 (objectstack generate scaffolds a flow the schema rejects) and #14088 (stripReadonlyFields Object.is provenance).

Unassigned and untriaged, per the single-producer rule for domain:*.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions