Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/22161-s17-widget-binding-fix-lines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@objectstack/lint": patch
---

fix(lint): the 11 dashboard widget-binding rule ids print a `fix:` line of one remedy sentence, and `os explain RULE_ID` carries what those lines used to explain

Clause-②: no

- **Shorter fix lines.** Each finding of these 11 rule ids (all in `validate-widget-bindings.ts`) now carries a `hint` (the CLI's `fix:` line, the runtime issue's `hint`) of one remedy sentence of at most 200 characters on every variant the lint suite fires (the lengths here are the `hint` alone; the CLI prints it after `fix: `). The longest of each, before → after:
- dashboard filters: `dashboard-filter-field-unprovisioned` (561 → 177), `dashboard-filter-field-unknown` (403 → 148) and `dashboard-filter-field-not-included` (218 → 144);
- widget filters: `widget-filter-field-unknown` (322 → 133);
- refused `chartConfig` keys: `chart-field-unknown` (415 → 152);
- empty selections: `chart-measures-missing` (369 → 144), `widget-measures-missing` (330 → 154) and `chart-dimensions-missing` (291 → 161);
- legacy analytics keys: `widget-legacy-analytics-shape` (338 → 113) and `widget-legacy-analytics-unrenderable` (320 → 105);
- tables: `table-count-only` (311 → 136).
- **Lists are no longer echoed.** `dashboard-filter-field-unknown` and `widget-filter-field-unknown` no longer append the object's whole field list, and `dashboard-filter-field-not-included` no longer lists the dataset's declared `include` paths: the verdict's (or, on a bare dashboard-filter field, the fix line's own) "Did you mean" names the nearest field. The three empty-selection ids still name what the dataset declares, because no "Did you mean" can: at most three names, then `…`, or "it declares no measures yet".
- **One line, one remedy.** The `suppressWarnings` clause leaves every warning's fix line, and with it: how DatasetWidget reads a dataset-bound widget; that a dotted filter target must traverse relationship fields and have its prefix in `include`, and that `a.b` includes `a`; how to vouch for or opt out of an unprovisioned anchor (`dashboard-filter-field-unprovisioned` now prints the shared one-line anchor fix the other anchor ids print); what replaces each refused `chartConfig` key, and the `os migrate meta --from 17` route; which widget families need no dimensions; and why a flat record listing is not an analytics dataset (ADR-0017).
- **`os explain RULE_ID` carries the rest.** What the fix lines stopped saying is each id's `RULE_EXPLANATIONS` entry in `@objectstack/lint`, which `os explain RULE_ID` prints. Two ids gain an entry, so their `rule:` line now carries the `os explain` pointer: `table-count-only` and `widget-legacy-analytics-shape`. Seven existing entries gain text.
- **At the runtime publish gate** (Studio, REST `PUT /api/v1/meta`, and the MCP and AI authors that write through that door), all 11 fire on a `dashboard` write; the `hint` FROM → TO, measured at the door:
- 422 `issues[].hint`: `widget-legacy-analytics-unrenderable` 320 → 105; `dashboard-filter-field-unknown` 196 / 195 / 290 → 148 / 147 / 137 (an inherited bare field, a re-targeted bare field, a dotted path); `dashboard-filter-field-not-included` 213 → 139; `widget-filter-field-unknown` 220 → 126;
- 2xx `advisories[].hint`: `widget-legacy-analytics-shape` 338 → 112; `dashboard-filter-field-unprovisioned` 550 → 173; `chart-field-unknown` 413 / 294 / 324 → 150 / 124 / 127 (an x-axis, a y-axis, a series); `chart-measures-missing` 240 → 96; `widget-measures-missing` 310 → 134; `chart-dimensions-missing` 297 → 156; `table-count-only` 311 → 136.

Studio's save-advisory toast prints an advisory's `hint` after its message, so the seven advisories shorten there; Studio renders no 422 issue's `hint`. No Studio, REST or MCP reader in this repository or in objectui matches on `hint` text; a tool that did should match on `rule` and `path` instead.
- **Unchanged:** every `message` (the verdict), rule id, severity and `path`, and what each rule accepts or refuses.
77 changes: 69 additions & 8 deletions packages/lint/src/rule-explanations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,28 @@ const WIDGET_LEGACY_ANALYTICS_UNRENDERABLE_EXPLANATION: RuleExplanation = {
],
};

const WIDGET_LEGACY_ANALYTICS_SHAPE_EXPLANATION: RuleExplanation = {
rule: 'widget-legacy-analytics-shape',
covers: 'why the legacy analytics keys are ignored',
paragraphs: [
'The ADR-0021 single-form cutover removed the inline analytics shape — `categoryField`, ' +
'`valueField`, `xAxisField`, `yAxisFields`, `aggregate`, `aggregation`, `rowField` and ' +
'`columnField`. Dataset-bound widgets render through DatasetWidget, which reads only the ' +
'semantic dataset: pivot rows and columns come from `dimensions`, cell values from `values`. ' +
'These inline keys are a no-op.',
'This id is the warning case: the widget also has a data source (`dataset`, `object`, or inline ' +
'`data`, top-level or under `options`), so it still renders and the legacy keys are only ignored ' +
'noise. With no data source the widget renders nothing, and that is the error ' +
'`widget-legacy-analytics-unrenderable`.',
'On the schema-parsed paths (`compile`, `validate`) the strict widget schema refuses these keys as ' +
'a parse error before this rule runs; the rule is the steer on the raw-config paths (`lint`, ' +
'`doctor`).',
'To fix it, delete the legacy keys and select fields by name through the widget\'s dataset. ' +
'Suppress with `suppressWarnings: [\'widget-legacy-analytics-shape\']` if the keys are ' +
'intentional.',
],
};

const DASHBOARD_FILTER_FIELD_UNKNOWN_EXPLANATION: RuleExplanation = {
rule: 'dashboard-filter-field-unknown',
covers: 'how a dashboard filter reaches every widget',
Expand All @@ -348,6 +370,12 @@ const DASHBOARD_FILTER_FIELD_UNKNOWN_EXPLANATION: RuleExplanation = {
'a typo to correct, an inherited default is one the widget may opt out of. Not reported: a ' +
'widget that opts out, an object this stack does not define, and an object with no readable ' +
'field map (an ADR-0015 external object, an introspected datasource).',
'A re-target may be a field of the dataset\'s object or a `relationship[.relationship].field` ' +
'path. Every hop of such a path must be a relationship field (lookup, master_detail, user or ' +
'tree; ADR-0021 derives every join from the object graph, so there is no ON clause to write), ' +
'and its prefix must be declared in the dataset\'s `include` ' +
'(`dashboard-filter-field-not-included` otherwise). The verdict\'s "Did you mean" names the ' +
'nearest field of the object the miss landed on.',
],
};

Expand Down Expand Up @@ -384,8 +412,8 @@ const UNPROVISIONED_ANCHOR_CAUSE =
* The three ways out of an unprovisioned anchor, which the one-line `fix:` of
* the converted anchor ids (`unprovisionedAnchorFix()`) names without the how:
* `sort-field-unprovisioned`, `searchable-field-unprovisioned`,
* `react-chart-field-unprovisioned` and the hook, action and flow-node
* write-anchor ids write it out.
* `react-chart-field-unprovisioned`, `dashboard-filter-field-unprovisioned`
* and the hook, action and flow-node write-anchor ids write it out.
*/
const UNPROVISIONED_ANCHOR_REMEDY =
'To fix it, check the remote table. If it really carries the column, declare it in the object\'s ' +
Expand All @@ -409,6 +437,7 @@ const DASHBOARD_FILTER_FIELD_UNPROVISIONED_EXPLANATION: RuleExplanation = {
'`filterBindings: { NAME: false }`, or suppress with ' +
'`suppressWarnings: [\'dashboard-filter-field-unprovisioned\']` if the remote schema resolves ' +
'the column some other way.',
UNPROVISIONED_ANCHOR_REMEDY,
],
};

Expand All @@ -424,6 +453,11 @@ const WIDGET_FILTER_FIELD_UNKNOWN_EXPLANATION: RuleExplanation = {
'columns — and the verdict names the hop that failed. This rule judges the KEYS; ' +
'`filter-token-unknown` judges the values in the same subtree. Not reported: an object this ' +
'stack does not define, or one with no readable field map.',
'To fix it, filter on a field of the dataset\'s object, or on a `relationship[.relationship].field` ' +
'path: every hop must be a relationship field (lookup, master_detail, user or tree; ADR-0021 ' +
'derives every join from the object graph), and the prefix must be declared in the dataset\'s ' +
'`include` (`widget-filter-field-not-included` otherwise). The verdict\'s "Did you mean" names ' +
'the nearest field of the object the miss landed on.',
],
};

Expand Down Expand Up @@ -476,6 +510,11 @@ const CHART_FIELD_UNKNOWN_EXPLANATION: RuleExplanation = {
'refused on a dataset-bound widget (ADR-0021). Post-cutover data is keyed by the dataset\'s ' +
'measure NAME, not the base column. Suppress with `suppressWarnings: [\'chart-field-unknown\']` ' +
'if the inert key is intentional.',
'What replaces each key: the x-axis is the widget\'s first `dimensions` entry; the y-axis ' +
'bindings come from `values`, one entry per mark; series membership follows `values` (and a ' +
'second `dimensions` entry to split). A measure named by a refused key that the dataset declares ' +
'but the widget does not select is plotted by adding it to `values`. ' +
'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.',
],
};

Expand All @@ -490,7 +529,8 @@ const CHART_MEASURES_MISSING_EXPLANATION: RuleExplanation = {
'tabular widget. A chart with no measures is reported once, here, even when it also selects no ' +
'dimensions: the placeholder returns before the renderer ever tests for dimensions. A chart ' +
'family still needs a dimension to plot against (`chart-dimensions-missing`).',
'To fix it, select at least one measure of the dataset by name in `values`. Suppress with ' +
'To fix it, select at least one measure of the dataset by name in `values` (the `fix:` line ' +
'names up to three the dataset declares). Suppress with ' +
'`suppressWarnings: [\'chart-measures-missing\']` while the widget is still being authored.',
],
};
Expand All @@ -505,8 +545,9 @@ const WIDGET_MEASURES_MISSING_EXPLANATION: RuleExplanation = {
'`kpi`, `gauge`, `solid-gauge`, `bullet`) loses the one KPI number the tile exists to show; a ' +
'tabular one (`table`, `pivot`) loses its grid.',
'Such a widget needs no `dimensions`, so measures are the whole fix: select at least one measure ' +
'of the dataset by name in `values`. Suppress with `suppressWarnings: [\'widget-measures-missing\']` ' +
'while the widget is still being authored.',
'of the dataset by name in `values` (the `fix:` line names up to three the dataset declares). ' +
'Suppress with `suppressWarnings: [\'widget-measures-missing\']` while the widget is still being ' +
'authored.',
],
};

Expand All @@ -518,9 +559,27 @@ const CHART_DIMENSIONS_MISSING_EXPLANATION: RuleExplanation = {
'a chart-family widget that selects measures but no dimensions draws a single KPI number instead ' +
'of its declared chart. The number is real, so nothing looks broken — the declared chart family ' +
'is simply gone.',
'To fix it, plot the chart against a dataset dimension (`dimensions`), or, if a single value IS ' +
'what the tile should show, declare it as a `metric` or `kpi` widget so the type matches what ' +
'renders. Suppress with `suppressWarnings: [\'chart-dimensions-missing\']`.',
'To fix it, plot the chart against a dataset dimension (`dimensions`; the `fix:` line names up to ' +
'three the dataset declares), or, if a single value IS what the tile should show, declare it as ' +
'a `metric` or `kpi` widget so the type matches what renders. Suppress with ' +
'`suppressWarnings: [\'chart-dimensions-missing\']`.',
],
};

const TABLE_COUNT_ONLY_EXPLANATION: RuleExplanation = {
rule: 'table-count-only',
covers: 'why a count-only table renders one summary row',
paragraphs: [
'A `table` or `pivot` widget bound to a dataset runs an analytics query: rows are the groups of ' +
'its `dimensions`, cells the aggregates of its `values`. With no dimensions and only count ' +
'measures (a derived measure is never counted as one), there is nothing to group by, so the ' +
'widget renders a single summary row — not a per-record list.',
'A flat record listing is not an analytics dataset. Model it as an object-bound ListView ' +
'(ADR-0017) surfaced through app navigation, and use a `metric` widget on the dashboard if only ' +
'the count is needed. If a single-row table is intentional, add an explicit dimension or ' +
'suppress with `suppressWarnings: [\'table-count-only\']`.',
'Not reported: a widget that selects a dimension, selects no measures (`widget-measures-missing`), ' +
'or names a measure the dataset does not declare (`widget-measure-unknown`).',
],
};

Expand Down Expand Up @@ -4009,6 +4068,7 @@ export const RULE_EXPLANATIONS: Readonly<Record<string, RuleExplanation>> = Obje
[VIEW_ROW_COLOR_UNRESOLVABLE_VALUE_EXPLANATION.rule]: VIEW_ROW_COLOR_UNRESOLVABLE_VALUE_EXPLANATION,
[WEBHOOK_WITHOUT_TRIGGERS_EXPLANATION.rule]: WEBHOOK_WITHOUT_TRIGGERS_EXPLANATION,
[WIDGET_LEGACY_ANALYTICS_UNRENDERABLE_EXPLANATION.rule]: WIDGET_LEGACY_ANALYTICS_UNRENDERABLE_EXPLANATION,
[WIDGET_LEGACY_ANALYTICS_SHAPE_EXPLANATION.rule]: WIDGET_LEGACY_ANALYTICS_SHAPE_EXPLANATION,
[DASHBOARD_FILTER_FIELD_UNKNOWN_EXPLANATION.rule]: DASHBOARD_FILTER_FIELD_UNKNOWN_EXPLANATION,
[DASHBOARD_FILTER_FIELD_NOT_INCLUDED_EXPLANATION.rule]: DASHBOARD_FILTER_FIELD_NOT_INCLUDED_EXPLANATION,
[DASHBOARD_FILTER_FIELD_UNPROVISIONED_EXPLANATION.rule]: DASHBOARD_FILTER_FIELD_UNPROVISIONED_EXPLANATION,
Expand All @@ -4019,6 +4079,7 @@ export const RULE_EXPLANATIONS: Readonly<Record<string, RuleExplanation>> = Obje
[CHART_MEASURES_MISSING_EXPLANATION.rule]: CHART_MEASURES_MISSING_EXPLANATION,
[WIDGET_MEASURES_MISSING_EXPLANATION.rule]: WIDGET_MEASURES_MISSING_EXPLANATION,
[CHART_DIMENSIONS_MISSING_EXPLANATION.rule]: CHART_DIMENSIONS_MISSING_EXPLANATION,
[TABLE_COUNT_ONLY_EXPLANATION.rule]: TABLE_COUNT_ONLY_EXPLANATION,
[DATASET_INCLUDE_UNKNOWN_EXPLANATION.rule]: DATASET_INCLUDE_UNKNOWN_EXPLANATION,
[DATASET_FIELD_UNKNOWN_EXPLANATION.rule]: DATASET_FIELD_UNKNOWN_EXPLANATION,
[DATASET_FIELD_NOT_INCLUDED_EXPLANATION.rule]: DATASET_FIELD_NOT_INCLUDED_EXPLANATION,
Expand Down
Loading
Loading