You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Measured on @objectstack/cli / @objectstack/spec17.2.0 while building a manager dashboard in the duly app (objectstack-ai/duly#10). Each mutation was applied to a real, shipping dashboard, confirmed on disk, and both gates re-run.
The interesting part is how narrow the remaining gap is: almost everything on this surface is already checked, including the sibling of one of the two misses.
What IS caught (no action needed — recorded so the gap below is precise)
mutated reference
validate
build
rule
widgets[].dataset → duly_stagnatoin
1
1
widget-dataset-unknown (+ "did you mean")
widgets[].dimensions[] → business_unitt
1
1
widget-dimension-unknown
widgets[].values[] → untouched_over_14
1
1
widget-measure-unknown
{token} inside widgets[].filter → {14_days_hence}
1
1
filter-token-unknown, path-precise at dashboards[0].widgets[4].filter.due_date.$lte
// widget bound to dataset `duly_workload`, base object `duly_task`filter: {due_daet: {$gte: '{today}',$lte: '{14_days_from_now}'}},// ^^^^^^^^ no such column on duly_task
validate → 0. build → 0. The condition reaches the engine, matches nothing, and the widget renders empty.
Two things make this sharper than a generic missing check:
On the same node, the TOKEN is checked and the COLUMN is not.filter-token-unknown fires with the exact path …widgets[4].filter.due_date.$lte, so the traversal already walks into the filter tree and already knows the widget's dataset. Only the key resolution is missing.
dashboard "duly_duty_health" › widget "not_moving_14d": inherits dashboard filter dateRange(nonexistent_column), but object duly_task (dataset "duly_stagnation") has no field nonexistent_column. … Object fields: subject, duty, owner, business_unit, …
So dataset → base object → field set is already computed per widget at that point. widgets[].filter is the same field-existence invariant on the filter an author is more likely to write by hand, and it was simply not fed through the same rule.
B — options.sortBy is not checked against what the widget selects
validate → 0. build → 0. The spec states the contract in its own words — "Order rows by this dimension or measure name — must be one this widget actually selects" (DashboardWidgetOptionsSchema.sortBy) — and DashboardWidgetOptions is one of the four keys deliberately declared because they reach the analytics query (framework#3588). The runtime falls back to ordering by the selected dimensions, so the authored order silently does not happen.
Why it matters beyond tidiness: for us the authored order is a product rule. A per-unit chart must be ordered by the unit dimension and never by the count, because ordering units by a count turns a workload chart into a league table. Today nothing but a hand-written repo test keeps that from silently degrading into "whatever the runtime picked".
Why silent-empty is the expensive failure here
The dashboard this came from leads with a "not moving" tile — open work untouched >14 days. An empty tile is indistinguishable from a healthy team: a missing number reads as zero, and zero is the answer the manager is hoping for. Every other class of authoring mistake on this surface already fails loudly at build; these two are the ones that ship.
Suggested fix
Both are one pass at the same site that already emits widget-dataset-unknown / widget-dimension-unknown / dashboard-filter-field-unknown:
A — for each widget with a filter, walk the condition's field KEYS (descending $and / $or / $not, skipping $-prefixed operator keys) and resolve each against the widget's dataset base object plus the platform system columns. Suggested rule id widget-filter-field-unknown, message shaped like dashboard-filter-field-unknown so the two read alike. A dotted path through a declared include should resolve or be explicitly out of scope, not silently pass.
B — error unless options.sortBy is in dimensions[] ∪ values[] for that widget. Suggested widget-sortby-unselected, listing what the widget does select.
Acceptance: both fail validate and build naming dashboard, widget, key and object; fixture coverage for the failing and the clean shape.
Happy to take either one if useful — the shape is mechanical and mirrors #3382.
Repo-local stopgap meanwhile: test/dashboard.test.ts in objectstack-ai/duly resolves both against the datasets barrel, and is written to be deleted when this lands.
Measured on
@objectstack/cli/@objectstack/spec17.2.0 while building a manager dashboard in thedulyapp (objectstack-ai/duly#10). Each mutation was applied to a real, shipping dashboard, confirmed on disk, and both gates re-run.The interesting part is how narrow the remaining gap is: almost everything on this surface is already checked, including the sibling of one of the two misses.
What IS caught (no action needed — recorded so the gap below is precise)
validatebuildwidgets[].dataset→duly_stagnatoinwidget-dataset-unknown(+ "did you mean")widgets[].dimensions[]→business_unittwidget-dimension-unknownwidgets[].values[]→untouched_over_14widget-measure-unknown{token}insidewidgets[].filter→{14_days_hence}filter-token-unknown, path-precise atdashboards[0].widgets[4].filter.due_date.$ltedateRange.field→nonexistent_columndashboard-filter-field-unknown(#3365 / #3382)dashboardName→duly_ghostdefineStackcross-reference validationA —
widgets[].filterkeys are not resolvedvalidate→ 0.build→ 0. The condition reaches the engine, matches nothing, and the widget renders empty.Two things make this sharper than a generic missing check:
On the same node, the TOKEN is checked and the COLUMN is not.
filter-token-unknownfires with the exact path…widgets[4].filter.due_date.$lte, so the traversal already walks into the filter tree and already knows the widget's dataset. Only the key resolution is missing.The identical resolution already exists one key over.
dashboard-filter-field-unknown(Validate dashboard filter field-existence at build time (extend ADR-0021) #3365, shipped in feat(lint): validate dashboard filter field-existence at build time (#3365) #3382) resolves a dashboard-level filter's field against each widget's dataset base object, and its message even prints that object's field list:So dataset → base object → field set is already computed per widget at that point.
widgets[].filteris the same field-existence invariant on the filter an author is more likely to write by hand, and it was simply not fed through the same rule.B —
options.sortByis not checked against what the widget selectsvalidate→ 0.build→ 0. The spec states the contract in its own words — "Order rows by this dimension or measure name — must be one this widget actually selects" (DashboardWidgetOptionsSchema.sortBy) — andDashboardWidgetOptionsis one of the four keys deliberately declared because they reach the analytics query (framework#3588). The runtime falls back to ordering by the selected dimensions, so the authored order silently does not happen.Why it matters beyond tidiness: for us the authored order is a product rule. A per-unit chart must be ordered by the unit dimension and never by the count, because ordering units by a count turns a workload chart into a league table. Today nothing but a hand-written repo test keeps that from silently degrading into "whatever the runtime picked".
Why silent-empty is the expensive failure here
The dashboard this came from leads with a "not moving" tile — open work untouched >14 days. An empty tile is indistinguishable from a healthy team: a missing number reads as zero, and zero is the answer the manager is hoping for. Every other class of authoring mistake on this surface already fails loudly at build; these two are the ones that ship.
Suggested fix
Both are one pass at the same site that already emits
widget-dataset-unknown/widget-dimension-unknown/dashboard-filter-field-unknown:filter, walk the condition's field KEYS (descending$and/$or/$not, skipping$-prefixed operator keys) and resolve each against the widget's dataset base object plus the platform system columns. Suggested rule idwidget-filter-field-unknown, message shaped likedashboard-filter-field-unknownso the two read alike. A dotted path through a declaredincludeshould resolve or be explicitly out of scope, not silently pass.options.sortByis indimensions[] ∪ values[]for that widget. Suggestedwidget-sortby-unselected, listing what the widget does select.Acceptance: both fail
validateandbuildnaming dashboard, widget, key and object; fixture coverage for the failing and the clean shape.Happy to take either one if useful — the shape is mechanical and mirrors #3382.
Repo-local stopgap meanwhile:
test/dashboard.test.tsin objectstack-ai/duly resolves both against the datasets barrel, and is written to be deleted when this lands.Generated by Claude Code