Summary
An ADR-0021 dataset can name a base object that does not exist, join a
relationship that does not exist, and bind every dimension and measure to
fields that do not exist, and objectstack validate exits 0 with
✓ Validation passed. objectstack build also exits 0 and writes the dangling dataset
into dist/objectstack.json.
The sting is that the author-time rule pass already walks these exact nodes: a bad
date-macro token in a measure filter is caught, path-precise, on the same traversal.
So the machinery and the traversal are both present — only the reference resolution is
missing.
This is one level below #7529 (closed by #8902, which refuses a widget → dataset
binding that names nothing). This is the dataset → object/field binding underneath it.
A board can now be proven to point at a real dataset, and that dataset can still point at
nothing.
Measured
@objectstack/spec 17.2.0, @objectstack/cli 17.2.0, on objectstack-ai/duly
(3 datasets over duly_task). Each row is one mutation applied on its own, confirmed on
disk before running (old spelling absent, new spelling present), then reverted.
| # | Mutation | pnpm validate |
|---|
| 1 | dimension field: 'period_key' → 'period_kee' (base field) | exit 0, passed |
| 2 | dimension field: 'duty.frequency' → 'duty.frequenci' (joined field) | exit 0, passed |
| 3 | measure field: 'last_update_at' → 'last_update_att' | exit 0, passed |
| 4 | measure filter key last_update_at → last_update_attt | exit 0, passed |
| 5 | include: ['duty'] → ['dutee'] (relationship does not exist) | exit 0, passed |
| 6 | object: 'duly_task' → 'duly_tsk' (base object does not exist) | exit 0, passed |
| 7 | duplicate measure name (schema superRefine) | exit 1 — caught |
| 8 | filter value '{7_days_ago}' → '{7_fortnights_ago}' | exit 1 — caught |
Rows 7 and 8 are the control: datasets genuinely are in the validation path, so rows 1–6
are not "the file was never read".
Row 8's message shows the walker reaching all the way into a measure filter with an exact
path:
✗ Author-time rules failed (1 issue)
• dataset "duly_stagnation": Filter value "{7_fortnights_ago}" is not a resolvable placeholder.
It is sent to the data engine as a literal string, matches no record, and the surface renders empty.
rule: filter-token-unknown at datasets[1].measures[1].filter.last_update_at.$lt
That message is the whole argument for this issue. filter-token-unknown already stands
at datasets[1].measures[1].filter.last_update_at.$lt and reasons about the value.
Nothing standing in that same position resolves the key — or the sibling
measures[].field, dimensions[].field, include[], or object.
Why this matters more for a dataset than for most metadata
filter-token-unknown's own wording states the failure mode it exists to prevent:
"sent to the data engine as a literal string, matches no record, and the surface renders
empty." A dangling field path produces the same outcome from the same node and is not
checked.
A dataset is the semantic layer: dashboards and reports bind its dimensions and measures
by name (ADR-0021), and the consumer end is now guarded (#8902). So the surviving failure
is the quiet one — every binding resolves, the board renders, and the charts are empty or
subtly wrong because the dataset underneath addresses columns that do not exist.
Row 6 is the one I would prioritise: a dataset over a non-existent base object is
unambiguously meaningless, has no forward-reference story worth preserving, and is the
cheapest possible check.
Precedent in the tree
The platform already resolves qualified field reads elsewhere and produces a good message.
From the same repo's flow-condition validation:
unknown field `needs_colection` on `duly_assignment` — did you mean `needs_collection`?
That is the message shape this wants. The resolver exists; it is not wired to dataset
field paths.
Related: #14089 (bare identifiers in flow conditions have no validation leg) is the same
family — a validator with a per-surface hole — though a different surface.
Suggested scope
- Resolve
Dataset.object against the object registry. (Cheapest, highest value.) - Resolve each
include[] path hop-by-hop against the object graph, honouring the
ADR-0071 3-hop limit that is already enforced structurally. - Resolve
dimensions[].field and measures[].field — a base field, or a
relationship[.relationship].field path whose relationship prefix must appear in
include. That last clause is a second real check: a dot-path whose prefix was never
declared in include is not joinable even if the field exists. - Resolve filter keys in
Dataset.filter and measures[].filter, reusing the
traversal filter-token-unknown already performs on the values.
Timing: validate/build at the latest, matching #8902's "refuse at publish at the
latest" posture for the layer above.
Reproduction
git clone objectstack-ai/duly, pnpm install.- In
src/datasets/duty-health.dataset.ts, change object: 'duly_task' to
object: 'duly_tsk'. pnpm validate → ✓ Validation passed, exit 0.pnpm build → exit 0; dist/objectstack.json contains the dataset with the dangling
base object.
Filed from the duly dogfood application (objectstack-ai/duly#9).
Summary
An ADR-0021 dataset can name a base object that does not exist, join a
relationship that does not exist, and bind every dimension and measure to
fields that do not exist, and
objectstack validateexits 0 with✓ Validation passed.objectstack buildalso exits 0 and writes the dangling datasetinto
dist/objectstack.json.The sting is that the author-time rule pass already walks these exact nodes: a bad
date-macro token in a measure filter is caught, path-precise, on the same traversal.
So the machinery and the traversal are both present — only the reference resolution is
missing.
This is one level below #7529 (closed by #8902, which refuses a widget → dataset
binding that names nothing). This is the dataset → object/field binding underneath it.
A board can now be proven to point at a real dataset, and that dataset can still point at
nothing.
Measured
@objectstack/spec17.2.0,@objectstack/cli17.2.0, onobjectstack-ai/duly(3 datasets over
duly_task). Each row is one mutation applied on its own, confirmed ondisk before running (old spelling absent, new spelling present), then reverted.
pnpm validatefield: 'period_key'→'period_kee'(base field)field: 'duty.frequency'→'duty.frequenci'(joined field)field: 'last_update_at'→'last_update_att'last_update_at→last_update_atttinclude: ['duty']→['dutee'](relationship does not exist)object: 'duly_task'→'duly_tsk'(base object does not exist)superRefine)'{7_days_ago}'→'{7_fortnights_ago}'Rows 7 and 8 are the control: datasets genuinely are in the validation path, so rows 1–6
are not "the file was never read".
Row 8's message shows the walker reaching all the way into a measure filter with an exact
path:
That message is the whole argument for this issue.
filter-token-unknownalready standsat
datasets[1].measures[1].filter.last_update_at.$ltand reasons about the value.Nothing standing in that same position resolves the key — or the sibling
measures[].field,dimensions[].field,include[], orobject.Why this matters more for a dataset than for most metadata
filter-token-unknown's own wording states the failure mode it exists to prevent:"sent to the data engine as a literal string, matches no record, and the surface renders
empty." A dangling field path produces the same outcome from the same node and is not
checked.
A dataset is the semantic layer: dashboards and reports bind its dimensions and measures
by name (ADR-0021), and the consumer end is now guarded (#8902). So the surviving failure
is the quiet one — every binding resolves, the board renders, and the charts are empty or
subtly wrong because the dataset underneath addresses columns that do not exist.
Row 6 is the one I would prioritise: a dataset over a non-existent base object is
unambiguously meaningless, has no forward-reference story worth preserving, and is the
cheapest possible check.
Precedent in the tree
The platform already resolves qualified field reads elsewhere and produces a good message.
From the same repo's flow-condition validation:
That is the message shape this wants. The resolver exists; it is not wired to dataset
field paths.
Related: #14089 (bare identifiers in flow conditions have no validation leg) is the same
family — a validator with a per-surface hole — though a different surface.
Suggested scope
Dataset.objectagainst the object registry. (Cheapest, highest value.)include[]path hop-by-hop against the object graph, honouring theADR-0071 3-hop limit that is already enforced structurally.
dimensions[].fieldandmeasures[].field— a base field, or arelationship[.relationship].fieldpath whose relationship prefix must appear ininclude. That last clause is a second real check: a dot-path whose prefix was neverdeclared in
includeis not joinable even if the field exists.Dataset.filterandmeasures[].filter, reusing thetraversal
filter-token-unknownalready performs on the values.Timing:
validate/buildat the latest, matching #8902's "refuse at publish at thelatest" posture for the layer above.
Reproduction
git clone objectstack-ai/duly,pnpm install.src/datasets/duty-health.dataset.ts, changeobject: 'duly_task'toobject: 'duly_tsk'.pnpm validate→✓ Validation passed, exit 0.pnpm build→ exit 0;dist/objectstack.jsoncontains the dataset with the danglingbase object.
Filed from the
dulydogfood application (objectstack-ai/duly#9).