Measured on @objectstack/cli 17.2.0 from a real app (objectstack-ai/duly). Every reading below has the mutation confirmed on disk before the command ran, and the tree restored after.
What is unchecked
A list view names fields in at least five places. None of them is resolved against the bound object — not by os validate, and not by os build, which is the publish gate:
| surface |
mutation |
os validate |
os build |
columns[].field |
'period_key' → 'B2_no_such_field' |
valid: true, warnings: [] |
exit 0, ✓ Build complete |
filter[].field |
'visible_from' → 'A8_no_such_field' |
valid: true, warnings: [] |
— |
grouping.fields[].field |
'business_unit' → 'A7_no_such_field' |
valid: true, warnings: [] |
— |
kanban.groupByField |
'status' → 'A9_no_such_field' |
valid: true, warnings: [] |
— |
gantt.startDateField |
'visible_from' → 'B1_no_such_field' |
valid: true, warnings: [] |
exit 0, ✓ Build complete |
The object (duly_task) is defined in the same stack and resolves fine; the field names simply do not exist on it.
Why this matters more than a normal typo
Every one of these fails silently at render, in the way ADR-0078 and the view/layout-without-binding rule already treat as the failure worth gating:
- a bad
columns[].field renders a column of blanks;
- a bad
filter[].field is sent to the engine and matches nothing — an empty list that looks like a true zero;
- a bad
gantt.startDateField makes every row's start unparseable, and the gantt renderer keeps only rows whose mapping resolved — a blank chart;
- a bad
kanban.groupByField collapses every card into the uncolumned bucket.
The irony is that the platform already ships the harder half of this check. view/layout-without-binding warns when a binding block is absent — precisely because "the renderer falls back to literal default field names … and the view renders empty while authoring reports success". A block that is present but points at a field that does not exist reaches the identical end state, and gets nothing.
It is also strictly easier to check than the cases that already are: the object is named right there in data.object, and validateReferenceIntegrity already walks the stack with the object universe in hand.
Suggested fix
One rule, view/field-ref-unknown, over every field-naming position on a ListViewSchema, keyed off data.object (skip when the provider is not object, and skip dotted paths, as the chart rule already does). The <field> is not a field on object <object> message and the Object fields: … suffix from REACT_CHART_FIELD_UNKNOWN are the right precedent — that rule does exactly this job for <ObjectChart aggregate> and shows the shape works.
Positions to cover: columns[] (both the bare-string and { field } forms), filter[].field, sort[].field, grouping.fields[].field, searchableFields[], filterableFields[], hiddenFields[], fieldOrder[], rowColor.field, userFilters.fields[].field, and every *Field / fields[] / columns[] key inside the kanban / calendar / gantt / timeline / gallery / map / tree blocks.
Severity: error matches the consequence, but warning first would already close the gap for authors and AI authors, who currently get no signal from any of the four gates.
Workaround in the meantime
objectstack-ai/duly carries a repo-local stopgap test (test/views.test.ts) that walks dulyViews and resolves every field reference against dulyObjects. It is written to be deleted when this lands, not maintained — the same shape as the test/flow-predicates.test.ts stopgap for #14089.
Measured on
@objectstack/cli17.2.0 from a real app (objectstack-ai/duly). Every reading below has the mutation confirmed on disk before the command ran, and the tree restored after.What is unchecked
A list view names fields in at least five places. None of them is resolved against the bound object — not by
os validate, and not byos build, which is the publish gate:os validateos buildcolumns[].field'period_key'→'B2_no_such_field'valid: true,warnings: []✓ Build completefilter[].field'visible_from'→'A8_no_such_field'valid: true,warnings: []grouping.fields[].field'business_unit'→'A7_no_such_field'valid: true,warnings: []kanban.groupByField'status'→'A9_no_such_field'valid: true,warnings: []gantt.startDateField'visible_from'→'B1_no_such_field'valid: true,warnings: []✓ Build completeThe object (
duly_task) is defined in the same stack and resolves fine; the field names simply do not exist on it.Why this matters more than a normal typo
Every one of these fails silently at render, in the way ADR-0078 and the
view/layout-without-bindingrule already treat as the failure worth gating:columns[].fieldrenders a column of blanks;filter[].fieldis sent to the engine and matches nothing — an empty list that looks like a true zero;gantt.startDateFieldmakes every row's start unparseable, and the gantt renderer keeps only rows whose mapping resolved — a blank chart;kanban.groupByFieldcollapses every card into the uncolumned bucket.The irony is that the platform already ships the harder half of this check.
view/layout-without-bindingwarns when a binding block is absent — precisely because "the renderer falls back to literal default field names … and the view renders empty while authoring reports success". A block that is present but points at a field that does not exist reaches the identical end state, and gets nothing.It is also strictly easier to check than the cases that already are: the object is named right there in
data.object, andvalidateReferenceIntegrityalready walks the stack with the object universe in hand.Suggested fix
One rule,
view/field-ref-unknown, over every field-naming position on aListViewSchema, keyed offdata.object(skip when the provider is notobject, and skip dotted paths, as the chart rule already does). The<field> is not a field on object <object>message and theObject fields: …suffix fromREACT_CHART_FIELD_UNKNOWNare the right precedent — that rule does exactly this job for<ObjectChart aggregate>and shows the shape works.Positions to cover:
columns[](both the bare-string and{ field }forms),filter[].field,sort[].field,grouping.fields[].field,searchableFields[],filterableFields[],hiddenFields[],fieldOrder[],rowColor.field,userFilters.fields[].field, and every*Field/fields[]/columns[]key inside thekanban/calendar/gantt/timeline/gallery/map/treeblocks.Severity:
errormatches the consequence, butwarningfirst would already close the gap for authors and AI authors, who currently get no signal from any of the four gates.Workaround in the meantime
objectstack-ai/dulycarries a repo-local stopgap test (test/views.test.ts) that walksdulyViewsand resolves every field reference againstdulyObjects. It is written to be deleted when this lands, not maintained — the same shape as thetest/flow-predicates.test.tsstopgap for #14089.