@object-ui/core 17.6.0 → 18.0.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 (147) hide show
  1. package/CHANGELOG.md +7260 -0
  2. package/README.md +228 -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 +473 -69
  6. package/dist/actions/ActionRunner.js +792 -125
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/UndoManager.d.ts +8 -13
  9. package/dist/actions/actionKeys.d.ts +9 -7
  10. package/dist/actions/actionKeys.js +67 -14
  11. package/dist/actions/actionResponse.js +2 -2
  12. package/dist/actions/index.d.ts +2 -0
  13. package/dist/actions/index.js +2 -0
  14. package/dist/actions/navigationTarget.d.ts +41 -0
  15. package/dist/actions/navigationTarget.js +20 -0
  16. package/dist/actions/recordDelete.d.ts +154 -0
  17. package/dist/actions/recordDelete.js +110 -0
  18. package/dist/adapters/ApiDataSource.d.ts +8 -4
  19. package/dist/adapters/ApiDataSource.js +19 -7
  20. package/dist/adapters/ValueDataSource.d.ts +6 -1
  21. package/dist/adapters/ValueDataSource.js +1090 -98
  22. package/dist/adapters/batchTransaction.d.ts +1 -1
  23. package/dist/adapters/batchTransaction.js +1 -1
  24. package/dist/adapters/resolveDataSource.d.ts +9 -6
  25. package/dist/adapters/resolveDataSource.js +9 -12
  26. package/dist/builder/schema-builder.d.ts +38 -81
  27. package/dist/builder/schema-builder.js +33 -152
  28. package/dist/data-scope/ViewDataProvider.d.ts +8 -25
  29. package/dist/data-scope/ViewDataProvider.js +27 -54
  30. package/dist/data-scope/element-data-source.d.ts +261 -7
  31. package/dist/data-scope/element-data-source.js +342 -52
  32. package/dist/data-scope/index.d.ts +9 -4
  33. package/dist/data-scope/index.js +9 -4
  34. package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
  35. package/dist/evaluator/ExpressionEvaluator.js +184 -12
  36. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  37. package/dist/evaluator/FormulaFunctions.js +166 -27
  38. package/dist/evaluator/deadlineSettled.d.ts +40 -0
  39. package/dist/evaluator/deadlineSettled.js +45 -0
  40. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  41. package/dist/evaluator/declaredPredicate.js +117 -16
  42. package/dist/evaluator/fieldRules.d.ts +88 -4
  43. package/dist/evaluator/fieldRules.js +208 -31
  44. package/dist/evaluator/index.d.ts +2 -0
  45. package/dist/evaluator/index.js +2 -0
  46. package/dist/evaluator/listConditional.d.ts +103 -34
  47. package/dist/evaluator/listConditional.js +127 -50
  48. package/dist/evaluator/optionRules.d.ts +77 -6
  49. package/dist/evaluator/optionRules.js +87 -10
  50. package/dist/evaluator/predicateInput.d.ts +15 -4
  51. package/dist/evaluator/predicateInput.js +35 -2
  52. package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
  53. package/dist/evaluator/rowPredicateCanon.js +65 -115
  54. package/dist/evaluator/subjectPermissions.d.ts +105 -0
  55. package/dist/evaluator/subjectPermissions.js +86 -0
  56. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  57. package/dist/evaluator/unevaluablePredicate.js +70 -0
  58. package/dist/index.d.ts +15 -0
  59. package/dist/index.js +83 -2
  60. package/dist/protocols/index.d.ts +0 -1
  61. package/dist/protocols/index.js +9 -1
  62. package/dist/registry/Registry.d.ts +382 -34
  63. package/dist/registry/Registry.js +247 -12
  64. package/dist/registry/WidgetRegistry.js +7 -3
  65. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  66. package/dist/registry/html-tier-intrinsics.js +110 -0
  67. package/dist/registry/public-blocks.d.ts +8 -0
  68. package/dist/registry/public-blocks.js +57 -0
  69. package/dist/theme/ThemeEngine.js +1 -1
  70. package/dist/utils/affordanceGrants.d.ts +371 -0
  71. package/dist/utils/affordanceGrants.js +252 -0
  72. package/dist/utils/chart-category-key.d.ts +82 -0
  73. package/dist/utils/chart-category-key.js +84 -0
  74. package/dist/utils/chart-measure-key.d.ts +64 -0
  75. package/dist/utils/chart-measure-key.js +65 -0
  76. package/dist/utils/chart-presentation.d.ts +95 -112
  77. package/dist/utils/chart-presentation.js +169 -104
  78. package/dist/utils/chart-series.d.ts +165 -5
  79. package/dist/utils/chart-series.js +123 -18
  80. package/dist/utils/column-sortability.d.ts +157 -0
  81. package/dist/utils/column-sortability.js +217 -0
  82. package/dist/utils/dashboard-filters.d.ts +38 -6
  83. package/dist/utils/dashboard-filters.js +107 -61
  84. package/dist/utils/dataset-format.d.ts +211 -30
  85. package/dist/utils/dataset-format.js +389 -46
  86. package/dist/utils/date-display.d.ts +346 -0
  87. package/dist/utils/date-display.js +644 -0
  88. package/dist/utils/date-macros.js +7 -0
  89. package/dist/utils/drill-down.d.ts +78 -0
  90. package/dist/utils/drill-down.js +93 -0
  91. package/dist/utils/emptiness.d.ts +96 -0
  92. package/dist/utils/emptiness.js +101 -0
  93. package/dist/utils/expand-fields.d.ts +81 -9
  94. package/dist/utils/expand-fields.js +81 -9
  95. package/dist/utils/extract-records.d.ts +65 -5
  96. package/dist/utils/extract-records.js +65 -8
  97. package/dist/utils/filter-converter.d.ts +158 -6
  98. package/dist/utils/filter-converter.js +1098 -24
  99. package/dist/utils/filter-tokens.d.ts +49 -5
  100. package/dist/utils/filter-tokens.js +105 -33
  101. package/dist/utils/grouping-fields.d.ts +61 -0
  102. package/dist/utils/grouping-fields.js +84 -0
  103. package/dist/utils/managedBy.d.ts +3 -3
  104. package/dist/utils/managedBy.js +5 -5
  105. package/dist/utils/native-date-value.d.ts +48 -0
  106. package/dist/utils/native-date-value.js +167 -0
  107. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  108. package/dist/utils/non-grid-row-ceiling.js +141 -0
  109. package/dist/utils/normalize-list-view.d.ts +135 -11
  110. package/dist/utils/normalize-list-view.js +243 -64
  111. package/dist/utils/number-display.d.ts +63 -14
  112. package/dist/utils/number-display.js +34 -12
  113. package/dist/utils/object-aggregate-query.d.ts +113 -0
  114. package/dist/utils/object-aggregate-query.js +50 -0
  115. package/dist/utils/parent-scope.d.ts +123 -0
  116. package/dist/utils/parent-scope.js +140 -0
  117. package/dist/utils/predicate-fields.d.ts +45 -12
  118. package/dist/utils/predicate-fields.js +141 -12
  119. package/dist/utils/record-color.d.ts +75 -0
  120. package/dist/utils/record-color.js +65 -0
  121. package/dist/utils/record-source.d.ts +298 -0
  122. package/dist/utils/record-source.js +331 -0
  123. package/dist/utils/record-title.d.ts +148 -8
  124. package/dist/utils/record-title.js +220 -26
  125. package/dist/utils/reference-keys.d.ts +21 -22
  126. package/dist/utils/reference-keys.js +532 -29
  127. package/dist/utils/sort-query.d.ts +115 -15
  128. package/dist/utils/sort-query.js +240 -13
  129. package/dist/utils/text-comparand.d.ts +92 -0
  130. package/dist/utils/text-comparand.js +99 -0
  131. package/dist/utils/without-denied-fields.d.ts +49 -0
  132. package/dist/utils/without-denied-fields.js +52 -0
  133. package/dist/validation/index.d.ts +8 -4
  134. package/dist/validation/index.js +8 -4
  135. package/dist/validation/schema-validator.d.ts +1 -1
  136. package/dist/validation/schema-validator.js +225 -83
  137. package/dist/validation/server-owned-value.d.ts +5 -1
  138. package/dist/validation/server-owned-value.js +5 -1
  139. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  140. package/dist/validation/validators/object-validation-engine.js +14 -3
  141. package/package.json +8 -9
  142. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  143. package/dist/data-scope/DataScopeManager.js +0 -229
  144. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  145. package/dist/protocols/ResponsiveProtocol.js +0 -158
  146. package/dist/validation/validation-engine.d.ts +0 -88
  147. package/dist/validation/validation-engine.js +0 -434
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import type { AnalyticsResult } from '@objectstack/spec/contracts';
19
19
  import type { PercentScale } from '@objectstack/spec/data';
20
+ import { type BuiltinAggregateLabels } from './chart-series.js';
20
21
  /**
21
22
  * Column metadata the analytics server returns alongside the rows — the spec's
22
23
  * `AnalyticsResult.fields[]` element BY REFERENCE, never a local restatement of
@@ -59,28 +60,35 @@ export type DatasetResultField = AnalyticsResult['fields'][number];
59
60
  */
60
61
  export type { PercentScale } from '@objectstack/spec/data';
61
62
  /**
62
- * Scale a stored `percent`-field value to its DISPLAY magnitude.
63
- *
64
- * Percent fields store a FRACTION (0–1) by convention — a stored `0.75` means
65
- * 75% (see the percent edit widget `PercentField`, which divides input by 100).
66
- * A value already in whole-percent form (magnitude ≥ 1, e.g. a `progress` /
67
- * `completion` field storing `57`) is passed through unchanged. This is the
68
- * SINGLE source of truth for percent display scaling, shared by the list-view
69
- * percent cell renderer (`formatPercent` in `@object-ui/fields`) and the dataset
70
- * measure formatter ({@link formatMeasure}) so a percent renders identically as
71
- * a row value and as an aggregated metric — the two surfaces can never drift.
72
- *
73
- * ⚠️ That last sentence was briefly FALSE, and objectui#4576 is what made it
74
- * true again. Sharing the SCALING was never enough on its own: between #4553
75
- * and #4576 the cell renderer got the locale's percent CONVENTION from `Intl`
76
- * while {@link formatMeasure} appended a literal '%', so a German session read
77
- * `1.234,5 %` in a list cell and `1.234,5%` in a dashboard measure — the same
78
- * number, scaled identically, rendered under two conventions. Both ends now go
79
- * through the locale's own percent affix. If a third surface ever needs percent
80
- * display, it takes BOTH halves from here — the scaling AND the convention —
81
- * or this promise breaks again in the same place.
63
+ * Scale a stored percentage to its DISPLAY magnitude (percentage points), at
64
+ * the storage the caller STATES.
65
+ *
66
+ * `percentScale` is the spec's `PercentScale`: `fraction` is a 0–1 ratio
67
+ * (`0.75` ⇒ 75, `1` ⇒ 100), `whole` is already in percentage points (`75` ⇒
68
+ * 75, `1` ⇒ 1). A caller holding a field definition reads it with the spec's
69
+ * `percentScaleOf` (`@objectstack/spec/data`): a `percent` field stores a
70
+ * fraction unless it declares a `max` above 1. A caller with no field (a
71
+ * computed ratio, a server measure column) states the storage it knows: a
72
+ * `derived: { op: 'ratio' }` measure is a fraction by definition, and the
73
+ * server annotates a measure column with its `percentScale`.
74
+ *
75
+ * ⛔ No storage is inferred from the VALUE any more (objectui#11475). This
76
+ * function used to be `value > -1 && value < 1 ? value * 100 : value`, a guess
77
+ * from the magnitude that read neither the field nor its `max`. A fraction-
78
+ * stored `1` (100%) therefore read `1%` in the list cell while the read-only
79
+ * form, which reads the declaration, read `100%`; a whole-stored `0.5` read
80
+ * `50%`. objectui#3136 was the same guess on a dashboard measure. The
81
+ * parameter is REQUIRED, so every caller says which storage it holds, and a
82
+ * value outside the union throws rather than falling to a branch silently.
83
+ *
84
+ * ⭐ The contract a third percent surface still owes is BOTH halves: this
85
+ * scaling, at the stored scale, AND the locale's percent convention
86
+ * (`style: 'percentPoints'`, see {@link formatMeasure} and `formatPercent` in
87
+ * `@object-ui/fields`). Between objectui#4553 and objectui#4576 the two ends
88
+ * shared the scaling and not the convention, so a German session read
89
+ * `1.234,5 %` in a list cell and `1.234,5%` in a dashboard measure.
82
90
  */
83
- export declare function percentDisplayValue(value: number): number;
91
+ export declare function percentDisplayValue(value: number, percentScale: PercentScale): number;
84
92
  /**
85
93
  * Format a MEASURE value. Currency comes from the field's declared `currency`
86
94
  * (locale-correct symbol via `Intl`), NOT from a "$" baked into the format
@@ -118,8 +126,54 @@ export declare function percentDisplayValue(value: number): number;
118
126
  * is pure, so it moved DOWN into this package (`./number-display.ts`) and
119
127
  * `@object-ui/i18n` re-exports it. The duplicate implementation this file used
120
128
  * to carry is gone, and with it the drift that duplicate produced — see the
121
- * percent note in the body, and {@link percentDisplayValue}'s promise, which
122
- * is true again.
129
+ * percent note in the body, and the both-halves contract
130
+ * {@link percentDisplayValue}'s doc comment states.
131
+ *
132
+ * ── Date-valued measures (objectui#7178) ──
133
+ * `min` / `max` over a date or datetime field is a legitimate measure, and it
134
+ * used to render its stored value verbatim: the guard above returned
135
+ * `String(v)` for anything non-numeric BEFORE `format` was read, so a KPI tile
136
+ * showed `2026-07-04T07:00:00.000Z` in `text-2xl` and wrapped it over two
137
+ * lines. A date-shaped value now routes through {@link formatMeasureDate} to
138
+ * the display path list cells use; nothing about the numeric path moved.
139
+ *
140
+ * ⚠️ What `format` can and cannot say for those values, measured rather than
141
+ * assumed, and stated PER ARM — a date-shaped measure takes one of two arms in
142
+ * {@link formatMeasureDate}, and this paragraph used to describe only one of
143
+ * them while reading as though it described both. That cost a card: an
144
+ * objectui triage pass read the undifferentiated claim, concluded the defect
145
+ * was already fixed, and downgraded objectui#8352, which a driven browser run
146
+ * then refuted. The two arms agree TODAY, but only because #8352 made them; a
147
+ * universal sentence here is what hid the disagreement before, so it is not
148
+ * coming back even now that it would be true.
149
+ *
150
+ * The shared date path takes a named STYLE, not a date pattern. On BOTH arms
151
+ * the honoured vocabulary is `'short'` and `'relative'`:
152
+ *
153
+ * - `Field.date` (`2026-09-08`) — honoured by threading `format` into
154
+ * `formatDate`'s style parameter. The same words `DateCellRenderer`
155
+ * honours from `field.format` for the same field.
156
+ * - `Field.datetime` (`2026-09-01T00:00:00.000Z`) — honoured by SELECTING a
157
+ * formatter, because `formatDateTime` has no style parameter to thread
158
+ * into (objectui#8352). Before that card this arm honoured NOTHING: it
159
+ * dropped `format` entirely, so `'relative'` rendered the absolute face.
160
+ *
161
+ * Every other string falls to that arm's default face — a PATTERN like
162
+ * `format: 'YYYY-MM-DD'` is accepted by the schema, reaches this function, and
163
+ * renders the locale default, and so does `'compact'`, which is
164
+ * `formatDateTime`'s own vocabulary but not this path's. The pattern part is
165
+ * not new behaviour introduced here: `plugin-dashboard`'s `recordFields`
166
+ * already routes a date-shaped `format` into the same style slot and gets the
167
+ * same locale default. Closing it would mean teaching the shared path a
168
+ * pattern grammar, which is a change to the path itself and to every list cell
169
+ * that reads it — not a measure concern, and not either card.
170
+ *
171
+ * ⚠️ `'relative'` renders the ABSOLUTE face for a value more than ±7 days from
172
+ * today, on both arms, by `formatRelativeDate`'s own design. That is correct
173
+ * output, not a regression — but it also means an out-of-window value is
174
+ * useless for testing this path, because it renders identically whether
175
+ * `format` is honoured or dropped. Both readings that missed #8352 were taken
176
+ * on 40–60-day-old data.
123
177
  */
124
178
  export declare function formatMeasure(v: unknown, format?: string, currency?: string, percentScale?: PercentScale, locale?: string): string;
125
179
  /**
@@ -140,13 +194,31 @@ export declare function formatDimensionValue(v: unknown, locale?: string): strin
140
194
  *
141
195
  * `fieldLabel` is injected (rather than imported) so this stays React/i18n-free;
142
196
  * callers pass `useSafeFieldLabel().fieldLabel`.
197
+ *
198
+ * ## `builtinAggregateLabels` (objectui#7534, sibling of #7258)
199
+ *
200
+ * A result field the analytics service minted as a BUILT-IN default measure
201
+ * carries `builtinAggregate` (objectstack#14492) and a hard-coded English
202
+ * `label` ('Count'). {@link resolveMeasureLabel} is the ONE resolver for that —
203
+ * #7258 wired it into `buildChartSeries()`, so a chart legend already read
204
+ * `计数` on a zh console while the table underneath it, the KPI caption, the
205
+ * pivot header and the dataset preview all still said `Count`, because they
206
+ * resolve their titles through here instead.
207
+ *
208
+ * Passing the caller's resolved labels closes that gap without a second
209
+ * resolution order: the discriminator is consulted FIRST (exactly as on the
210
+ * chart seam), the existing `fieldLabel` convention still applies on top, and
211
+ * the raw name is still the floor. Omitting the argument reproduces the
212
+ * previous output byte for byte — an author-declared measure carries no
213
+ * discriminator and keeps its wire `label` verbatim (objectui#4106), and a
214
+ * provider-less host resolves no labels and so changes nothing.
143
215
  */
144
- export declare function buildDatasetFieldHelpers(fields: DatasetResultField[] | undefined, object: string | undefined, fieldLabel?: (objectName: string, fieldName: string, fallback: string) => string): {
216
+ export declare function buildDatasetFieldHelpers(fields: DatasetResultField[] | undefined, object: string | undefined, fieldLabel?: (objectName: string, fieldName: string, fallback: string) => string, builtinAggregateLabels?: BuiltinAggregateLabels): {
145
217
  measureField: (name: string) => DatasetResultField | undefined;
146
218
  headerLabel: (name: string) => string;
147
219
  };
148
220
  /**
149
- * A half-open date-range drill scope for one time-bucketed dimension (#1752):
221
+ * A half-open date-range drill scope for one time-bucketed dimension (objectstack-ai/objectstack#1752):
150
222
  * the object FIELD to filter and its inclusive `gte` / exclusive `lt` bounds
151
223
  * (the server's `drillRanges` sidecar entry).
152
224
  */
@@ -161,18 +233,127 @@ export interface DatasetDrillRange {
161
233
  * Each drillable dimension maps to its underlying object field, filtered by the
162
234
  * dimension's RAW grouped value (from the server's parallel `drillRawRows`, NOT
163
235
  * the visible row which carries the display LABEL — a select/lookup label would
164
- * mis-filter). An empty/undefined raw value normalizes to `null` (an explicit
165
- * "is empty" filter). The render-time `runtimeFilter` is ANDed in so the drilled
166
- * list stays within the same slice the aggregate was computed over.
236
+ * mis-filter). The render-time `runtimeFilter` is ANDed in so the drilled list
237
+ * stays within the same slice the aggregate was computed over.
238
+ *
239
+ * ## What the EMPTY bucket means, and why it is spelled `{ $null: true }`
167
240
  *
168
- * A time-bucketed date dimension (#1752) drills by RANGE, not equality — a
241
+ * A drill must return exactly the rows the clicked bucket COUNTED. The empty
242
+ * bucket is the one the aggregate grouped rows with NO VALUE for that dimension
243
+ * into, so the drill means "this dimension has no value" — NOT "this dimension
244
+ * holds the literal value null". Those are different row sets, and the
245
+ * difference is measurable rather than theoretical: against `ValueDataSource`'s
246
+ * matcher over rows `{owner:'alice'}` / `{owner:null}` / `{}` (no `owner` key),
247
+ * `['owner','is_null',true]` selects the explicit-null row AND the missing-key
248
+ * row, while an equality test against `null` selects only the explicit-null one
249
+ * and drops the row that has no `owner` at all. The bucket counted both, so
250
+ * is-null is the one that agrees with the number the user clicked.
251
+ *
252
+ * ⇒ every empty authoring — `''`, `null` and `undefined` alike — becomes ONE
253
+ * spelling, `{ [field]: { $null: true } }`, which `convertFiltersToAST` lowers
254
+ * to `[field, 'is_null', true]`. The three are not told apart because the
255
+ * server already merged them into the single bucket the user clicked; there is
256
+ * no second bucket here for a second spelling to address.
257
+ *
258
+ * ⚠️ This value used to be a bare `null`, and that is the defect objectui#9085
259
+ * records: `convertFiltersToAST`'s OLDEST pinned behaviour is to SKIP a key
260
+ * whose value is `null` / `undefined` (pinned as "should skip null and
261
+ * undefined values"), so the constraint was dropped on the way to the wire and
262
+ * the empty-bucket drill answered with a SUPERSET — every row, silently. The
263
+ * repair is here at the PRODUCER and ⛔ not at that converter: the skip is
264
+ * relied on by other producers, and changing it would move every caller's
265
+ * meaning at once (objectui#9020 ruled it stays). `$null` is not a new
266
+ * vocabulary either — it is a declared spec operator this dialect already
267
+ * lowers, and the one spelling that says the same thing on BOTH of
268
+ * `ValueDataSource`'s routes, where a bare `null` said "only an explicit null"
269
+ * on the plain-object route and "no constraint at all" on the AST route.
270
+ *
271
+ * ⚠️ Known BOUNDARY, unchanged by this and deliberately not widened: the drill
272
+ * "escape hatch" (the host's `openRecordList`, which serializes a drill filter
273
+ * into `filter[...]` URL params) has NO spelling for is-null — its operator
274
+ * vocabulary is equality plus four range bounds — so it drops this condition
275
+ * exactly as it already dropped the bare `null`, byte-for-byte the same query
276
+ * string. That surface is a superset today and stays one; closing it needs a
277
+ * URL-dialect operator on both the write and the read side, which is its own
278
+ * card.
279
+ *
280
+ * A time-bucketed date dimension (objectstack-ai/objectstack#1752) drills by RANGE, not equality — a
169
281
  * humanized bucket ("2026-Q2") can't be exact-matched, so the server sends a
170
282
  * half-open `[gte, lt)` per date dim in `rawRanges` instead of a raw value.
171
283
  * Each becomes an ObjectQL range operator object (`{ $gte, $lt }`) so the drill
172
284
  * scopes the list to the clicked time bucket instead of every bucket (which the
173
285
  * old date-dim skip degraded to — a superset).
174
286
  *
287
+ * ## The widget filter is CONJOINED, not spread (objectui#9137)
288
+ *
289
+ * ⛔ `runtimeFilter` is composed through {@link composeDrillFilter}, never by
290
+ * spreading it into the object literal below. Spreading is correct only for the
291
+ * OBJECT arm; this parameter's declared producer sends the ARRAY arm. The
292
+ * dashboard's `DashboardWidgetSchema.filter` docblock states it outright —
293
+ * "objectui passes an ObjectQL FilterNode array here, not the spec's
294
+ * `FilterCondition` envelope" — and `DatasetWidget`'s guard admits it
295
+ * (`typeof [] === 'object'` and `Object.keys(['x']).length === 1` are both
296
+ * true), so the array arm reached this line unfiltered. Spreading an ARRAY
297
+ * yields INDEX keys: `[['region','=','emea']]` became
298
+ * `{ '0': ['region','=','emea'] }`, which is not a condition anyone can honour.
299
+ *
300
+ * ⚠️ MEASURED, and it is NOT the silent superset the sibling sites had. An
301
+ * index key is not dropped on the way to the wire — every sink in circulation
302
+ * refuses it LOUDLY, because a bare array is not a legal equality comparand
303
+ * (objectui#8530 / objectui#8514): `convertFiltersToAST` THROWS
304
+ * `FilterOperatorError`, `driver-sql` answers `400 INVALID_FILTER`, and
305
+ * `ValueDataSource`'s matcher selects NOTHING. Against rows scoped
306
+ * `region = 'emea'`, drilling the `stage = 'won'` bucket returned `(none)`
307
+ * where the widget alone returned three rows and the conjunction returns the
308
+ * right two. So the widget's conditions were not merely lost — the drill was
309
+ * dead in the array arm. The failure is fail-CLOSED here, and this is the third
310
+ * site of the class objectui#8944 (chart) and objectui#9024 (pivot) removed.
311
+ *
312
+ * Three properties of THIS site, each confirmed rather than assumed — it is not
313
+ * a copy-paste of the other two:
314
+ *
315
+ * 1. The return stays a NON-optional `Record<string, unknown>`, because callers
316
+ * do not expect `undefined`. `composeDrillFilter` answers `undefined` when
317
+ * both sources are empty, so that one case is spelled `?? {}` — the same
318
+ * empty object this function already returned for a bucket with no dims.
319
+ * 2. Range operators SURVIVE. A time-bucketed dim contributes
320
+ * `{ $gte, $lt }`, and the composition re-spells it as two `$and` children
321
+ * on the same field (`[{d:{$gte}},{d:{$lt}}]`) rather than losing a bound;
322
+ * the selected rows are identical, and `serializeDrillFilterParams` walks
323
+ * `$and` (objectui#8944) so both bounds still reach `filter[d][gte|lt]`.
324
+ * 3. `runtimeFilter` is declared `unknown`, which is what the two consumers
325
+ * actually hand it — `any` from the dashboard, `Record<string, unknown>`
326
+ * from `DatasetReportRenderer`. Widening the PARAMETER is what admits both
327
+ * without a cast; ⛔ neither consumer's own type is changed by this.
328
+ *
329
+ * ⚠️ Conjunction changes one behaviour deliberately: a field named by BOTH the
330
+ * widget filter and the clicked bucket used to be overwritten by the bucket,
331
+ * and now both conditions apply. That is the point — a drill may narrow the
332
+ * widget's scope and may never widen it — and it is the posture the chart and
333
+ * pivot sites already ship.
334
+ *
335
+ * ## ⭐ Only the COMPOSING leg moves; the identity leg is left byte-identical
336
+ *
337
+ * The sink is asked only when there IS a second source. That is this site's
338
+ * one departure from `ObjectChart` / `ObjectPivotTable`, which call it
339
+ * unconditionally, and it is deliberate rather than a shortcut: the spread
340
+ * this replaces only ever ran when `runtimeFilter` was present, so the
341
+ * no-widget-filter path carries no defect to repair.
342
+ *
343
+ * ⚠️ Routing it anyway was MEASURED and rejected. `composeDrillFilter`'s own
344
+ * note — "a lone surviving source lowers back to exactly the flat object the
345
+ * spread produced" — holds only for a source of ONE condition; with two it
346
+ * lowers to `$and`, because `convertFiltersToAST` emits one node per condition
347
+ * and `parseFilterAST` cannot fold two nodes back onto one key. A `{ $gte,
348
+ * $lt }` range is two conditions on its own, so even a single date bucket
349
+ * re-spells. Sending the identity leg through the sink therefore re-shaped
350
+ * every multi-condition dataset drill — including the controls objectui#9085,
351
+ * objectui#4056, objectstack#5473 and objectstack-ai/objectstack#1752 pin on paths this card does not
352
+ * touch — for ZERO change in the rows selected or the `filter[...]` params
353
+ * emitted. ⛔ Do not "simplify" this to an unconditional call without redoing
354
+ * that measurement.
355
+ *
175
356
  * Shared by the dashboard `DatasetWidget` and the report renderer so a drill
176
357
  * filters identically (and correctly, including lookups) on both surfaces.
177
358
  */
178
- export declare function buildDatasetDrillFilter(rawRow: Record<string, unknown> | undefined, drillDims: string[], dimensionFields: Record<string, string>, runtimeFilter?: Record<string, unknown>, rawRanges?: Record<string, DatasetDrillRange>): Record<string, unknown>;
359
+ export declare function buildDatasetDrillFilter(rawRow: Record<string, unknown> | undefined, drillDims: string[], dimensionFields: Record<string, string>, runtimeFilter?: unknown, rawRanges?: Record<string, DatasetDrillRange>): Record<string, unknown>;