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:
- 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. - 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?). - 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 (stripReadonlyFieldsObject.is provenance).
Unassigned and untriaged, per the single-producer rule for domain:*.
Found while building an ObjectStack application in
objectstack-ai/dulyagainst published@objectstack/*17.2.0. Filed here because the fix lands inpackages/lint.Measured
On a real
record_changeflow whose START node config bindsobjectName: 'duly_assignment':objectstack validateP\status == "dispatched"`` — bare identifierP\record.needs_colection == true`` — typo'd field, same siteunknown 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, andcollectBoundRecordReadssays so deliberately: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:
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:
iteratorVariable/indexVariable, assignment targets, input/output variables) — including insideloopand region node bodies, where a hand-written predicate is easiest to miss.did you mean record.status?).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 generatescaffolds a flow the schema rejects) and #14088 (stripReadonlyFieldsObject.isprovenance).Unassigned and untriaged, per the single-producer rule for
domain:*.