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:*.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions