@object-ui/core 17.6.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +6610 -0
  2. package/README.md +101 -14
  3. package/dist/actions/ActionEngine.d.ts +4 -2
  4. package/dist/actions/ActionEngine.js +13 -8
  5. package/dist/actions/ActionRunner.d.ts +288 -27
  6. package/dist/actions/ActionRunner.js +443 -63
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/actionKeys.d.ts +9 -7
  9. package/dist/actions/actionKeys.js +57 -14
  10. package/dist/actions/actionResponse.js +2 -2
  11. package/dist/actions/index.d.ts +1 -0
  12. package/dist/actions/index.js +1 -0
  13. package/dist/actions/recordDelete.d.ts +118 -0
  14. package/dist/actions/recordDelete.js +112 -0
  15. package/dist/adapters/ValueDataSource.d.ts +6 -1
  16. package/dist/adapters/ValueDataSource.js +1090 -98
  17. package/dist/adapters/batchTransaction.d.ts +1 -1
  18. package/dist/adapters/batchTransaction.js +1 -1
  19. package/dist/builder/schema-builder.d.ts +38 -81
  20. package/dist/builder/schema-builder.js +33 -152
  21. package/dist/data-scope/ViewDataProvider.d.ts +1 -1
  22. package/dist/data-scope/ViewDataProvider.js +21 -1
  23. package/dist/data-scope/element-data-source.d.ts +261 -7
  24. package/dist/data-scope/element-data-source.js +342 -52
  25. package/dist/data-scope/index.d.ts +9 -4
  26. package/dist/data-scope/index.js +9 -4
  27. package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
  28. package/dist/evaluator/ExpressionEvaluator.js +184 -12
  29. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  30. package/dist/evaluator/FormulaFunctions.js +166 -27
  31. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  32. package/dist/evaluator/declaredPredicate.js +117 -16
  33. package/dist/evaluator/fieldRules.d.ts +88 -4
  34. package/dist/evaluator/fieldRules.js +203 -31
  35. package/dist/evaluator/index.d.ts +1 -0
  36. package/dist/evaluator/index.js +1 -0
  37. package/dist/evaluator/listConditional.d.ts +103 -34
  38. package/dist/evaluator/listConditional.js +127 -50
  39. package/dist/evaluator/optionRules.d.ts +77 -6
  40. package/dist/evaluator/optionRules.js +87 -10
  41. package/dist/evaluator/predicateInput.d.ts +15 -4
  42. package/dist/evaluator/predicateInput.js +35 -2
  43. package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
  44. package/dist/evaluator/rowPredicateCanon.js +65 -115
  45. package/dist/evaluator/subjectPermissions.d.ts +76 -0
  46. package/dist/evaluator/subjectPermissions.js +44 -0
  47. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  48. package/dist/evaluator/unevaluablePredicate.js +70 -0
  49. package/dist/index.d.ts +14 -0
  50. package/dist/index.js +80 -2
  51. package/dist/protocols/index.d.ts +0 -1
  52. package/dist/protocols/index.js +9 -1
  53. package/dist/registry/Registry.d.ts +358 -31
  54. package/dist/registry/Registry.js +263 -11
  55. package/dist/registry/WidgetRegistry.js +7 -3
  56. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  57. package/dist/registry/html-tier-intrinsics.js +110 -0
  58. package/dist/registry/public-blocks.d.ts +8 -0
  59. package/dist/registry/public-blocks.js +57 -0
  60. package/dist/theme/ThemeEngine.js +1 -1
  61. package/dist/utils/chart-category-key.d.ts +82 -0
  62. package/dist/utils/chart-category-key.js +84 -0
  63. package/dist/utils/chart-measure-key.d.ts +64 -0
  64. package/dist/utils/chart-measure-key.js +65 -0
  65. package/dist/utils/chart-presentation.d.ts +95 -112
  66. package/dist/utils/chart-presentation.js +169 -104
  67. package/dist/utils/chart-series.d.ts +131 -5
  68. package/dist/utils/chart-series.js +98 -16
  69. package/dist/utils/column-sortability.d.ts +157 -0
  70. package/dist/utils/column-sortability.js +217 -0
  71. package/dist/utils/dashboard-filters.d.ts +38 -6
  72. package/dist/utils/dashboard-filters.js +107 -61
  73. package/dist/utils/dataset-format.d.ts +211 -30
  74. package/dist/utils/dataset-format.js +389 -46
  75. package/dist/utils/date-display.d.ts +354 -0
  76. package/dist/utils/date-display.js +463 -0
  77. package/dist/utils/date-macros.js +7 -0
  78. package/dist/utils/drill-down.d.ts +78 -0
  79. package/dist/utils/drill-down.js +93 -0
  80. package/dist/utils/emptiness.d.ts +96 -0
  81. package/dist/utils/emptiness.js +101 -0
  82. package/dist/utils/expand-fields.d.ts +76 -6
  83. package/dist/utils/expand-fields.js +76 -6
  84. package/dist/utils/extract-records.d.ts +65 -5
  85. package/dist/utils/extract-records.js +65 -8
  86. package/dist/utils/filter-converter.d.ts +152 -4
  87. package/dist/utils/filter-converter.js +1092 -22
  88. package/dist/utils/filter-tokens.d.ts +49 -5
  89. package/dist/utils/filter-tokens.js +105 -33
  90. package/dist/utils/grouping-fields.d.ts +61 -0
  91. package/dist/utils/grouping-fields.js +84 -0
  92. package/dist/utils/managedBy.d.ts +3 -3
  93. package/dist/utils/managedBy.js +5 -5
  94. package/dist/utils/native-date-value.d.ts +48 -0
  95. package/dist/utils/native-date-value.js +167 -0
  96. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  97. package/dist/utils/non-grid-row-ceiling.js +141 -0
  98. package/dist/utils/normalize-list-view.d.ts +121 -1
  99. package/dist/utils/normalize-list-view.js +186 -6
  100. package/dist/utils/number-display.d.ts +63 -14
  101. package/dist/utils/number-display.js +34 -12
  102. package/dist/utils/object-aggregate-query.d.ts +113 -0
  103. package/dist/utils/object-aggregate-query.js +50 -0
  104. package/dist/utils/parent-scope.d.ts +123 -0
  105. package/dist/utils/parent-scope.js +140 -0
  106. package/dist/utils/predicate-fields.d.ts +45 -12
  107. package/dist/utils/predicate-fields.js +137 -12
  108. package/dist/utils/record-color.d.ts +75 -0
  109. package/dist/utils/record-color.js +65 -0
  110. package/dist/utils/record-source.d.ts +298 -0
  111. package/dist/utils/record-source.js +331 -0
  112. package/dist/utils/record-title.d.ts +148 -8
  113. package/dist/utils/record-title.js +220 -26
  114. package/dist/utils/reference-keys.d.ts +21 -22
  115. package/dist/utils/reference-keys.js +532 -29
  116. package/dist/utils/sort-query.d.ts +115 -15
  117. package/dist/utils/sort-query.js +240 -13
  118. package/dist/utils/text-comparand.d.ts +92 -0
  119. package/dist/utils/text-comparand.js +99 -0
  120. package/dist/utils/without-denied-fields.d.ts +49 -0
  121. package/dist/utils/without-denied-fields.js +52 -0
  122. package/dist/validation/index.d.ts +8 -4
  123. package/dist/validation/index.js +8 -4
  124. package/dist/validation/schema-validator.d.ts +1 -1
  125. package/dist/validation/schema-validator.js +200 -82
  126. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  127. package/dist/validation/validators/object-validation-engine.js +14 -3
  128. package/package.json +8 -9
  129. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  130. package/dist/data-scope/DataScopeManager.js +0 -229
  131. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  132. package/dist/protocols/ResponsiveProtocol.js +0 -158
  133. package/dist/validation/validation-engine.d.ts +0 -88
  134. package/dist/validation/validation-engine.js +0 -434
@@ -8,8 +8,11 @@
8
8
  const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
9
9
  /**
10
10
  * An i18n label is a plain string or a `{ en, zh-CN, … }` record; charts render
11
- * a string. Same pick `normalizeChartSchema` makes, so a label reads the same
12
- * on both paths.
11
+ * a string.
12
+ *
13
+ * ⚠️ Its one caller is {@link seriesPresentation}'s `label`. An axis `title`
14
+ * read this too until `061f5e829` moved it off the pick, and the axis lowering
15
+ * itself is gone since objectui#11372 (file header).
13
16
  *
14
17
  * **First-string-wins, deliberately, and deliberately NOT locale-aware** — this
15
18
  * package is React-free and holds no i18n provider, so it cannot know which
@@ -27,17 +30,83 @@ function labelText(v) {
27
30
  }
28
31
  return undefined;
29
32
  }
33
+ /**
34
+ * An authored `I18nLabel` that travels to the renderer **unresolved** — the
35
+ * chart's own `title` / `subtitle` / `description` (objectui#9038).
36
+ *
37
+ * ## Why this does not resolve, and why that is not {@link labelText}'s answer
38
+ *
39
+ * These three keys are lowered onto a chart SCHEMA slot that already declares
40
+ * the union: `normalizeChartSchema` (`@object-ui/plugin-charts`) resolves
41
+ * `title`/`subtitle`/`description` through `pickLocalized` with the viewer's
42
+ * language, which `ChartRenderer` reads from `useObjectTranslation` and hands
43
+ * down (objectui#8943), and `ObjectChart` reads `schema.title` through the same
44
+ * resolver for its drill heading. So the value's one resolution point is
45
+ * already downstream of here, holding the one thing this package does not have
46
+ * — the viewer. Forwarding is what lets that resolver see the value at all.
47
+ *
48
+ * Resolving here is not merely unnecessary, it is unavailable: `pickLocalized`
49
+ * lives in `@object-ui/i18n`, which DEPENDS on this package. Declaring the
50
+ * reverse edge makes the build graph cyclic (measured: `turbo run build
51
+ * --filter=@object-ui/core` refuses with `Cyclic dependency detected`), and
52
+ * that package's entry point is a React provider plus hooks — the layering
53
+ * this file's header states, arrived at from the other side.
54
+ *
55
+ * ⛔ Do NOT "fix" this by reusing {@link labelText}. The two answer different
56
+ * questions and only one of them is a choice:
57
+ *
58
+ * - `labelText` is a locale-unaware PICK for a slot whose consumer takes a
59
+ * plain string. It is wrong for a zh console and is ledgered as such
60
+ * (objectui#4020) because a caller that CAN resolve the language overrides
61
+ * the merged label — the mitigation depends on the value still existing.
62
+ * - The guard this replaced (`typeof v === 'string' && v ? v : undefined`)
63
+ * offered no such route: it ERASED the map arm, the `if (title)` guard then
64
+ * skipped the assignment, and the chart drew **no heading at all**, in every
65
+ * language, with no diagnostic. Nothing downstream could override a key that
66
+ * never arrived.
67
+ *
68
+ * ## The admission test, and what it deliberately does not decide
69
+ *
70
+ * A plain string, or a record carrying at least one usable string entry. That
71
+ * is the same "is there anything here" question the `if (title)` truthiness
72
+ * guard always asked, extended to the map arm — it does NOT decide which limb
73
+ * wins, which stays `pickLocalized`'s alone. Everything else (a number, a
74
+ * boolean, an array, `''`, `{}`, a record with no string value) is refused
75
+ * exactly as before, so no key reaches the caller that cannot render, and the
76
+ * `@returns` contract below — only keys that resolved — still holds.
77
+ *
78
+ * The map is forwarded VERBATIM: `InlineLocaleMapSchema` is
79
+ * `z.record(<tag>, z.string())` and enforcing that belongs at the parse, not in
80
+ * a renderer-side coercion (AGENTS.md #0.1). A non-string entry falls through
81
+ * `pickLocalized`'s limbs exactly as an absent one does, which is why admitting
82
+ * the record on the strength of one usable entry cannot paint `[object
83
+ * Object]`.
84
+ */
85
+ function forwardedI18nLabel(v) {
86
+ if (typeof v === 'string')
87
+ return v || undefined;
88
+ if (isRecord(v) && Object.values(v).some((x) => typeof x === 'string' && x)) {
89
+ return v;
90
+ }
91
+ return undefined;
92
+ }
30
93
  /**
31
94
  * One authored `ChartSeries`, minus its `name` — i.e. everything about it that
32
95
  * is presentation rather than membership.
33
96
  *
34
- * `type` is narrowed to the three families that COMPOSE on one cartesian plot,
35
- * because this array reaches the renderer already speaking the internal shape
36
- * (`ChartRenderer` forwards a `dataKey`-shaped array untouched, so
37
- * `normalizeChartSchema`'s own identical narrowing never sees it). Without the
38
- * narrowing a `type: 'pie'` would not merely be inert — it would count as a
39
- * family disagreement in `effectiveChartFamily`, flip the whole chart into a
40
- * combo, and then draw that series as a bar anyway.
97
+ * `type` is narrowed to the three families that COMPOSE on one cartesian plot.
98
+ * Without the narrowing a `type: 'pie'` would not merely be inert — it would
99
+ * count as a family disagreement in `effectiveChartFamily`, flip the whole
100
+ * chart into a combo, and then draw that series as a bar anyway.
101
+ *
102
+ * `ChartRenderer` now narrows the same way for every series it receives,
103
+ * `dataKey`-shaped or not (`01c27c431` fixed the fast path that used to
104
+ * forward a `dataKey`-shaped array straight through, so `normalizeChartSchema`'s
105
+ * own identical narrowing never saw it) — so this module is no longer that
106
+ * narrowing's only line of defense. It stays anyway:
107
+ * `AuthoredSeriesPresentation.chartType` is a typed, narrower field
108
+ * (`'bar' | 'line' | 'area'`) this module's own callers read directly, and the
109
+ * double narrowing downstream is idempotent.
41
110
  */
42
111
  export function seriesPresentation(raw) {
43
112
  const out = {};
@@ -61,54 +130,10 @@ export function seriesPresentation(raw) {
61
130
  out.opacity = raw.opacity;
62
131
  return out;
63
132
  }
64
- /**
65
- * One authored `ChartAxis`, minus its `field` — the axis's presentation.
66
- *
67
- * `field` is the one DATA key on an axis (it names the plotted column), and
68
- * dropping it here is what keeps membership with the dataset **structurally**
69
- * rather than by a guard: `normalizeChartSchema` synthesises series out of
70
- * `yAxis[].field` when a chart declares no series, so a forwarded `field`
71
- * would be a live membership channel on an empty selection. With it gone the
72
- * axis carries scale and chrome only, and the count of entries — which is what
73
- * turns on the secondary axis (`yAxes.length > 1`) — survives, including for
74
- * an entry that declares nothing but its own existence.
75
- *
76
- * Keys the renderer does not read on a given axis are dropped by
77
- * `normalizeChartSchema`, the ONE normalization layer (#2880 S1): today it
78
- * keeps `format`/`title`/`showGridLines` on the x-axis and the full set on the
79
- * y-axes. That narrowing is deliberately NOT mirrored here — a second copy
80
- * would drift from the renderer's real capability the moment it grew.
81
- */
82
- export function axisPresentation(raw) {
83
- const out = {};
84
- if (!isRecord(raw))
85
- return out;
86
- const title = labelText(raw.title);
87
- if (title)
88
- out.title = title;
89
- if (typeof raw.format === 'string' && raw.format)
90
- out.format = raw.format;
91
- if (typeof raw.min === 'number' && Number.isFinite(raw.min))
92
- out.min = raw.min;
93
- if (typeof raw.max === 'number' && Number.isFinite(raw.max))
94
- out.max = raw.max;
95
- if (typeof raw.stepSize === 'number' && Number.isFinite(raw.stepSize) && raw.stepSize > 0) {
96
- out.stepSize = raw.stepSize;
97
- }
98
- if (typeof raw.showGridLines === 'boolean')
99
- out.showGridLines = raw.showGridLines;
100
- if (raw.position === 'left' || raw.position === 'right' || raw.position === 'top' || raw.position === 'bottom') {
101
- out.position = raw.position;
102
- }
103
- if (typeof raw.logarithmic === 'boolean')
104
- out.logarithmic = raw.logarithmic;
105
- return out;
106
- }
107
133
  /**
108
134
  * Merge an authored `series[]` array's PRESENTATION onto the bindings the
109
- * dataset selection derived — the series half of #4229's split, and the whole
110
- * of it for a surface whose axes are names rather than objects (see the file
111
- * header).
135
+ * dataset selection derived — the series half of #4229's split, and the only
136
+ * half this module lowers (see the file header).
112
137
  *
113
138
  * The match rule is **by name/key**: an authored `series[].name` is paired with
114
139
  * the derived binding whose `dataKey` it equals, and the pairing decides
@@ -149,43 +174,10 @@ export function mergeAuthoredSeries(derived, authoredSeries) {
149
174
  });
150
175
  }
151
176
  /**
152
- * Merge an authored chart config's PRESENTATION onto the series and axes the
153
- * dataset selection derived — the one place that happens for a surface whose
154
- * axes are spec `ChartAxis` OBJECTS (#4229).
155
- *
156
- * The series half is {@link mergeAuthoredSeries}; see it for the match rule and
157
- * for why membership is safe. The axes half reads `xAxis` / `yAxis` through
158
- * {@link axisPresentation}, which is why a surface spelling those as bare NAME
159
- * strings must call `mergeAuthoredSeries` directly instead (file header).
160
- *
161
- * @param derived the bindings {@link buildChartSeries} produced from the selection
162
- * @param raw the authored chart config (anything, incl. absent)
163
- * @returns the merged series, plus the presentation-only axes to spread onto
164
- * the chart schema (absent when the author declared none)
165
- */
166
- export function mergeAuthoredPresentation(derived, raw) {
167
- const config = isRecord(raw) ? raw : {};
168
- const series = mergeAuthoredSeries(derived, config.series);
169
- const axes = {};
170
- const xAxis = axisPresentation(config.xAxis);
171
- if (Object.keys(xAxis).length > 0)
172
- axes.xAxis = xAxis;
173
- // The COUNT of y-axis entries is itself presentation — it is what declares a
174
- // secondary axis — so every declared entry keeps its slot even when it
175
- // carries nothing but `field` (which is data and does not travel).
176
- const yAxisRaw = Array.isArray(config.yAxis)
177
- ? config.yAxis
178
- : config.yAxis !== undefined
179
- ? [config.yAxis]
180
- : [];
181
- if (yAxisRaw.length > 0)
182
- axes.yAxis = yAxisRaw.map(axisPresentation);
183
- return { series, axes };
184
- }
185
- /**
186
- * Lower an authored chart config's CHROME (spec `ChartConfigSchema` /
187
- * `ReportChartSchema` — the same keys on both) onto the chart schema a
188
- * dataset-bound surface hands the renderer.
177
+ * Lower an authored chart config's CHROME (spec
178
+ * `DashboardWidgetChartConfigSchema` / `ReportChartSchema`, both extensions of
179
+ * `ChartConfigSchema` that keep its chrome keys unchanged) onto the chart
180
+ * schema a dataset-bound surface hands the renderer.
189
181
  *
190
182
  * ## Why this is a whitelist and not a spread
191
183
  *
@@ -209,20 +201,48 @@ export function mergeAuthoredPresentation(derived, raw) {
209
201
  * exists to remove.
210
202
  * 2. **It does not fight the dataset derivation.** `type` stays out: the
211
203
  * calling surface's own type already picks the chart family, which is the
212
- * dataset path's chart-family channel.
204
+ * dataset path's chart-family channel. The spec agrees on the dashboard
205
+ * carrier: since 17.5.0 `DashboardWidgetChartConfigSchema` refuses
206
+ * `chartConfig.type` at parse, with `xAxis` / `yAxis` / `series`, none of
207
+ * which this whitelist ever lowered (objectui#11315).
213
208
  *
214
- * `aria` is the one declared key with **no reader at all**: `AdvancedChartImpl`
215
- * has no `aria` prop, and `SchemaRenderer`'s ARIA injection reads the FLAT
216
- * `ariaLabel`/`ariaDescribedBy`/`role`, never a nested `aria` object. It is
217
- * therefore left unforwarded on purpose (criterion 1) rather than papered over
218
- * with a caller-side flattening that would also collide with the accessible
219
- * name `description` already sets — reported back to objectstack#5175's
220
- * narrowing half instead.
209
+ * `aria` is not lowered either. No renderer reads it: `AdvancedChartImpl` has
210
+ * no `aria` prop, and `SchemaRenderer`'s ARIA injection reads the FLAT
211
+ * `ariaLabel`/`ariaDescribedBy`/`role`, never a nested `aria` object (criterion
212
+ * 1). And it is no longer declared: spec 17.5.0 retired `ChartConfig.aria` as
213
+ * a `retiredKey` tombstone (objectstack#17751, objectui#4044), so an authored
214
+ * one is refused at parse. The accessible name a chart applies is
215
+ * `description`, above.
221
216
  *
222
217
  * `title` IS emitted here. A caller that paints the title itself (the report
223
218
  * renderer's own `h3` above the chart) must drop it from the result, or the
224
219
  * chart draws a second one.
225
220
  *
221
+ * ## `title` / `subtitle` / `description` are `I18nLabel`, and travel as such
222
+ *
223
+ * `ChartConfigSchema` types all three as the spec's `I18nLabel` union — a plain
224
+ * string OR an inline locale map — so all three are lowered by
225
+ * {@link forwardedI18nLabel}, which hands the authored value on untouched for
226
+ * the renderer to resolve against the viewer's language. Read that helper for
227
+ * why resolving cannot happen in this package.
228
+ *
229
+ * ⚠️ LEDGERED, by name, as still unresolved after objectui#9038 — not in
230
+ * that card's scope and not reached by the change above:
231
+ *
232
+ * - **A series `label`** still goes through {@link labelText}'s
233
+ * first-string-wins pick, the design objectui#4020 ledgered and a caller
234
+ * can override. Not reopened here.
235
+ *
236
+ * ✓ CLEARED, objectui#9150 — **the report renderer's own heading.**
237
+ * `DatasetReportChart` (`@object-ui/plugin-report`) still drops `title` from
238
+ * this result and still paints its own `h3`, but that read site no longer
239
+ * narrows `chart.title` to a plain string: it resolves the union through
240
+ * `pickLocalized` against the viewer's language, like every other `I18nLabel`
241
+ * on that surface. Kept here rather than deleted because the ledger entry is
242
+ * what the fixing card was dispatched from, and because the FIRST half of it —
243
+ * that a caller painting the title itself must drop `title` from this result —
244
+ * is a live constraint on this whitelist, stated above.
245
+ *
226
246
  * @param raw the authored chart config (anything, incl. absent)
227
247
  * @param fieldCategoryColors per-category colours resolved from the category
228
248
  * dimension's own select/lookup option colours, merged UNDER an explicit
@@ -233,18 +253,20 @@ export function mergeAuthoredPresentation(derived, raw) {
233
253
  export function chartConfigPresentation(raw, fieldCategoryColors) {
234
254
  const config = isRecord(raw) ? raw : {};
235
255
  const out = {};
236
- const text = (v) => (typeof v === 'string' && v ? v : undefined);
237
256
  if (typeof config.showLegend === 'boolean')
238
257
  out.showLegend = config.showLegend;
239
258
  if (typeof config.showDataLabels === 'boolean')
240
259
  out.showDataLabels = config.showDataLabels;
241
- const title = text(config.title);
260
+ // The three `I18nLabel` slots, carried through unresolved — see
261
+ // {@link forwardedI18nLabel} for why this package forwards rather than picks,
262
+ // and for what is still ledgered as unresolved on purpose.
263
+ const title = forwardedI18nLabel(config.title);
242
264
  if (title)
243
265
  out.title = title;
244
- const subtitle = text(config.subtitle);
266
+ const subtitle = forwardedI18nLabel(config.subtitle);
245
267
  if (subtitle)
246
268
  out.subtitle = subtitle;
247
- const description = text(config.description);
269
+ const description = forwardedI18nLabel(config.description);
248
270
  if (description)
249
271
  out.description = description;
250
272
  // A non-positive height would collapse the plot; the container default is the
@@ -278,3 +300,46 @@ export function chartConfigPresentation(raw, fieldCategoryColors) {
278
300
  }
279
301
  return out;
280
302
  }
303
+ /** The families {@link chartTypeIgnoresCompareTo} answers `true` for. */
304
+ const CHART_TYPES_IGNORING_COMPARE_TO = new Set(['pie', 'donut', 'funnel', 'scatter']);
305
+ /**
306
+ * Whether a chart of this family IGNORES `compareTo` — the one declaration of
307
+ * that rule (objectui#7495).
308
+ *
309
+ * Every surface that turns a `compareTo` into a comparison reads this; none
310
+ * keeps a list of its own. The inline chart path (`@object-ui/plugin-charts`'
311
+ * ObjectChart) skips the comparison fetch and synthesises no overlay series for
312
+ * these families, and the dataset widget path (`@object-ui/plugin-dashboard`'s
313
+ * DatasetWidget) forwards no `compareTo` to the executor for a chart of one of
314
+ * them, so the comparison pass never runs and no overlay series is appended.
315
+ *
316
+ * - `pie` / `donut` / `funnel` — single-distribution charts: a comparison
317
+ * overlay has no meaning on them, and the renderer's pie and funnel arms
318
+ * draw only `series[0]`.
319
+ * - `scatter` (objectui#7402) — a scatter binds ONE measure: the renderer reads
320
+ * y through the single `YAxis dataKey={series[0].dataKey}`, so an overlay was
321
+ * painted on the PRIMARY's y and "previous period" landed exactly on top of
322
+ * "current". Drawing it honestly needs the multi-measure projection declined
323
+ * as option A of objectui#7194; `compareTo` on a scatter returns WITH that
324
+ * projection.
325
+ *
326
+ * It reads the RENDERER's chart family (`bar`, `pie`, `scatter`, …), not a
327
+ * dashboard widget type: a caller whose widget types alias a family (the
328
+ * dashboard maps `bubble` to `scatter` and `pyramid` to `funnel`) maps first and
329
+ * asks about the family. An absent or unrecognised family does NOT ignore
330
+ * `compareTo` — the comparison runs.
331
+ *
332
+ * Why it lives here: the dashboard reaches `@object-ui/plugin-charts` only as a
333
+ * devDependency, so this package — a runtime dependency of both — is the one
334
+ * place both paths can read. Two copies were how the lists drifted apart: the
335
+ * dashboard's said "scatter only" while the charts package said all four, so a
336
+ * compare-to pie widget ran a comparison query whose overlay the renderer then
337
+ * dropped (objectui#7495; objectui#4389 is the precedent that a second copy of
338
+ * chart-presentation logic is a defect).
339
+ *
340
+ * @param chartType the chart family, or `undefined` when none is declared
341
+ * @returns `true` for `pie`, `donut`, `funnel` and `scatter`; `false` otherwise
342
+ */
343
+ export function chartTypeIgnoresCompareTo(chartType) {
344
+ return typeof chartType === 'string' && CHART_TYPES_IGNORING_COMPARE_TO.has(chartType);
345
+ }
@@ -10,6 +10,37 @@ export interface ChartResultField {
10
10
  label?: string;
11
11
  format?: string;
12
12
  }
13
+ /**
14
+ * The OPTIONAL wire discriminator naming which server-side BUILT-IN default
15
+ * measure a result column is (objectui#7258; producer half objectstack#14492).
16
+ * The analytics service sets it ONLY on the measures it minted itself with a
17
+ * hard-coded English default label (`count` → `'Count'`, …); an author-declared
18
+ * measure never carries it, so its `label` stays the author's verbatim text
19
+ * (objectui#4106). {@link resolveMeasureLabel} is the one reader.
20
+ *
21
+ * Declared BESIDE {@link ChartResultField} rather than on it, deliberately:
22
+ * `dataset-result-field-spec-parity` pins every key of `ChartResultField` as a
23
+ * key of the spec's `AnalyticsResult.fields[]`, and the `@objectstack/spec`
24
+ * this package is built against does not carry `builtinAggregate` yet. The
25
+ * intersection {@link ChartMeasureField} is what the helpers accept, so a
26
+ * spec-typed field flows in unchanged today and carries the key the moment
27
+ * the spec does — structurally, with no cast at the call sites.
28
+ *
29
+ * Typed as `string` rather than the closed {@link BuiltinAggregate} union on
30
+ * purpose: a JSON response is handed here as-is, so the value is validated at
31
+ * the lookup ({@link isBuiltinAggregate}) instead of trusted at the type.
32
+ *
33
+ * TODO(objectstack#14492): once the spec dependency is bumped past the release
34
+ * that adds `AnalyticsResult.fields[].builtinAggregate`, fold this key into
35
+ * `ChartResultField` as `DatasetResultField['builtinAggregate']` (pinning the
36
+ * two closed vocabularies to each other in the parity suite) and retire the
37
+ * carrier.
38
+ */
39
+ export interface BuiltinAggregateCarrier {
40
+ builtinAggregate?: string;
41
+ }
42
+ /** A result field as the series helpers read it: the renderer shape plus the optional discriminator. */
43
+ export type ChartMeasureField = ChartResultField & BuiltinAggregateCarrier;
13
44
  /**
14
45
  * One renderer-internal series binding produced by {@link buildChartSeries}:
15
46
  * WHICH result-set column to draw and what to call it.
@@ -31,6 +62,47 @@ export interface ChartSeriesResult {
31
62
  xAxisKey: string | undefined;
32
63
  series: ChartSeriesBinding[];
33
64
  }
65
+ /**
66
+ * The closed vocabulary of server-side built-in default measures a result field
67
+ * can declare itself as (objectui#7258 / objectstack#14492) — the spec's own
68
+ * measure-aggregate enum spelling (`count_distinct`, not `countDistinct`), so
69
+ * there is exactly ONE spelling on the wire and this list never grows on its
70
+ * own: a value outside it is not "a new aggregate", it is an unrecognised one,
71
+ * and {@link resolveMeasureLabel} treats it as absent.
72
+ */
73
+ export declare const BUILTIN_AGGREGATES: readonly ["count", "sum", "avg", "min", "max", "count_distinct"];
74
+ /** One member of {@link BUILTIN_AGGREGATES}. */
75
+ export type BuiltinAggregate = (typeof BUILTIN_AGGREGATES)[number];
76
+ /** Runtime membership test for {@link BUILTIN_AGGREGATES}, over an untrusted wire value. */
77
+ export declare function isBuiltinAggregate(value: unknown): value is BuiltinAggregate;
78
+ /**
79
+ * The LOCALIZED display label per built-in aggregate, as the layer that holds
80
+ * the i18n provider resolved them (`@object-ui/i18n`'s
81
+ * `builtinAggregateLabels(tt)`). Partial on purpose: an aggregate the caller
82
+ * did not resolve falls back to the wire `label`, exactly as if the field had
83
+ * carried no discriminator.
84
+ */
85
+ export type BuiltinAggregateLabels = Partial<Record<BuiltinAggregate, string>>;
86
+ /**
87
+ * The display label of ONE result field, for a legend / axis / tooltip.
88
+ *
89
+ * Resolution order (objectui#7258, maintainer ruling B of 2026-09-02):
90
+ * 1. the field declares itself a built-in default measure
91
+ * (`builtinAggregate` ∈ {@link BUILTIN_AGGREGATES}) AND the caller resolved
92
+ * a locale label for it → that label. This is the only path that can
93
+ * replace a wire `label`, and it is keyed by a STRUCTURAL discriminator —
94
+ * never by matching the label's text or the field's name, both of which
95
+ * the ruling rejected (a real author measure literally named `count`, or
96
+ * labelled `Count`, must keep its own label);
97
+ * 2. otherwise the wire `label` verbatim — the objectui#4106 contract for an
98
+ * author-declared measure (`'Tasks'` reaches the legend untouched);
99
+ * 3. otherwise the field's `name`.
100
+ *
101
+ * So a discriminator the caller has no label for (a provider-less host, or a
102
+ * value outside the closed vocabulary) costs nothing: the field renders exactly
103
+ * what it rendered before the discriminator existed.
104
+ */
105
+ export declare function resolveMeasureLabel(field: ChartMeasureField, labels?: BuiltinAggregateLabels): string;
34
106
  /**
35
107
  * The bucket a NULL/undefined category VALUE renders under, when the caller
36
108
  * supplies no localized label of its own (objectui#4466).
@@ -110,6 +182,16 @@ export interface ChartSeriesOptions {
110
182
  * it, so a mismatch costs the null bucket its drill-through.
111
183
  */
112
184
  nullCategoryLabel?: string;
185
+ /**
186
+ * Locale labels for the server's BUILT-IN default measures, keyed by the
187
+ * wire discriminator a result field carries as `builtinAggregate`
188
+ * (objectui#7258). Same division of labour as `nullCategoryLabel`: this
189
+ * package is React-free and i18n-free, so the renderer that holds the
190
+ * provider resolves the strings (`builtinAggregateLabels(tt)` in
191
+ * `@object-ui/i18n`) and passes them in. Absent ⇒ every measure keeps its
192
+ * wire `label`, which for a built-in default is the server's English.
193
+ */
194
+ builtinAggregateLabels?: BuiltinAggregateLabels;
113
195
  }
114
196
  /** Per-CLICK knobs for {@link findChartSeriesRow}, on top of the shared ones. */
115
197
  export interface ChartDrillLookupOptions extends ChartSeriesOptions {
@@ -174,7 +256,7 @@ export interface ChartSegmentClickEvent {
174
256
  /** The measure value at the click point. */
175
257
  value?: number;
176
258
  }
177
- export declare function buildChartSeries(rows: Array<Record<string, unknown>> | null | undefined, dimensions: string[] | null | undefined, values: string[] | null | undefined, fields?: ChartResultField[] | null, options?: ChartSeriesOptions): ChartSeriesResult;
259
+ export declare function buildChartSeries(rows: Array<Record<string, unknown>> | null | undefined, dimensions: string[] | null | undefined, values: string[] | null | undefined, fields?: ChartMeasureField[] | null, options?: ChartSeriesOptions): ChartSeriesResult;
178
260
  /**
179
261
  * Resolve select/enum dimension VALUES to display LABELS in chart rows.
180
262
  *
@@ -362,10 +444,54 @@ export declare function buildDimensionLabelMap(options: unknown, translate?: Opt
362
444
  * The object a relationship field points at, or `undefined` when the field is
363
445
  * not a relationship (or names no target).
364
446
  *
365
- * The target lives under `reference` on framework-served field defs; older /
366
- * spec shapes spell it `reference_to` / `referenceTo` / `reference_to_object`,
367
- * and any of them may carry a bare name, a one-element array, or `{ object }`.
368
- * Same canonicalization as the dataset designer's `resolveReferenceTo`.
447
+ * The target lives under `reference` — the ONLY spelling read here, and the same
448
+ * canonicalization as the dataset designer's `resolveReferenceTo`, which is
449
+ * narrowed to match in the same pass (objectui#6528). As of objectui#6648 the
450
+ * CARRIER matches too: a bare string and nothing else. The two helpers are one
451
+ * doctrine in two files and must never drift — a divergence recreates the
452
+ * defect one file over.
453
+ *
454
+ * The three legacy spellings this dropped (`reference_to` / `referenceTo` /
455
+ * `reference_to_object`) were censused against every producer, with `reference`
456
+ * itself as the positive control (445 of 565 lookup / master_detail defs in the
457
+ * framework tree; ACCEPTED by `ObjectSchema.safeParse` on spec 17.2.0, which
458
+ * REFUSES all three others BY NAME). No producer emits any of them onto an
459
+ * object metadata document: `reference_to` was then live only on ObjectUI's own
460
+ * view/field schema (a different contract, translated INTO from `reference`;
461
+ * objectui#11070 round 4 moved that contract to `reference` as well),
462
+ * `referenceTo`'s two producers were retired by objectui#6041, and
463
+ * `reference_to_object` occurs nowhere in either tree outside this chain and the
464
+ * test that called it.
465
+ *
466
+ * ⚠ Unlike `resolveReferenceTo`, this helper reads the `GET /meta/object/:name`
467
+ * document DIRECTLY — no `readFields` door strips retired keys on this path. So
468
+ * the narrowing is load-bearing rather than cosmetic: a stored pre-objectui#6041
469
+ * row spelling the target `referenceTo` now resolves to `undefined` here, the
470
+ * walk yields no entry, and the caller keeps the raw value (best-effort by
471
+ * construction — see {@link resolveDimensionFieldMeta}). That is the intended
472
+ * outcome: per AGENTS.md #0.1 such a row is a PRODUCER-side defect, and a
473
+ * lenient consumer here is exactly where it would have stayed hidden. Note
474
+ * `reference` was already HEAD of the old chain, so any doc carrying both is
475
+ * unaffected.
476
+ *
477
+ * The array and `{ object }` CARRIERS this used to accept were censused on the
478
+ * same standard and removed with the same reasoning (objectui#6648).
479
+ * `FieldSchema.reference` is a plain `z.string()`; `ObjectSchema.safeParse`
480
+ * (spec 17.2.0) ACCEPTS `reference: 'crm_account'` and REFUSES both
481
+ * `['crm_account']` and `{ object: 'crm_account' }` as `invalid_type`. A
482
+ * structure-walking, key-position-aware census of both trees found 587
483
+ * bare-string carriers at the field-def key position and ZERO array or
484
+ * `{ object }` carriers from any producer — the only ones in either tree were
485
+ * the green pins asserting this very tolerance. See `resolveReferenceTo` for
486
+ * the full census, including why a multi-target lookup is NOT what the array
487
+ * branch was serving: it returned `raw[0]` and discarded the rest, and
488
+ * polymorphic lookup remains an undeclared, open spec gap.
489
+ *
490
+ * This path matters MORE than the designer's for the same reason the spelling
491
+ * narrowing did: it reads the metadata document directly, with no `readFields`
492
+ * door in between, so a non-string carrier now degrades visibly (the walk
493
+ * yields no entry and the caller keeps the raw value) instead of being absorbed
494
+ * here.
369
495
  *
370
496
  * The **type gate is deliberate**: only a declared relationship is walked, so a
371
497
  * path segment naming a plain field can never be turned into an object name and
@@ -44,6 +44,47 @@
44
44
  * know" refuses — one doctrine, and now BOTH branches state it.
45
45
  */
46
46
  import { pivotBucketId, pivotDimensionValue } from './dataset-pivot.js';
47
+ /**
48
+ * The closed vocabulary of server-side built-in default measures a result field
49
+ * can declare itself as (objectui#7258 / objectstack#14492) — the spec's own
50
+ * measure-aggregate enum spelling (`count_distinct`, not `countDistinct`), so
51
+ * there is exactly ONE spelling on the wire and this list never grows on its
52
+ * own: a value outside it is not "a new aggregate", it is an unrecognised one,
53
+ * and {@link resolveMeasureLabel} treats it as absent.
54
+ */
55
+ export const BUILTIN_AGGREGATES = ['count', 'sum', 'avg', 'min', 'max', 'count_distinct'];
56
+ /** Runtime membership test for {@link BUILTIN_AGGREGATES}, over an untrusted wire value. */
57
+ export function isBuiltinAggregate(value) {
58
+ return typeof value === 'string' && BUILTIN_AGGREGATES.includes(value);
59
+ }
60
+ /**
61
+ * The display label of ONE result field, for a legend / axis / tooltip.
62
+ *
63
+ * Resolution order (objectui#7258, maintainer ruling B of 2026-09-02):
64
+ * 1. the field declares itself a built-in default measure
65
+ * (`builtinAggregate` ∈ {@link BUILTIN_AGGREGATES}) AND the caller resolved
66
+ * a locale label for it → that label. This is the only path that can
67
+ * replace a wire `label`, and it is keyed by a STRUCTURAL discriminator —
68
+ * never by matching the label's text or the field's name, both of which
69
+ * the ruling rejected (a real author measure literally named `count`, or
70
+ * labelled `Count`, must keep its own label);
71
+ * 2. otherwise the wire `label` verbatim — the objectui#4106 contract for an
72
+ * author-declared measure (`'Tasks'` reaches the legend untouched);
73
+ * 3. otherwise the field's `name`.
74
+ *
75
+ * So a discriminator the caller has no label for (a provider-less host, or a
76
+ * value outside the closed vocabulary) costs nothing: the field renders exactly
77
+ * what it rendered before the discriminator existed.
78
+ */
79
+ export function resolveMeasureLabel(field, labels) {
80
+ const aggregate = field.builtinAggregate;
81
+ if (isBuiltinAggregate(aggregate)) {
82
+ const localized = labels?.[aggregate];
83
+ if (typeof localized === 'string' && localized !== '')
84
+ return localized;
85
+ }
86
+ return field.label ?? field.name;
87
+ }
47
88
  /**
48
89
  * The bucket a NULL/undefined category VALUE renders under, when the caller
49
90
  * supplies no localized label of its own (objectui#4466).
@@ -296,7 +337,13 @@ export function buildChartSeries(rows, dimensions, values, fields, options) {
296
337
  const dims = (dimensions ?? []).filter(Boolean);
297
338
  const vals = (values ?? []).filter(Boolean);
298
339
  const safeRows = Array.isArray(rows) ? rows : [];
299
- const labelOf = (name) => (fields ?? []).find((f) => f.name === name)?.label ?? name;
340
+ // A measure's series label: the locale label when the field declares itself
341
+ // a built-in default measure and the caller resolved one, else the wire
342
+ // `label` verbatim (objectui#4106), else the name — see `resolveMeasureLabel`.
343
+ const labelOf = (name) => {
344
+ const field = (fields ?? []).find((f) => f.name === name);
345
+ return field ? resolveMeasureLabel(field, options?.builtinAggregateLabels) : name;
346
+ };
300
347
  // Multi-dimension, single-measure → pivot the second dimension into series.
301
348
  if (dims.length >= 2 && vals.length === 1) {
302
349
  const xKey = dims[0];
@@ -765,10 +812,54 @@ function fieldDefsOf(schema) {
765
812
  * The object a relationship field points at, or `undefined` when the field is
766
813
  * not a relationship (or names no target).
767
814
  *
768
- * The target lives under `reference` on framework-served field defs; older /
769
- * spec shapes spell it `reference_to` / `referenceTo` / `reference_to_object`,
770
- * and any of them may carry a bare name, a one-element array, or `{ object }`.
771
- * Same canonicalization as the dataset designer's `resolveReferenceTo`.
815
+ * The target lives under `reference` — the ONLY spelling read here, and the same
816
+ * canonicalization as the dataset designer's `resolveReferenceTo`, which is
817
+ * narrowed to match in the same pass (objectui#6528). As of objectui#6648 the
818
+ * CARRIER matches too: a bare string and nothing else. The two helpers are one
819
+ * doctrine in two files and must never drift — a divergence recreates the
820
+ * defect one file over.
821
+ *
822
+ * The three legacy spellings this dropped (`reference_to` / `referenceTo` /
823
+ * `reference_to_object`) were censused against every producer, with `reference`
824
+ * itself as the positive control (445 of 565 lookup / master_detail defs in the
825
+ * framework tree; ACCEPTED by `ObjectSchema.safeParse` on spec 17.2.0, which
826
+ * REFUSES all three others BY NAME). No producer emits any of them onto an
827
+ * object metadata document: `reference_to` was then live only on ObjectUI's own
828
+ * view/field schema (a different contract, translated INTO from `reference`;
829
+ * objectui#11070 round 4 moved that contract to `reference` as well),
830
+ * `referenceTo`'s two producers were retired by objectui#6041, and
831
+ * `reference_to_object` occurs nowhere in either tree outside this chain and the
832
+ * test that called it.
833
+ *
834
+ * ⚠ Unlike `resolveReferenceTo`, this helper reads the `GET /meta/object/:name`
835
+ * document DIRECTLY — no `readFields` door strips retired keys on this path. So
836
+ * the narrowing is load-bearing rather than cosmetic: a stored pre-objectui#6041
837
+ * row spelling the target `referenceTo` now resolves to `undefined` here, the
838
+ * walk yields no entry, and the caller keeps the raw value (best-effort by
839
+ * construction — see {@link resolveDimensionFieldMeta}). That is the intended
840
+ * outcome: per AGENTS.md #0.1 such a row is a PRODUCER-side defect, and a
841
+ * lenient consumer here is exactly where it would have stayed hidden. Note
842
+ * `reference` was already HEAD of the old chain, so any doc carrying both is
843
+ * unaffected.
844
+ *
845
+ * The array and `{ object }` CARRIERS this used to accept were censused on the
846
+ * same standard and removed with the same reasoning (objectui#6648).
847
+ * `FieldSchema.reference` is a plain `z.string()`; `ObjectSchema.safeParse`
848
+ * (spec 17.2.0) ACCEPTS `reference: 'crm_account'` and REFUSES both
849
+ * `['crm_account']` and `{ object: 'crm_account' }` as `invalid_type`. A
850
+ * structure-walking, key-position-aware census of both trees found 587
851
+ * bare-string carriers at the field-def key position and ZERO array or
852
+ * `{ object }` carriers from any producer — the only ones in either tree were
853
+ * the green pins asserting this very tolerance. See `resolveReferenceTo` for
854
+ * the full census, including why a multi-target lookup is NOT what the array
855
+ * branch was serving: it returned `raw[0]` and discarded the rest, and
856
+ * polymorphic lookup remains an undeclared, open spec gap.
857
+ *
858
+ * This path matters MORE than the designer's for the same reason the spelling
859
+ * narrowing did: it reads the metadata document directly, with no `readFields`
860
+ * door in between, so a non-string carrier now degrades visibly (the walk
861
+ * yields no entry and the caller keeps the raw value) instead of being absorbed
862
+ * here.
772
863
  *
773
864
  * The **type gate is deliberate**: only a declared relationship is walked, so a
774
865
  * path segment naming a plain field can never be turned into an object name and
@@ -781,17 +872,8 @@ export function resolveRelationshipTarget(fieldDef) {
781
872
  const type = typeof def.type === 'string' ? def.type.toLowerCase() : '';
782
873
  if (!RELATIONSHIP_FIELD_TYPES.has(type))
783
874
  return undefined;
784
- const raw = def.reference ?? def.reference_to ?? def.referenceTo ?? def.reference_to_object;
785
- if (typeof raw === 'string' && raw)
786
- return raw;
787
- if (Array.isArray(raw) && typeof raw[0] === 'string' && raw[0])
788
- return raw[0];
789
- if (raw && typeof raw === 'object') {
790
- const obj = raw.object;
791
- if (typeof obj === 'string' && obj)
792
- return obj;
793
- }
794
- return undefined;
875
+ const raw = def.reference;
876
+ return typeof raw === 'string' && raw ? raw : undefined;
795
877
  }
796
878
  /**
797
879
  * Resolve each dataset dimension's underlying `field` to that field's select