Filed from an app repo (objectstack-ai/hotclm, @objectstack/* 17.3.0, @objectstack/console as shipped, objectui at 8f9d87a) while building its three §09 dashboards — objectstack-ai/hotclm#29, PR objectstack-ai/hotclm#30. ⛔ Unassigned, ungraded; routing is triage's.
Two findings, filed together because they are the same contract seam — what a dataset-bound widget's authored keys actually do — and because they pull in opposite directions: one drops authored metadata without a word, the other warns about a failure that never happens. Split them if triage prefers a card per defect.
Provenance. Both were measured in a browser by the card-10 implementation run. I reviewed the evidence and the reasoning; I did not re-drive the browser myself, and I say so rather than implying otherwise.
Dedup: searched DatasetWidget / chart-config-missing / zero-dimension widget. Nothing open or closed covers either. Related but not this: objectstack-ai/hotclm PR #25's record:details findings, filed separately as #16695.
Finding 1 — a zero-dimension widget is a KPI card whatever its declared type, and the extra measures vanish
DatasetWidget.tsx:423:
METRIC_TYPES.has(widgetType) || dimensions.length === 0
So a widget declaring type: 'bar' (or 'table') that selects four measures and no dimension renders as a single KPI card showing one number. The other three are dropped — no error, no warning, no placeholder.
Observed: an approval-routing tile selecting four route counts rendered
11 · Routes: Head of Legal
and nothing else. Three of four rungs silently absent from a card that looked finished and deliberate. objectstack validate and objectstack lint both pass. It was caught only by reading the rendered DOM and asking why one number was there instead of four.
Why this is worth a card and not a docs note. The failure mode is a dashboard that looks complete and is wrong — the reviewer's eye has nothing to catch, because a KPI card showing a real number is exactly what a finished tile looks like. It is the metadata-the-runtime-silently-drops class: the author declared type: 'bar', the runtime decided otherwise, and nothing in between said so.
A spec-side refusal (a bar/table with zero dimensions is not authorable) or a lint rule would both close it. The app worked around it by splitting the tile into four, which is correct for the app and does nothing for the next author.
Finding 2 — chart-config-missing asserts a rendering failure that does not occur on dataset-bound widgets
The rule's message reads:
the renderer cannot determine which measure to plot, so the series renders empty.
On a dataset-bound widget that is not what happens. objectui's buildChartSeries(rows, dimensions, values, fields) derives the bindings from the widget's own dimension/measure selection, and an authored chartConfig is merged onto that derivation as presentation only (mergeAuthoredPresentation).
Measured in Chromium with no chartConfig anywhere in the app: 7 funnel trapezoids · 3 bars · 2 lines · 2 more bars · 2 more lines · 10 table rows. Every series rendered.
The rule's own hint concedes it — it prescribes suppressWarnings: ['chart-config-missing'] "if the default rendering is intentional", which only makes sense if the default rendering exists.
The harm is not the noise. A warning that says a widget renders empty trains an author to add chartConfig blocks that do nothing — decoration that reads as effect, which is a shape hotcrm has already had to remove once. Six of these warnings appeared during the card and were retired with the measurement above rather than tolerated, but the next author will meet them fresh.
Either the message should describe what actually happens (presentation defaults are derived; chartConfig overrides them), or the rule should not fire on widgets whose bindings are derivable.
One more, adjacent, and load-bearing
@objectstack/service-analytics 17.3.0 carries a comment stating that a SQLite Field.datetime is an INTEGER epoch (#2034). Measured on 17.3.0 with better-sqlite3 13.0.3, it is TEXT:
sqlite> select typeof(submitted_at), submitted_at from clm_contract limit 1;
text|2026-05-19T00:00:00.000Z
sqlite> select avg(submitted_at) from clm_contract;
2025.9166666666667 ← SQLite's text→numeric coercion: the average YEAR
That annotation is the assumption under which an author would expect AVG() over a datetime to mean anything. Downstream of it, derived: { op: 'difference', of: [avg_a, avg_b] } does not error — it parses, runs, and returns -0.849999999999909, the difference of two average years, which is a clean plausible number that renders happily on a dashboard. That is the form that would have shipped had the app not asked what the number meant.
Filed here as context rather than as a third finding, because the remedy could be the comment, the storage, or a duration primitive, and that is a bigger question than this card. Say the word and I will split it out.
Boundaries
Filed from an app repo (
objectstack-ai/hotclm,@objectstack/*17.3.0,@objectstack/consoleas shipped, objectui at8f9d87a) while building its three §09 dashboards —objectstack-ai/hotclm#29, PRobjectstack-ai/hotclm#30. ⛔ Unassigned, ungraded; routing is triage's.Two findings, filed together because they are the same contract seam — what a dataset-bound widget's authored keys actually do — and because they pull in opposite directions: one drops authored metadata without a word, the other warns about a failure that never happens. Split them if triage prefers a card per defect.
Provenance. Both were measured in a browser by the card-10 implementation run. I reviewed the evidence and the reasoning; I did not re-drive the browser myself, and I say so rather than implying otherwise.
Dedup: searched
DatasetWidget/chart-config-missing/ zero-dimension widget. Nothing open or closed covers either. Related but not this:objectstack-ai/hotclmPR #25'srecord:detailsfindings, filed separately as #16695.Finding 1 — a zero-dimension widget is a KPI card whatever its declared
type, and the extra measures vanishDatasetWidget.tsx:423:So a widget declaring
type: 'bar'(or'table') that selects four measures and no dimension renders as a single KPI card showing one number. The other three are dropped — no error, no warning, no placeholder.Observed: an approval-routing tile selecting four route counts rendered
and nothing else. Three of four rungs silently absent from a card that looked finished and deliberate.
objectstack validateandobjectstack lintboth pass. It was caught only by reading the rendered DOM and asking why one number was there instead of four.Why this is worth a card and not a docs note. The failure mode is a dashboard that looks complete and is wrong — the reviewer's eye has nothing to catch, because a KPI card showing a real number is exactly what a finished tile looks like. It is the metadata-the-runtime-silently-drops class: the author declared
type: 'bar', the runtime decided otherwise, and nothing in between said so.A spec-side refusal (a
bar/tablewith zero dimensions is not authorable) or a lint rule would both close it. The app worked around it by splitting the tile into four, which is correct for the app and does nothing for the next author.Finding 2 —
chart-config-missingasserts a rendering failure that does not occur on dataset-bound widgetsThe rule's message reads:
On a dataset-bound widget that is not what happens. objectui's
buildChartSeries(rows, dimensions, values, fields)derives the bindings from the widget's own dimension/measure selection, and an authoredchartConfigis merged onto that derivation as presentation only (mergeAuthoredPresentation).Measured in Chromium with no
chartConfiganywhere in the app: 7 funnel trapezoids · 3 bars · 2 lines · 2 more bars · 2 more lines · 10 table rows. Every series rendered.The rule's own hint concedes it — it prescribes
suppressWarnings: ['chart-config-missing']"if the default rendering is intentional", which only makes sense if the default rendering exists.The harm is not the noise. A warning that says a widget renders empty trains an author to add
chartConfigblocks that do nothing — decoration that reads as effect, which is a shapehotcrmhas already had to remove once. Six of these warnings appeared during the card and were retired with the measurement above rather than tolerated, but the next author will meet them fresh.Either the message should describe what actually happens (presentation defaults are derived;
chartConfigoverrides them), or the rule should not fire on widgets whose bindings are derivable.One more, adjacent, and load-bearing
@objectstack/service-analytics17.3.0 carries a comment stating that a SQLiteField.datetimeis an INTEGER epoch (#2034). Measured on 17.3.0 withbetter-sqlite313.0.3, it is TEXT:That annotation is the assumption under which an author would expect
AVG()over a datetime to mean anything. Downstream of it,derived: { op: 'difference', of: [avg_a, avg_b] }does not error — it parses, runs, and returns-0.849999999999909, the difference of two average years, which is a clean plausible number that renders happily on a dashboard. That is the form that would have shipped had the app not asked what the number meant.Filed here as context rather than as a third finding, because the remedy could be the comment, the storage, or a duration primitive, and that is a bigger question than this card. Say the word and I will split it out.
Boundaries
better-sqlite3. Re-confirm against a current@objectstack/consoleand on a second dialect before acting — therecord:detailscard (record:details: the #13855sections[].groupreference form crashes the renderer, and the enumeratedfieldsform renders raw field keys instead of declared labels #16695) shows how much the render path can differ from what the schema implies.