@objectstack/lint 17.2.0 → 17.4.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.
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { Manifest } from '@objectstack/sdui-parser';
2
1
  import ts from 'typescript';
2
+ import { Manifest } from '@objectstack/sdui-parser';
3
3
  import { SearchFieldMeta } from '@objectstack/spec/data';
4
4
  import { Options } from 'ajv';
5
5
  import { AccessMatrixParsed } from '@objectstack/spec/security';
6
- export { A as AUTHORING_COMMANDS, a as AUTHORING_RULES, b as AUTHORING_SURFACES, c as AuthoringCommand, d as AuthoringFinding, e as AuthoringRule, f as AuthoringRuleContext, g as AuthoringRuleInputTier, h as AuthoringRuleRun, i as AuthoringRuleTier, j as AuthoringSeverity, k as AuthoringSurface, E as EXPRESSION_INVALID, R as RuntimeGateResult, l as RuntimePackageScope, m as RuntimeStackContext, n as authoringRulesFor, o as buildRuntimeWriteSnapshots, p as narrowObjectsToPackageClosure, r as runAuthoringRules, q as runRuntimeAuthoringRules, s as runtimeAuthoringRulesFor, t as runtimeGatedTypes, u as splitBySeverity, v as stackKeyForType } from './runtime-cV-l_vtC.js';
6
+ export { A as AUTHORING_COMMANDS, a as AUTHORING_RULES, b as AUTHORING_SURFACES, c as AuthoringCommand, d as AuthoringFinding, e as AuthoringRule, f as AuthoringRuleContext, g as AuthoringRuleInputTier, h as AuthoringRuleRun, i as AuthoringRuleTier, j as AuthoringSeverity, k as AuthoringSurface, E as EXPRESSION_INVALID, R as RuntimeGateResult, l as RuntimePackageScope, m as RuntimeStackContext, n as authoringRulesFor, o as buildRuntimeWriteSnapshots, p as narrowObjectsToPackageClosure, r as runAuthoringRules, q as runRuntimeAuthoringRules, s as runtimeAuthoringRulesFor, t as runtimeGatedTypes, u as splitBySeverity, v as stackKeyForType } from './runtime-S3OZQDZV.js';
7
7
 
8
8
  /**
9
9
  * Build-time dashboard widget binding diagnostics (issues #1719, #1721).
@@ -23,13 +23,6 @@ export { A as AUTHORING_COMMANDS, a as AUTHORING_RULES, b as AUTHORING_SURFACES,
23
23
  * name on the bound dataset.
24
24
  * - `widget-measure-unknown` — a `values[]` entry is not a measure name on
25
25
  * the bound dataset.
26
- * - `chart-field-unknown` — a `chartConfig` binding names a field the query
27
- * result will not contain: `xAxis.field` must be one of the widget's
28
- * dimensions (or a dataset dimension), and each `yAxis[].field` /
29
- * `series[].name` must be one of the widget's selected measures
30
- * (`values`). Post-cutover (ADR-0021) the result rows are keyed by
31
- * measure NAME (e.g. `sum_amount`), not the base column (`amount`) — a
32
- * stale base-column reference renders the axis but an empty series.
33
26
  * - `widget-legacy-analytics-unrenderable` (#1878/#1894) — a widget uses the
34
27
  * removed pre-ADR-0021 inline-analytics shape (`categoryField`/`rowField`/…)
35
28
  * as its ONLY data wiring: no `dataset`, no `object`, no inline `data`. The
@@ -40,18 +33,68 @@ export { A as AUTHORING_COMMANDS, a as AUTHORING_RULES, b as AUTHORING_SURFACES,
40
33
  * - `dashboard-filter-field-unknown` (#3365) — a dashboard-level filter
41
34
  * (`dateRange` or a `globalFilters[]` entry) is wired into EVERY widget's
42
35
  * analytics query (#2501), but its EFFECTIVE field (after any `filterBindings`
43
- * re-target) does not exist on a bound widget's dataset object. The widget's
36
+ * re-target) does not resolve on a bound widget's dataset object. The widget's
44
37
  * query then references a non-existent column and crashes at render time
45
38
  * (`no such column …`) — a build-decidable invariant that previously escaped
46
39
  * the static gate and failed only when a user opened the dashboard. A widget
47
40
  * opts out with `filterBindings: { <name>: false }` or re-targets to a real
48
41
  * field. This is the same field-existence invariant ADR-0032 enforces for
49
42
  * CEL formula / sharing-rule references, applied to dashboard filter fields.
43
+ * Resolution is {@link resolveFieldPath}'s (#14275), so a DOTTED effective
44
+ * field (`account.signed_at`) is walked hop by hop rather than skipped, and a
45
+ * bare name is judged against the object's OWN injected columns rather than
46
+ * the object-independent `SYSTEM_FIELDS` union — see "What #14275 closed"
47
+ * below for why each half was a hole rather than a nicety.
48
+ * - `dashboard-filter-field-not-included` (#14275) — the effective field
49
+ * RESOLVES, but its relationship prefix is not declared in the bound
50
+ * dataset's `include`, so ADR-0021 compiles no join for it and the column is
51
+ * out of the broadcast query's reach. The `include` half of the same two
52
+ * clauses `widget-filter-field-not-included` applies to the widget's own
53
+ * `filter`, at the position one level up — a separate id because the fix is a
54
+ * different edit (declare the join, versus point the filter somewhere real)
55
+ * and because the family keeps one id per class.
50
56
  *
51
57
  * Advisory rules — severity `warning`, build stays green:
52
58
  *
53
- * - `chart-config-missing` a chart-type widget (bar/line/pie/…) has no
54
- * `chartConfig`, so the renderer cannot tell which measure to plot.
59
+ * - `chart-field-unknown` (#1721; tier ruled on #15463) a `chartConfig`
60
+ * BINDING key names something the widget's selection does not carry:
61
+ * `xAxis.field` is not one of the widget's dimensions (or a dataset
62
+ * dimension), or a `yAxis[].field` / `series[].name` is not one of its
63
+ * selected measures (`values`). Post-cutover (ADR-0021) the result rows are
64
+ * keyed by measure NAME (e.g. `sum_amount`), not the base column (`amount`),
65
+ * so a stale base-column reference is the usual way to write one. The pinned
66
+ * renderer REFUSES all three keys as bindings — see "The three refused
67
+ * binding keys" below — so the authored key changes nothing that renders: it
68
+ * is a silent no-op, exactly `widget-legacy-analytics-shape`'s class. Kept as
69
+ * a finding because the author wrote a binding and believes it is in force.
70
+ * - `chart-config-missing` — a `combo` widget has no `chartConfig`, so no
71
+ * series carries a mark and the combination chart draws as one uniform
72
+ * family. Narrowed to `combo` because `chartConfig` carries NO binding:
73
+ * `DatasetWidget` derives the axis and one series per measure from the
74
+ * widget's own `dimensions` / `values`, and actively REFUSES an authored
75
+ * `ChartAxis.field` / `ChartSeries.name`. See "What the renderer actually
76
+ * derives" below for the pinned contract this now mirrors.
77
+ * - `chart-measures-missing` — a chart-family widget selects NO measures
78
+ * (`values` empty or absent). The pinned renderer short-circuits to an
79
+ * authoring placeholder — *"Pick measures (values) for this dataset
80
+ * widget."* — before any query runs, so no chart is drawn at all. Warning
81
+ * rather than error because an empty selection is a legitimate
82
+ * work-in-progress state a build must tolerate; erroring would gate the
83
+ * `sys_metadata` publish path on a half-authored widget.
84
+ * - `widget-measures-missing` (#15508) — a NON-chart dataset widget — a
85
+ * single-value family (`metric`/`kpi`/`gauge`/`solid-gauge`/`bullet`) or a
86
+ * tabular one (`table`/`pivot`) — selects NO measures. The placeholder above
87
+ * is returned for these families too: the pinned `values.length === 0` return
88
+ * stands above every family branch, so the KPI number or the table is not
89
+ * drawn either. Same warning tier and suppression as the chart-family
90
+ * spelling; a separate id because "chart" does not name this condition and
91
+ * because the family keeps one id per class.
92
+ * - `chart-dimensions-missing` — a chart-family widget selects at least one
93
+ * measure but NO dimensions. The pinned renderer's
94
+ * `isMetric = METRIC_TYPES.has(widgetType) || dimensions.length === 0`
95
+ * routes it to the single-value branch, so it renders a KPI number and the
96
+ * family the author declared is silently ignored. Warning because the
97
+ * number is real and correct — it is the chart that is gone.
55
98
  * - `table-count-only` (#1719) — a `table`/`pivot` widget whose selected
56
99
  * measures are ALL `aggregate: 'count'` and which declares no
57
100
  * `dimensions` asks the analytics service for a single summary row. That
@@ -79,6 +122,261 @@ export { A as AUTHORING_COMMANDS, a as AUTHORING_RULES, b as AUTHORING_SURFACES,
79
122
  * of the same invariant, warned rather than errored because this pass cannot
80
123
  * see the remote schema (#8116's severity reasoning).
81
124
  *
125
+ * ── The widget's OWN two references (#14148) ─────────────────────────────────
126
+ *
127
+ * - `widget-filter-field-unknown` — a KEY of the widget's own `filter` resolves
128
+ * to no column on the bound dataset's object graph.
129
+ * - `widget-filter-field-not-included` — that key RESOLVES, but its relationship
130
+ * prefix is not declared in the dataset's `include`, so ADR-0021 compiles no
131
+ * join for it and the column is out of the query's reach.
132
+ * - `widget-sortby-unselected` — `options.sortBy` names neither a `dimensions[]`
133
+ * nor a `values[]` entry of this widget, so the ordering the author wrote
134
+ * cannot be applied to a result that will not contain that column.
135
+ *
136
+ * All three are `error`, and the reason is the reporting card's, quoted because
137
+ * it is the sharpest statement of it in the family: the dashboard it was
138
+ * measured on leads with a "not moving" tile — open work untouched >14 days —
139
+ * and *"an empty tile is indistinguishable from a healthy team: a missing
140
+ * number reads as zero, and zero is the answer the manager is hoping for."*
141
+ * The failure is not merely silent, it is silent in the direction the reader
142
+ * WANTS to believe, which is why it gates rather than advises.
143
+ *
144
+ * ### Why these two were the surviving holes
145
+ *
146
+ * On the very same node, the TOKEN was checked and the COLUMN was not:
147
+ * `filter-token-unknown` (#3574) fires path-precise at
148
+ * `…widgets[4].filter.due_date.$lte`, so the traversal already walked the
149
+ * filter tree and already knew the widget's dataset. And the identical
150
+ * resolution already existed one key over — `dashboard-filter-field-unknown`
151
+ * resolves a DASHBOARD-level filter's field against each widget's dataset base
152
+ * object. `widgets[].filter` is the same field-existence invariant on the
153
+ * filter an author is MORE likely to write by hand, and it was simply never fed
154
+ * through it. `options.sortBy` is the declared-≠-enforced half:
155
+ * `DashboardWidgetOptionsSchema.sortBy` states its own contract in prose —
156
+ * *"must be one this widget actually selects"* — and nothing enforced it.
157
+ *
158
+ * ### The `include` clause, and why a dotted path is RESOLVED here
159
+ *
160
+ * A widget's `filter` is ANDed into the dataset query as `runtimeFilter`
161
+ * (`DashboardWidgetSchema.filter`; `dataset-executor.ts` `combineFilters(
162
+ * compiled.filter, selection.runtimeFilter)`), and that compiled query carries
163
+ * ONLY the joins the dataset's `include` declared (`dataset-compiler.ts`: joins
164
+ * are derived from `include`, and `assertDeclared` refuses an undeclared
165
+ * relationship path). So a dotted key here is judged on the same two clauses
166
+ * `validate-dataset-references.ts` applies one level down — existence, then
167
+ * joinability — rather than skipped. The runtime is NOT a backstop for it
168
+ * either — `assertDeclared` runs over `dimensions` and `measures` only, never
169
+ * over `runtimeFilter` — so nothing between the author and the empty tile asks
170
+ * this question.
171
+ *
172
+ * ### What #14275 closed, one position up
173
+ *
174
+ * `dashboard-filter-field-unknown` (a1) shipped with the two limitations the
175
+ * paragraph above used to record as deliberate, and #14275 closed both by
176
+ * migrating that branch onto this same seam:
177
+ *
178
+ * 1. it SKIPPED every dotted effective field (`if (field.includes('.'))
179
+ * continue;`), which was accurate when nothing in this package could walk
180
+ * hops and false once {@link resolveFieldPath} landed. The consequence was
181
+ * the sharper one: a dashboard filter is broadcast to EVERY widget on the
182
+ * board, so a single unjudged `filterBindings: { dateRange:
183
+ * 'account.signed_at' }` degraded the whole dashboard rather than one tile
184
+ * — unjudged whether or not `account` existed, whether or not `signed_at`
185
+ * existed on it, and whether or not `account` was in the dataset's
186
+ * `include`;
187
+ * 2. it resolved bare names against the object-independent `SYSTEM_FIELDS`
188
+ * union, which answers "could this be a system column ANYWHERE". The two
189
+ * differ exactly where it matters: on `ownership: 'none'` the platform
190
+ * injects no `owner_id`, so the union answered a real defect as
191
+ * resolvable. Both positions now take skip 3 per object, through
192
+ * {@link resolveFieldPath}'s use of `injectedColumnsFor`.
193
+ *
194
+ * The #8340 provenance question rides on the migrated branch unchanged, and is
195
+ * now GATED by the verdict's `injected` marker rather than by union membership:
196
+ * a leaf that resolved because it is injected is the only leaf whose anchor can
197
+ * be unprovisioned (an author-DECLARED column of the same name is the author's,
198
+ * #7859), and the marker states that fact where the flat union could not. It
199
+ * also travels: the anchor is looked up on the object the LEAF landed on, so a
200
+ * dotted filter path ending on an ADR-0015 `external` object is answered too.
201
+ *
202
+ * Resolution is {@link resolveFieldPath}'s and its `unknowable` verdicts are
203
+ * never reported (ADR-0072 D1), so the three skips every field-existence rule
204
+ * in this package takes apply unchanged: an object this stack does not define,
205
+ * an object with no readable field map (ADR-0015 `external`), and a
206
+ * registry-injected system column — the last resolved PER OBJECT rather than
207
+ * through the flat `SYSTEM_FIELDS` union, which is what lets a reference to
208
+ * `owner_id` on an `ownership: 'none'` object stay a real finding.
209
+ *
210
+ * ### What the renderer actually derives (#14436)
211
+ *
212
+ * `chart-config-missing` used to fire on EVERY chart family that was not a
213
+ * single-value or tabular type, on the stated grounds that without
214
+ * `chartConfig` "the renderer cannot determine which measure to plot, so the
215
+ * series renders empty". That consequence was false, and it fired on this
216
+ * platform's OWN shipped metadata: the `system_overview` dashboard draws a pie
217
+ * and a bar, each with one dimension and one measure and no `chartConfig`, and
218
+ * both render correctly. A warning-tier rule that mis-fires on first-party
219
+ * metadata is the ADR-0072 D1 cost the whole family exists to avoid — it
220
+ * teaches every reader, human and agent, that this family is noise.
221
+ *
222
+ * The renderer's contract, read at the `@object-ui` revision this repo PINS
223
+ * (`.objectui-sha`), not at objectui's `origin/main` — the Console ships the
224
+ * pin, so the exemption is written against the behaviour the pin has:
225
+ *
226
+ * - `DatasetWidget` calls `buildChartSeries(rows, dimensions, values, …)`,
227
+ * which returns `{ data, xAxisKey, series }` — the x-axis key is
228
+ * `dimensions[0]` and there is exactly one series per entry of `values`.
229
+ * `chartConfig` is not an argument to it.
230
+ * - The authored `chartConfig` reaches the chart only through
231
+ * `mergeAuthoredPresentation` (per-series/axis PRESENTATION merged ONTO
232
+ * those derived bindings) and `chartConfigPresentation` (chrome: titles,
233
+ * height, colours, data labels, annotations, legend).
234
+ * - The BINDING half of an authored `chartConfig` is refused outright. The
235
+ * renderer pins this by name in
236
+ * `packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.test.tsx`:
237
+ * *"ignores an authored axis `field` and keeps the derived axis binding"*,
238
+ * *"ignores an authored series and keeps one derived series per measure"*,
239
+ * *"ignores `chartConfig.type` — the widget type owns the chart family"*,
240
+ * and *"emits none of the presentation keys when no chartConfig is
241
+ * declared"*.
242
+ *
243
+ * So for every chart family except one, a missing `chartConfig` costs the
244
+ * widget nothing at all, and ADDING one could not have repaired a widget whose
245
+ * selection is empty either — `chart-field-unknown` above reports a
246
+ * `yAxis[].field` that names anything the widget did not select, so
247
+ * `chartConfig` can never supply a measure the `values` array is missing.
248
+ *
249
+ * **The one surviving arm is `combo`**, and it is a real loss rather than an
250
+ * inferred one. A combination chart's whole identity is a per-series MARK, and
251
+ * that mark is authored as `chartConfig.series[].type` — presentation, so it
252
+ * does merge forward. `mergeAuthoredSeries` states the default it falls back
253
+ * to: *"a derived series with no authored entry keeps the family default"*,
254
+ * and `DatasetWidget.comboPresentation.test.tsx` records what that cost when
255
+ * the merge was missing — *"Without it the line measure drew as the second
256
+ * bar."* A `combo` with no `chartConfig` therefore draws every measure with
257
+ * one mark: the author asked for a combination and got a plain one. Warning
258
+ * rather than error, because the numbers are right and the chart renders — it
259
+ * is the shape that is wrong.
260
+ *
261
+ * ⚠️ Two genuinely un-renderable shapes are NOT this id's, because neither is
262
+ * caused by nor repairable with `chartConfig` and folding them in would leave
263
+ * this id misnaming its own condition: a chart-family widget selecting NO
264
+ * measures, and one selecting NO dimensions. They have their own ids —
265
+ * `chart-measures-missing` and `chart-dimensions-missing` — described next.
266
+ *
267
+ * ### The two empty-selection shapes (#15462)
268
+ *
269
+ * Read at the same PINNED `@object-ui` revision (`.objectui-sha`), in
270
+ * `packages/plugin-dashboard/src/DatasetWidget.tsx`:
271
+ *
272
+ * - `:683` — `if (values.length === 0)` returns the authoring placeholder
273
+ * `tt('dashboard.pickMeasures', 'Pick measures (values) for this dataset
274
+ * widget.')`. It stands ABOVE every family branch, so a widget selecting no
275
+ * measure never reaches a chart, a table or a KPI number: nothing is drawn.
276
+ * - `:423` — `const isMetric = METRIC_TYPES.has(widgetType) ||
277
+ * dimensions.length === 0;`, with `METRIC_TYPES` (`:343`) = `metric`, `kpi`,
278
+ * `gauge`, `solid-gauge`, `bullet`; `:424` is the tabular test
279
+ * (`table`/`pivot`); and the render branches (`:702`, `:798`, `:854`) route
280
+ * `isMetric ? KPI : isTable ? table : chart`. A dimensionless `bar` is
281
+ * therefore drawn as a single KPI number — the failure direction the family's
282
+ * own docblock names, except worse: the number is REAL, so the missing chart
283
+ * reads as a design choice rather than as a defect.
284
+ *
285
+ * So "chart family" here is not a hand list — it is what that routing leaves
286
+ * over: a declared `ChartTypeSchema` option that is neither a `METRIC_TYPES`
287
+ * member nor tabular ({@link CHART_FAMILY_WIDGET_TYPES}). The taxonomy supplies
288
+ * the universe and the renderer decides the exceptions, which is the same
289
+ * division #14436 settled for `MARK_MIXING_CHART_TYPES`: this file's copies of
290
+ * the renderer's two sets are held to `ChartTypeSchema` by the rule's tests, so
291
+ * a member that stops being a declared chart type reds instead of going quiet.
292
+ *
293
+ * The two ids never both fire on one widget, and the reason is the pin's own
294
+ * order: with no measures the placeholder returns at `:683` and the `isMetric`
295
+ * branch is never reached, so `chart-dimensions-missing`'s consequence (a KPI
296
+ * number in place of the chart) is not what that widget does. A widget missing
297
+ * both reports `chart-measures-missing` alone, whose hint names the missing
298
+ * dimension too.
299
+ *
300
+ * Both are warnings, per the tier decision on #15462: an empty selection is a
301
+ * state an author passes THROUGH, and the family's errors are reserved for
302
+ * bindings the analytics service cannot satisfy.
303
+ *
304
+ * **The measures shape is every family's, not the chart family's (#15508).**
305
+ * Re-read at the pinned `.objectui-sha` (`a472b0716`, where the four cited
306
+ * lines still read as quoted above), `:683` is ABOVE `:423`/`:424` and tests
307
+ * nothing but `values.length === 0`. So a `metric`, `kpi`, `gauge`,
308
+ * `solid-gauge`, `bullet`, `table` or `pivot` widget that selects no measures
309
+ * renders that same placeholder: the KPI number or the table the author
310
+ * declared is not drawn either, and a missing tile on a board reads as
311
+ * "nothing to report" rather than as a defect. Nothing else reported it —
312
+ * `table-count-only` requires `values.length > 0` before it looks, and rules
313
+ * (b)/(c) iterate the arrays, so an empty one is silent by construction.
314
+ *
315
+ * The population therefore widens to every DECLARED widget type
316
+ * ({@link NON_CHART_DATASET_WIDGET_TYPES} is the taxonomy minus the chart
317
+ * family), carried by TWO ids rather than one:
318
+ * {@link CHART_MEASURES_MISSING} keeps the chart family, and
319
+ * {@link WIDGET_MEASURES_MISSING} takes the rest. Two, because "chart" stops
320
+ * naming the condition once the population is every family — a `metric` tile
321
+ * is not a chart — while the old id is depended on from OUTSIDE this file (the
322
+ * `packages/lint` barrel re-exports it, which
323
+ * `rule-id-barrel-exports.test.ts` makes a public-surface contract, and a
324
+ * board may already carry `suppressWarnings: ['chart-measures-missing']`).
325
+ * Renaming would have retired a reachable id and falsified those references;
326
+ * splitting keeps every one of them true and lets each message state the
327
+ * consequence its family actually has (no chart drawn / no KPI number drawn /
328
+ * no table rendered), which one id could only do by branching anyway.
329
+ *
330
+ * The DIMENSIONS shape stays chart-family only, and deliberately: a
331
+ * dimensionless `metric` or `table` is what those families are FOR (the
332
+ * shipped `system_overview` KPI tiles are exactly that shape), and a
333
+ * dimensionless `table` already has its own id in `table-count-only`.
334
+ *
335
+ * ### The three refused binding keys (#15463)
336
+ *
337
+ * `chart-field-unknown` shipped at `error` with a message that named a QUERY
338
+ * failure — *"the query result will not contain it"*. Read at the same PINNED
339
+ * `@object-ui` revision (`.objectui-sha`), that consequence never happens,
340
+ * because the renderer never reads these keys as bindings at all. The rule id
341
+ * covers exactly three positions, and all three are refused:
342
+ *
343
+ * - `chartConfig.xAxis.field` — `axisPresentation`
344
+ * (`@object-ui/core` `src/utils/chart-presentation.ts`) builds the axis's
345
+ * presentation MINUS its `field`, and that dropping is structural, not a
346
+ * guard: the x-axis key is `buildChartSeries`' `xAxisKey`, i.e. the widget's
347
+ * `dimensions[0]`. An authored `field` re-points nothing.
348
+ * - `chartConfig.yAxis[].field` — the same `axisPresentation` call, per entry.
349
+ * The entry keeps its SLOT (the count is what turns on a secondary axis) and
350
+ * its scale/chrome; only the binding is dropped.
351
+ * - `chartConfig.series[].name` — `mergeAuthoredSeries` pairs an authored
352
+ * entry with the derived binding whose `dataKey` it EQUALS, one series per
353
+ * entry of `values`. A name matching no derived series is *"**ignored** —
354
+ * membership belongs to the dataset, so an author cannot add, remove or
355
+ * re-point a series from the chart config"*. So the presentation the author
356
+ * hung on that entry — the mark, the colour, the stack, the axis side —
357
+ * lands on nothing.
358
+ *
359
+ * The renderer pins all three by name in
360
+ * `packages/plugin-dashboard/src/__tests__/DatasetWidget.chartConfig.test.tsx`
361
+ * (*"ignores an authored axis `field` and keeps the derived axis binding"*,
362
+ * *"ignores an authored series and keeps one derived series per measure"*).
363
+ *
364
+ * So the failure is not a broken page, it is an ignored key — which is the
365
+ * class `widget-legacy-analytics-shape` above reports at WARNING tier in this
366
+ * same file (*"the dashboard renderer ignores them … a silent no-op"*). All
367
+ * three positions therefore drop from `error` to `warning`, suppressible per
368
+ * widget, and each message states what actually happens instead of a query that
369
+ * never runs. The finding is KEPT rather than deleted: unlike #14436's
370
+ * over-reach the metadata really is wrong — the author wrote a binding and
371
+ * believes it is in force.
372
+ *
373
+ * The tier drop is a behaviour change on the `sys_metadata` publish door: the
374
+ * 2026-08-15 ruling put all SIX of this rule's error ids on that door as one
375
+ * "this board cannot render" class, and this id leaves that set, so a publish
376
+ * carrying only a refused `chartConfig` binding key now SUCCEEDS with the
377
+ * finding on the advisory channel. The remaining five are unchanged; the
378
+ * accept-set is re-pinned in `runtime-gate.test.ts`.
379
+ *
82
380
  * Warnings can be deliberately suppressed per widget via
83
381
  * `suppressWarnings: ['<rule-id>']`; errors cannot — they describe a
84
382
  * binding the analytics service cannot satisfy.
@@ -88,12 +386,44 @@ declare const WIDGET_DIMENSION_UNKNOWN = "widget-dimension-unknown";
88
386
  declare const WIDGET_MEASURE_UNKNOWN = "widget-measure-unknown";
89
387
  declare const CHART_FIELD_UNKNOWN = "chart-field-unknown";
90
388
  declare const CHART_CONFIG_MISSING = "chart-config-missing";
389
+ /**
390
+ * [#15462] A chart-family widget selects no measures, so the pinned renderer
391
+ * draws the "Pick measures (values)" placeholder instead of a chart.
392
+ */
393
+ declare const CHART_MEASURES_MISSING = "chart-measures-missing";
394
+ /**
395
+ * [#15462] A chart-family widget selects no dimensions, so the pinned
396
+ * renderer's `isMetric` branch draws a KPI number instead of the declared
397
+ * chart family.
398
+ */
399
+ declare const CHART_DIMENSIONS_MISSING = "chart-dimensions-missing";
400
+ /**
401
+ * [#15508] A NON-chart dataset widget — a `METRIC_WIDGET_TYPES` or a
402
+ * `TABULAR_WIDGET_TYPES` one — selects no measures, so the pinned renderer
403
+ * draws the same "Pick measures (values)" placeholder in place of the KPI
404
+ * number or the table. The chart-family spelling of the same shape keeps its
405
+ * own id ({@link CHART_MEASURES_MISSING}); see "The two empty-selection
406
+ * shapes" in the module docblock for why the population is split across two
407
+ * ids rather than carried by one.
408
+ */
409
+ declare const WIDGET_MEASURES_MISSING = "widget-measures-missing";
91
410
  declare const TABLE_COUNT_ONLY = "table-count-only";
92
411
  declare const MEASURE_AGGREGATE_INCOHERENT = "measure-aggregate-incoherent";
93
412
  declare const WIDGET_LEGACY_ANALYTICS_SHAPE = "widget-legacy-analytics-shape";
94
413
  declare const WIDGET_LEGACY_ANALYTICS_UNRENDERABLE = "widget-legacy-analytics-unrenderable";
95
414
  declare const DASHBOARD_FILTER_FIELD_UNKNOWN = "dashboard-filter-field-unknown";
96
415
  declare const DASHBOARD_FILTER_FIELD_UNPROVISIONED = "dashboard-filter-field-unprovisioned";
416
+ /**
417
+ * [#14275] A dashboard filter's effective field resolves, but its relationship
418
+ * prefix is not declared in the bound dataset's `include`.
419
+ */
420
+ declare const DASHBOARD_FILTER_FIELD_NOT_INCLUDED = "dashboard-filter-field-not-included";
421
+ /** [#14148] A key of the widget's OWN `filter` that resolves to no column. */
422
+ declare const WIDGET_FILTER_FIELD_UNKNOWN = "widget-filter-field-unknown";
423
+ /** [#14148] A widget filter key whose relationship prefix is not in `include`. */
424
+ declare const WIDGET_FILTER_FIELD_NOT_INCLUDED = "widget-filter-field-not-included";
425
+ /** [#14148] `options.sortBy` names nothing this widget selects. */
426
+ declare const WIDGET_SORTBY_UNSELECTED = "widget-sortby-unselected";
97
427
  type WidgetBindingSeverity = 'error' | 'warning';
98
428
  interface WidgetBindingFinding {
99
429
  /** `error` = unresolvable binding (broken page); `warning` = advisory. */
@@ -109,7 +439,7 @@ interface WidgetBindingFinding {
109
439
  /** How to fix (or deliberately suppress) it. */
110
440
  hint: string;
111
441
  }
112
- type AnyRec$G = Record<string, unknown>;
442
+ type AnyRec$N = Record<string, unknown>;
113
443
  /**
114
444
  * Validate every dashboard widget's dataset binding. Returns the list of
115
445
  * findings (empty = clean). Caller decides how to surface them: `error`
@@ -117,7 +447,7 @@ type AnyRec$G = Record<string, unknown>;
117
447
  * should fail validate/build; `warning` findings are advisory and must
118
448
  * never fail the build on their own.
119
449
  */
120
- declare function validateWidgetBindings(stack: AnyRec$G): WidgetBindingFinding[];
450
+ declare function validateWidgetBindings(stack: AnyRec$N): WidgetBindingFinding[];
121
451
 
122
452
  interface ExprIssue {
123
453
  where: string;
@@ -130,12 +460,293 @@ interface ExprIssue {
130
460
  */
131
461
  severity?: 'error' | 'warning';
132
462
  }
133
- type AnyRec$F = Record<string, unknown>;
463
+ type AnyRec$M = Record<string, unknown>;
464
+ /**
465
+ * A FIELD-level conditional rule (`visibleWhen` / `readonlyWhen` /
466
+ * `requiredWhen`) that reaches for a namespace root the field level does not
467
+ * bind. The field level binds THREE roots and nothing else — `record`,
468
+ * `previous`, and `parent` on a master-detail line item — measured at three
469
+ * independent ends: the server (`rule-validator.ts` binds
470
+ * `{ record, previous, extra: { parent } }` for `readonlyWhen` and
471
+ * `{ record, previous, ...parentScope }` for `requiredWhen`), the client
472
+ * (`evalFieldPredicate` binds `record` + `previous` + a caller `scope` that
473
+ * is only ever `{ parent }` across objectui's five field-level call sites),
474
+ * and the authoring surface (objectui's `FIELD_RULE_ROOTS`, whose comment
475
+ * says "nothing else").
476
+ *
477
+ * ## Why this is a rule of its own rather than a missing root
478
+ *
479
+ * Until #6290 the same rejection fell out of `@objectstack/formula`'s
480
+ * `SCOPE_ROOTS` not listing `current_user` — a global baseline, doing a
481
+ * per-surface job by accident. Two things were wrong with that:
482
+ *
483
+ * 1. The rest of the package said the opposite. `introspectScope` hands
484
+ * `current_user` to authors as a legal root and `checkRoleCatalog`'s four
485
+ * position-membership regexes all lead with it, because ADR-0068 D1 makes
486
+ * it THE canonical spelling and `buildScope` really does mount it. One
487
+ * package, two accounts of one root.
488
+ * 2. The diagnostic was the generic bare-field one, so it prescribed
489
+ * "Write `record.current_user`" — a shape that binds on NO layer. An
490
+ * author who followed it wrote something strictly worse than what they
491
+ * started with, and the failure stayed silent.
492
+ *
493
+ * So the baseline now declares the root (a reference through it never faults
494
+ * platform-wide) and the surface that genuinely cannot bind it says so here,
495
+ * in its own words, with the prescriptions that actually exist.
496
+ *
497
+ * ## Why the membership test is an ALLOWLIST (#6713)
498
+ *
499
+ * Until #6713 this rule matched a hand-written DENYLIST — one root in #6584,
500
+ * three after #6585 (`current_user` / `user` / `ctx`). The denylist was never
501
+ * the truth: the three anchors above pin a three-item ALLOWLIST, and
502
+ * everything outside it is equally unbound, faults identically, and — this is
503
+ * the part that made the hole invisible — is equally SILENT. A root in
504
+ * `SCOPE_ROOTS` resolves in the strict env, so the bare-reference check one
505
+ * line up never fires on it either. #6713 measured 21 roots living in that
506
+ * gap (`input`, `output`, `os`, `vars`, `variables`, `automation`, `context`,
507
+ * `args`, `item`, `env`, `step`, `result`, `trigger`, `event`, `payload`,
508
+ * `data`, `params`, `config`, `settings`, `features`, `current`), two of them
509
+ * highly credible author typos rather than theoretical members:
510
+ *
511
+ * - `os.user.id` — ADR-0068 D1's FOURTH user spelling. #6585 took three and
512
+ * left this one, so the same semantic error stayed silent under one of the
513
+ * four names the platform itself mounts the user object under.
514
+ * - `data.status == 'x'` — `data` is the LEGAL root of this very
515
+ * `visibleWhen` key on a METADATA form (`view.zod.ts`: "Root: `record` …
516
+ * in runtime forms, or `data` in metadata forms"). Two form kinds, one key
517
+ * name, different roots — and the repo's own `*.form.ts` files are full of
518
+ * the `data` spelling for an author to copy.
519
+ *
520
+ * A denylist cannot track `SCOPE_ROOTS`: every root added there is unreported
521
+ * here until somebody remembers to copy it across (`current_user` itself
522
+ * arrived in #6290 and needed #6584 to be noticed). The allowlist inverts the
523
+ * maintenance burden onto the three roots that are pinned by three anchors and
524
+ * change only when the evaluators do.
525
+ *
526
+ * The membership test is {@link FIELD_RULE_JUDGED_ROOTS} minus the allowlist.
527
+ * Its bulk is `SCOPE_ROOTS`, taken from `@objectstack/formula` rather than
528
+ * restated here (#6713 published it for this consumer) — one list, one
529
+ * definition, no drift — plus the ambient roots #13935 measured outside that
530
+ * baseline; see "Why the judged vocabulary is WIDER than `SCOPE_ROOTS`" below.
531
+ * It deliberately is NOT `firstUndeclaredReference`, the declaredness oracle
532
+ * the sibling visibility rule uses, and the difference is a measured false
533
+ * positive rather than a preference: the strict env also declares CEL's TYPE
534
+ * names, so `type(record.x) == string` reports `string` as a root that
535
+ * "resolves". Judging by declaredness would reject that legitimate predicate;
536
+ * judging by membership does not. Everything the oracle owns and this list
537
+ * does not — bare field references, comprehension-macro variables — keeps
538
+ * falling to the bare-reference check, which has the right prescription for
539
+ * it. The two partitions are disjoint and there is no gap between them.
540
+ *
541
+ * ⚠️ That disjointness used to hold for FREE and no longer does. While every
542
+ * judged root was a `SCOPE_ROOTS` member it was declared in the strict env, so
543
+ * the bare-reference check could not fire on it whatever this rule decided.
544
+ * An AMBIENT root is undeclared there, so both checks see it — the walk
545
+ * suppresses the bare-reference verdict for a root this rule has claimed
546
+ * (`claimedRoot` on the `check` closure) to keep the invariant true by
547
+ * construction instead of by coincidence.
548
+ *
549
+ * ## Why it is an error and not a warning
550
+ *
551
+ * Every fault direction available here is silent, and two of the three are
552
+ * the opposite of what the author declared (see the per-slot table below):
553
+ * a `visibleWhen` written to HIDE leaves the field visible to everyone, a
554
+ * `readonlyWhen` written to unlock-under-a-condition locks the field on every
555
+ * write, and a `requiredWhen` simply never fires. None of the three produces
556
+ * a runtime error an author can find; the only signal that exists is this one
557
+ * (#6146).
558
+ *
559
+ * Verdict scope is the field level only. Per-option `visibleWhen` is checked
560
+ * by the loop in the field walk and deliberately NOT passed through here:
561
+ * options resolve against the host's predicate scope, which binds
562
+ * `current_user` (ADR-0068 / objectui#2284) — that surface is where such a
563
+ * predicate belongs, which is why it is also the first prescription below.
564
+ *
565
+ * ## The user roots, and why `ctx` is judged whole-root (#6585)
566
+ *
567
+ * ADR-0068 D1 makes `user` and `ctx.user` ALIASES of `current_user` — one
568
+ * `EvalUser` object under every spelling (`buildScope` in
569
+ * `formula/stdlib.ts` hangs the same reference on `current_user` / `user` /
570
+ * `ctx.user` / `os.user`). Matching only the canonical spelling meant the
571
+ * identical semantic error got a diagnostic under `current_user` and total
572
+ * silence under the aliases — which spelling the author picked decided
573
+ * whether they got the diagnostic, the exact fork AI authors cannot
574
+ * self-check. All four roots now share one verdict and one prescription;
575
+ * the message names the spelling found, nothing else varies. (`os` joined the
576
+ * set in #6713 — it is the fourth name `buildScope` mounts the same object
577
+ * under, and the allowlist would have rejected it anyway; what the user tier
578
+ * decides is only WHICH prescription it gets, and the user one is right for
579
+ * `os.user`. `os.org` / `os.env` land in the same tier because the option
580
+ * surface named by the first prescription binds the whole `os` namespace, not
581
+ * only its `user` member.)
582
+ *
583
+ * `ctx` is judged as a WHOLE root, not only in `ctx.user` form, because at
584
+ * this surface that is simply what is true: `buildScope` creates the `ctx`
585
+ * root ONLY when the evaluation carries a user (`scope.ctx = { user }`
586
+ * inside `if (ctx.user !== undefined)`), and no field-level site passes one
587
+ * — the server binds `record`+`previous`(+`parent`) (`rule-validator.ts`
588
+ * `readonlyWhenBindings` / the `requiredWhen` block) and the client's
589
+ * `evalFieldPredicate` binds `record`+`previous`+caller `scope` (only ever
590
+ * `{ parent }`). So `ctx.locale` faults exactly like `ctx.user.id` here.
591
+ * `ctx` IS ActionEngine's predicate root elsewhere — the platform's real
592
+ * `ctx.user` predicates all sit on action `visible` (`sys-user.object.ts`,
593
+ * `sys-invitation.object.ts`), a surface this helper never reads — and
594
+ * measured usage of field-level `*When` with ANY user root is zero across
595
+ * examples/, packages/ and objectui (#6585's sweep).
596
+ *
597
+ * The rejected alternative was matching `ctx` only in its `ctx.user` form.
598
+ * `collectCelRootIdentifiers` reports ROOTS and drops member names by
599
+ * design, so that reading needs a source-level spelling match — and a
600
+ * spelling match is precisely the defect this rule exists to remove: it
601
+ * would re-open the same fork one level down (`ctx["user"].id` silent,
602
+ * `ctx.user.id` rejected), while leaving a real fail-open fault (`ctx.locale`)
603
+ * unreported for the sake of a narrower rule NAME.
604
+ *
605
+ * ## The message tiers on TWO orthogonal axes
606
+ *
607
+ * The causal half tiers by SLOT (#6716); the prescription half tiers by ROOT
608
+ * (#6713). They are independent — `data` on a `readonlyWhen` needs the
609
+ * metadata-form prescription AND the locked-field consequence — so one
610
+ * skeleton carries both rather than a message per combination.
611
+ *
612
+ * ### Axis 1 — the consequence, by SLOT (#6716, all three cells measured)
613
+ *
614
+ * Until #6713/#6716 all three slots shared ONE sentence: "the predicate
615
+ * faults and falls back to VISIBLE, leaving the field the test was meant to
616
+ * hide showing for everyone". That is precise for exactly one of them. The
617
+ * measurement, at both ends of each slot:
618
+ *
619
+ * - **`visibleWhen` — client-only, and since objectui#6010 it fails in TWO
620
+ * ways rather than one.** The server half is unchanged and still exactly
621
+ * right: it never evaluates a FIELD-level `visibleWhen` at all —
622
+ * `rule-validator.ts`'s `ConditionalFieldDef` has no such member, and
623
+ * `hasFieldRules` gates on `requiredWhen || readonlyWhen ||
624
+ * fieldHasOptionVisibility` — the `visibleWhen` it does evaluate is the
625
+ * per-OPTION one. The RENDERER half moved. `plugin-form`'s
626
+ * `sectionFields.ts` (`fromObjectSchema`) copies an object field's ADR-0036
627
+ * `visibleWhen` / `readonlyWhen` / `requiredWhen` straight onto the runtime
628
+ * form field, and the SDUI form renderer resolves those through
629
+ * `resolveFieldRuleState` **with `predicateScope` bound**. So under a host
630
+ * that publishes a scope the predicate does not fault: it RESOLVES, the
631
+ * control is hidden in that one form, and the server still returns the value
632
+ * to every other reader — a silent enforcement gap. Only where no host
633
+ * publishes a scope (the public `/f/:slug` route, every non-form reader)
634
+ * does the old sentence still describe what happens: unbound root, fault,
635
+ * `fallback: true`, field visible to everyone.
636
+ *
637
+ * ⛔ This is not a reason to relax {@link FIELD_RULE_BOUND_ROOTS}. The
638
+ * verdict is MORE justified than when it was written, not less: trading a
639
+ * loud lint error for a gap nobody can see is the one outcome worse than
640
+ * the error. Only the causal clause was re-measured.
641
+ * - **`readonlyWhen` — the two ends fault in OPPOSITE directions, and the
642
+ * server wins.** Server: `isReadonlyWhenLocked` matches the fault with
643
+ * `unknownVariableOf` and returns `true` — "the declared lock is not
644
+ * waived because it could not be evaluated" (#4889's carve-out, whose
645
+ * trigger is precisely the unbound-ROOT case, not the undeclared-key one)
646
+ * — and `stripReadonlyWhenFields` then DELETES the field from the incoming
647
+ * payload and lets the rest of the write through. Client:
648
+ * `resolveFieldRuleState` passes `fallback: false`, so the form renders the
649
+ * field editable. The standing "server enforces, client is courtesy" rule
650
+ * — ADR-0124 D1 — resolves the disagreement: the author edits
651
+ * the field, the save reports
652
+ * success, and the value silently never lands. The old sentence told this
653
+ * author the field would be VISIBLE TO EVERYONE — the opposite failure, and
654
+ * the opposite troubleshooting direction.
655
+ * - **`requiredWhen` — fail-OPEN at both ends, and never about visibility.**
656
+ * Server: the `requiredWhen` block logs `unknownVariableOf`'s name and
657
+ * `continue`s — #4977 deliberately did not copy #4889's carve-out, so the
658
+ * required-check is skipped for that write. Client: `fallback: false`, so
659
+ * the form does not mark the field required either. Both ends agree and
660
+ * both do nothing: the requirement is never enforced anywhere, and a record
661
+ * saves with the field empty. "Falls back to VISIBLE" was not merely
662
+ * imprecise here, it named the wrong property of the field.
663
+ *
664
+ * `conditionalRequired` also reaches this helper (the field walk still passes
665
+ * it). It is a `retiredKey` in `FieldSchema` — the strict schema rejects it by
666
+ * name — so the branch is inert on the parsed compile path, and it gets a
667
+ * slot-agnostic clause rather than a fabricated fourth measurement.
668
+ *
669
+ * ### Axis 2 — the prescription, by ROOT (#6713)
670
+ *
671
+ * The pre-#6713 prescriptions are user-oriented (move to the option level,
672
+ * declare permission-set FLS) because the pre-#6713 denylist held only user
673
+ * roots. Handed to an author who wrote `data.type == 'select'`, "move it to
674
+ * the option's `visibleWhen`" answers a question nobody asked. Three tiers:
675
+ *
676
+ * - **user roots** (`current_user` / `user` / `ctx` / `os`) keep the existing
677
+ * two user-specific prescriptions plus the `record` rewrite. What changed is
678
+ * the GROUNDING of the first one, not the prescription: it used to read
679
+ * "per-option is the one `*When` surface that binds `current_user`", and
680
+ * since objectui#6010 a form VIEW's field predicate binds those roots too —
681
+ * client-side only, because nothing on the write path evaluates a form-view
682
+ * field `visibleWhen`. So per-option is recommended for the reason that
683
+ * survives the change: the rule validator enforces it on write. The message
684
+ * now says that, and names the view surface only to refuse it as a
685
+ * destination for a server-enforced object rule (trading a loud lint error
686
+ * for a silent enforcement gap is the one outcome worse than the error);
687
+ * - **`data`** gets the metadata-form-vs-runtime-form explanation, because
688
+ * that is what the mistake IS — the same key name, the other form kind's
689
+ * root;
690
+ * - **`app` and the other AMBIENT roots** get the renderer-mounted
691
+ * explanation and an explicit refusal of `record.<root>` (#13935, below);
692
+ * - **everything else** gets the general rewrite, phrased without claiming
693
+ * which other surface the author copied it from.
694
+ *
695
+ * ## Why the judged vocabulary is WIDER than `SCOPE_ROOTS` (#13935)
696
+ *
697
+ * `SCOPE_ROOTS` was the membership test until #13935, and it is the wrong
698
+ * question by one word: it answers "is this root declared PLATFORM-WIDE",
699
+ * while the rule needs "is this root bound at SOME evaluation site". The two
700
+ * agreed for 27 roots and then disagreed for `app` — bound by objectui's
701
+ * `ExpressionProvider` on the very surface an author migrates a rule DOWN
702
+ * from, absent from the baseline. Falling outside the membership test sent it
703
+ * to the bare-reference check, whose prescription is "Write `record.app`" —
704
+ * and following that earns ``unknown field `app` `` from the field-existence
705
+ * pass one line up. A first diagnostic that is actively false about where the
706
+ * root binds, and a wasted correction cycle.
707
+ *
708
+ * `SCOPE_ROOTS`' own docblock made this measurable rather than a matter of
709
+ * taste: its `current_user` entry claims to be "the last one this list was
710
+ * missing (#6290)". `app` is that sentence's second counterexample — the same
711
+ * mechanism (#6713's point: a hand-maintained list doing a per-surface job
712
+ * drifts), a second sighting, not an analogy to the first.
713
+ *
714
+ * ⛔ The repair deliberately does NOT add `app` to `SCOPE_ROOTS`. That list is
715
+ * the published strict-lint accept baseline in `@objectstack/formula`, so
716
+ * adding a root there stops EVERY surface that judges bare identifiers from
717
+ * faulting it — a widened public accept set, to fix one surface's diagnostic.
718
+ * The judged vocabulary is assembled HERE, where the per-surface question is
719
+ * asked, and `SCOPE_ROOTS` is left as the proper subset it already is.
720
+ */
721
+ /**
722
+ * The roots a field-level `*When` predicate binds. Everything else in
723
+ * {@link FIELD_RULE_JUDGED_ROOTS} is rejected — see the allowlist section above.
724
+ */
725
+ declare const FIELD_RULE_BOUND_ROOTS: readonly ["record", "previous", "parent"];
726
+ /**
727
+ * The decision half of the rule documented above, as ONE exported function so
728
+ * every surface that judges a field-level `*When` stands on the same verdict
729
+ * AND the same message (Prime Directive #12). Two consumers today: the
730
+ * metadata walk in {@link validateStackExpressions} below, and the docs-corpus
731
+ * gate `scripts/check-doc-formula-expressions.mjs`, which judges the same three
732
+ * slots where a fenced example's enclosing structure identifies the field layer
733
+ * (#11407). Before that gate existed this lived as a closure inside the walk —
734
+ * fine while there was one caller, and exactly how a second caller comes to own
735
+ * a DIALECT of the rule instead of the rule.
736
+ *
737
+ * `null` = nothing to report: the source does not parse (the syntax pass owns
738
+ * that, and reporting it twice under a worse name is the #4889 mistake), or
739
+ * every root it reads is one the field level binds.
740
+ */
741
+ declare function fieldRuleRootIssue(slot: string, source: string): {
742
+ root: string;
743
+ message: string;
744
+ } | null;
134
745
  /**
135
746
  * Validate every predicate in the stack. Returns the list of issues (empty =
136
747
  * clean). Caller decides how to surface / whether to fail the build.
137
748
  */
138
- declare function validateStackExpressions(stack: AnyRec$F): ExprIssue[];
749
+ declare function validateStackExpressions(stack: AnyRec$M): ExprIssue[];
139
750
 
140
751
  /**
141
752
  * The corrective sentence, lifted **verbatim** from `unevaluableRuleError` in
@@ -328,7 +939,7 @@ interface ListViewModeFinding {
328
939
  hint: string;
329
940
  }
330
941
  declare const LIST_VIEW_FILTERS_IN_VIEWS_MODE = "list-view-filters-in-views-mode";
331
- type AnyRec$E = Record<string, unknown>;
942
+ type AnyRec$L = Record<string, unknown>;
332
943
  /**
333
944
  * Flag ADR-0047 "views" mode violations on an object's built-in named views or a
334
945
  * `defineView` default `list` / named `listViews`: `quickFilters`, or a `tabs`
@@ -338,7 +949,7 @@ type AnyRec$E = Record<string, unknown>;
338
949
  *
339
950
  * Feed the PRE-parse stack (normalizeStackInput output) — see file header.
340
951
  */
341
- declare function validateListViewMode(stack: AnyRec$E): ListViewModeFinding[];
952
+ declare function validateListViewMode(stack: AnyRec$L): ListViewModeFinding[];
342
953
 
343
954
  type FunctionalCompletenessSeverity = 'error' | 'warning';
344
955
  interface FunctionalCompletenessFinding {
@@ -453,7 +1064,7 @@ declare const FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-des
453
1064
  * call sites that each skip it.
454
1065
  */
455
1066
  declare const FLOW_TRIGGER_UNROUTABLE = "flow-trigger-unroutable";
456
- type AnyRec$D = Record<string, unknown>;
1067
+ type AnyRec$K = Record<string, unknown>;
457
1068
  /**
458
1069
  * Validate auto-launched flow trigger wiring against the stack definition.
459
1070
  *
@@ -462,7 +1073,7 @@ type AnyRec$D = Record<string, unknown>;
462
1073
  * owns the `timeRelative` descriptor's contract, which is the point: the rule
463
1074
  * ASKS that schema rather than restating it.
464
1075
  */
465
- declare function validateFlowTriggerReadiness(stack: AnyRec$D): FlowTriggerReadinessFinding[];
1076
+ declare function validateFlowTriggerReadiness(stack: AnyRec$K): FlowTriggerReadinessFinding[];
466
1077
 
467
1078
  type FlowTemplatePathSeverity = 'error' | 'warning';
468
1079
  interface FlowTemplatePathFinding {
@@ -478,12 +1089,12 @@ interface FlowTemplatePathFinding {
478
1089
  declare const FLOW_TEMPLATE_UNKNOWN_FIELD = "flow-template-unknown-field";
479
1090
  declare const FLOW_TEMPLATE_LOOKUP_TRAVERSAL = "flow-template-lookup-traversal";
480
1091
  declare const FLOW_TEMPLATE_FIELD_UNPROVISIONED = "flow-template-field-unprovisioned";
481
- type AnyRec$C = Record<string, unknown>;
1092
+ type AnyRec$J = Record<string, unknown>;
482
1093
  /**
483
1094
  * Validate `{record.<path>}` template references across every record-change
484
1095
  * flow. Pure and dependency-free; safe on pre- or post-parse stacks.
485
1096
  */
486
- declare function validateFlowTemplatePaths(stack: AnyRec$C): FlowTemplatePathFinding[];
1097
+ declare function validateFlowTemplatePaths(stack: AnyRec$J): FlowTemplatePathFinding[];
487
1098
 
488
1099
  type ReadonlyFlowWriteSeverity = 'error' | 'warning';
489
1100
  interface ReadonlyFlowWriteFinding {
@@ -498,12 +1109,350 @@ interface ReadonlyFlowWriteFinding {
498
1109
  }
499
1110
  declare const FLOW_UPDATE_READONLY_FIELD = "flow-update-readonly-field";
500
1111
  declare const FLOW_UPDATE_READONLY_WHEN_FIELD = "flow-update-readonly-when-field";
501
- type AnyRec$B = Record<string, unknown>;
1112
+ type AnyRec$I = Record<string, unknown>;
1113
+ /**
1114
+ * Validate flow `update_record` / `create_record` writes against target-object
1115
+ * readonly declarations. Pure and dependency-free; safe on pre- or post-parse
1116
+ * stacks.
1117
+ */
1118
+ declare function validateReadonlyFlowWrites(stack: AnyRec$I): ReadonlyFlowWriteFinding[];
1119
+
1120
+ /** What a caller needs to tell an author WHICH source went unread, and where. */
1121
+ interface SourceParseFailure {
1122
+ /** The first parse diagnostic, in the compiler's own wording, flattened to one line. */
1123
+ message: string;
1124
+ /** 1-based line, in the AUTHORED source's coordinates (see `synthesizedLinesBefore`). */
1125
+ line: number;
1126
+ /** 1-based column. */
1127
+ column: number;
1128
+ /** How many parse diagnostics in total — `message` is the first of `count`. */
1129
+ count: number;
1130
+ }
1131
+ /** A parse plus the verdict on whether it succeeded. `failure` absent ⇒ it parsed. */
1132
+ interface CheckedParse {
1133
+ /**
1134
+ * The tree, ALWAYS returned — including when `failure` is set. Error recovery
1135
+ * produces a partial tree, and a caller that already reports findings from it
1136
+ * keeps doing so: the fix here is the missing SIGNAL, not the removal of
1137
+ * whatever the recovered tree could still be read for.
1138
+ */
1139
+ sourceFile: ts.SourceFile;
1140
+ /** Set when the parser reported at least one syntax diagnostic. */
1141
+ failure?: SourceParseFailure;
1142
+ }
1143
+ interface CheckedParseOptions {
1144
+ target: ts.ScriptTarget;
1145
+ setParentNodes: boolean;
1146
+ scriptKind: ts.ScriptKind;
1147
+ /**
1148
+ * Lines the CALLER synthesised ahead of the authored source, subtracted from
1149
+ * the reported position so it lands in the author's coordinates.
1150
+ *
1151
+ * `validate-hook-body-writes.ts` parses an L2 hook body wrapped in
1152
+ * `async function __body(ctx) {\n…\n}` — the shape the runtime compiles it
1153
+ * into — so its diagnostics are one line low. The reported line is clamped to
1154
+ * at least 1, so a diagnostic that lands on a synthesised line is attributed
1155
+ * to the nearest AUTHORED line and never to a line the author did not write.
1156
+ */
1157
+ synthesizedLinesBefore?: number;
1158
+ }
1159
+ /**
1160
+ * The one wording every caller's message embeds, so three findings about the
1161
+ * same defect do not describe it three ways.
1162
+ */
1163
+ declare function describeParseFailure(failure: SourceParseFailure): string;
1164
+ /**
1165
+ * The hint every caller's finding carries. It says what the finding IS — a
1166
+ * statement about what the checker could read, not a second syntax verdict —
1167
+ * because a source that does not parse is not scored, and an author who reads
1168
+ * "no problems found" about it would be reading a green line that lied.
1169
+ */
1170
+ declare const PARSE_FAILURE_HINT: string;
1171
+
1172
+ type HookBodyWriteSeverity = 'warning';
1173
+ interface HookBodyWriteFinding {
1174
+ /** v1 is advisory-only by contract — the type says so. */
1175
+ severity: HookBodyWriteSeverity;
1176
+ rule: string;
1177
+ /** Human-readable location, e.g. `hook "normalize_lead" › body`. */
1178
+ where: string;
1179
+ /** Config path, e.g. `hooks[0].body.source`. */
1180
+ path: string;
1181
+ message: string;
1182
+ hint: string;
1183
+ }
1184
+ declare const HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
1185
+ /**
1186
+ * [#10653] The body did not parse, so its write set is whatever error recovery
1187
+ * left readable.
1188
+ *
1189
+ * Reported rather than skipped for the reason this rule exists at all: a
1190
+ * mistake must be visible where it is MADE. An unparseable body reached the
1191
+ * extractor, produced fewer matches, and came back as a hook with nothing to
1192
+ * report — the same silence the undeclared write itself has at run time, this
1193
+ * time wearing the checker's badge. `warning` because the whole rule is
1194
+ * advisory and never gates (the severity type admits nothing else).
1195
+ */
1196
+ declare const HOOK_BODY_SOURCE_UNPARSEABLE = "hook-body-source-unparseable";
1197
+ /**
1198
+ * [#8663] The write-axis twin of `flow-template-field-unprovisioned` (#8340):
1199
+ * the body writes a field {@link IMPLICIT_FIELDS} exempts, but on THIS target
1200
+ * the platform registered that anchor without provisioning storage for it.
1201
+ *
1202
+ * A separate id at `warning` severity rather than a reclassification of
1203
+ * {@link HOOK_BODY_WRITE_UNKNOWN_FIELD}, matching #8340's precedent exactly:
1204
+ * the existence verdict is unchanged (the name IS addressable), and what is
1205
+ * added is a second, independently suppressible finding on the path where the
1206
+ * existence check stays silent.
1207
+ */
1208
+ declare const HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
1209
+ /** One syntactic write shape the extractor recognizes. */
1210
+ interface HookBodyWritePattern {
1211
+ /** Stable pattern id, carried on every extracted write. */
1212
+ readonly id: string;
1213
+ /** Author-facing syntax summary (for docs/diagnostics, not matching). */
1214
+ readonly syntax: string;
1215
+ /** Reconciliation fixture: extracting `source` must yield exactly `writes`. */
1216
+ readonly example: {
1217
+ readonly source: string;
1218
+ readonly writes: ReadonlyArray<{
1219
+ field: string;
1220
+ object?: string;
1221
+ }>;
1222
+ };
1223
+ }
1224
+ declare const HOOK_BODY_WRITE_PATTERNS: readonly HookBodyWritePattern[];
1225
+ /** A ledger pattern a given rule does NOT consume, and why. */
1226
+ interface BodyWritePatternExclusion {
1227
+ /** The {@link HOOK_BODY_WRITE_PATTERNS} entry id being excluded. */
1228
+ readonly id: string;
1229
+ /** Why the shape does not mean the same thing on this rule's surface. */
1230
+ readonly reason: string;
1231
+ }
1232
+ /**
1233
+ * The ledger shapes THIS rule consumes.
1234
+ *
1235
+ * Declared rather than implied: before the ledger carried a shape the hook
1236
+ * surface does not have, every write with no `object` was necessarily a
1237
+ * `ctx.input` write, and the rule could branch on that alone. It no longer can
1238
+ * — a `record-property-assign` write also carries no object, and would have
1239
+ * been reported as "the hook writes 'stage' to its input", which is false.
1240
+ * Each consumer declaring its own subset is what stops the next added shape
1241
+ * from silently landing in a branch that was never written for it.
1242
+ */
1243
+ declare const HOOK_BODY_WRITE_PATTERN_IDS: readonly string[];
1244
+ /** Ledger shapes this rule leaves alone, each with its reason. */
1245
+ declare const HOOK_BODY_WRITE_EXCLUSIONS: readonly BodyWritePatternExclusion[];
1246
+ type AnyRec$H = Record<string, unknown>;
1247
+ /** One statically-extracted field write found in an L2 body. */
1248
+ interface ExtractedHookBodyWrite {
1249
+ /** Which {@link HOOK_BODY_WRITE_PATTERNS} entry matched. */
1250
+ patternId: string;
1251
+ /** Target object name; `undefined` = the hook's own target object(s). */
1252
+ object?: string;
1253
+ /** The `ctx.api` method for diagnostics (`insert`/`create`/`update`/`updateById`). */
1254
+ method?: string;
1255
+ field: string;
1256
+ }
1257
+ /** Everything one parse of an L2 body yields. */
1258
+ interface ExtractedHookBodyWriteSet {
1259
+ /** Every literal write the {@link HOOK_BODY_WRITE_PATTERNS} ledger declares. */
1260
+ writes: ExtractedHookBodyWrite[];
1261
+ /**
1262
+ * `ctx.record` is handed to something as a VALUE somewhere in the body — an
1263
+ * argument, an assignment RHS, a spread, a return — rather than only having
1264
+ * its properties read and written, or being truthiness/type tested.
1265
+ *
1266
+ * The action rule needs this to tell a dead snapshot write from a live one:
1267
+ * `ctx.record.stage = 'won'; await ctx.api.object('d').update(ctx.record)`
1268
+ * builds a payload and persists it, so the assignment is not a no-op. When
1269
+ * this is true, no record write in the body can be judged, and none is
1270
+ * reported. (One-level aliasing — `const r = ctx.record` — reads as an
1271
+ * escape too, which is the safe direction: it suppresses findings.)
1272
+ */
1273
+ ctxRecordEscapes: boolean;
1274
+ /**
1275
+ * [#10653] Set when the body did not parse, so `writes` is whatever error
1276
+ * recovery left readable rather than the body's actual write set.
1277
+ *
1278
+ * Absent means one of two things, and they are not the same: the body parsed,
1279
+ * or the cheap pre-filter above rejected it before any parse. The filter is a
1280
+ * raw-text scan for `ctx` / `Object`, and a body containing neither cannot
1281
+ * match any pattern however it parses — so a skipped parse claims nothing and
1282
+ * hides nothing.
1283
+ *
1284
+ * ## Whose fault an unparseable body is — asked, not assumed
1285
+ *
1286
+ * The body is parsed inside a synthesised wrapper (`async function __body(ctx)
1287
+ * { … }`) because that is the shape the runtime compiles it into
1288
+ * (`new AsyncFunction('ctx', source)`). So a parse failure here could in
1289
+ * principle be the WRAPPER's fault rather than the author's, and blaming the
1290
+ * author for the checker's own bug is the failure this whole change is about.
1291
+ * It cannot be: the wrapper is a constant, and `validate-hook-body-writes.
1292
+ * test.ts` pins that it parses clean around an empty body and around every
1293
+ * example in the pattern ledger. Any diagnostic therefore comes from the
1294
+ * body — and its position is reported in the BODY's own coordinates (the
1295
+ * wrapper's line is subtracted, and the result is clamped so it can never
1296
+ * point at a line the author did not write).
1297
+ */
1298
+ parseFailure?: SourceParseFailure;
1299
+ }
1300
+ /**
1301
+ * Extract every literal field write the pattern ledger declares from an L2
1302
+ * body's source. Parse-only (the source is never executed), error-tolerant
1303
+ * (a body with syntax errors simply yields fewer matches), and lazy: the
1304
+ * TypeScript compiler is not loaded when no pattern can possibly match.
1305
+ *
1306
+ * Thin projection of {@link extractHookBodyWriteSet} — use that one when the
1307
+ * `ctx.record` liveness signal matters, so the body is parsed once, not twice.
1308
+ */
1309
+ declare function extractHookBodyWrites(source: string): ExtractedHookBodyWrite[];
1310
+ /** {@link extractHookBodyWrites} plus the `ctx.record` liveness signal, one parse. */
1311
+ declare function extractHookBodyWriteSet(source: string): ExtractedHookBodyWriteSet;
1312
+ /**
1313
+ * Validate L2 hook-body writes against target-object field declarations.
1314
+ * Pure `(stack) => Finding[]` (ADR-0019); safe on pre- or post-parse stacks.
1315
+ *
1316
+ * ## Which intakes reach a hook authored as an inline `handler` (#16095)
1317
+ *
1318
+ * This rule opens on `body.language === 'js'`. A hook written as
1319
+ * `handler: async (ctx) => { … }` carries no `body`, so whether it is judged
1320
+ * is a property of the DOOR — what each caller hands this function — not of
1321
+ * the rule. Measured (`packages/cli/test/lint-hook-rules-reach-handler-hooks*`):
1322
+ *
1323
+ * Every leg below was measured with the body-authored control beside it, so a
1324
+ * silent leg is a reading about that door and never about this rule. The doors
1325
+ * are the call sites of `runAuthoringRules`, enumerated — not the three `os *`
1326
+ * commands, which are fewer than the doors:
1327
+ *
1328
+ * `os build` union `compile.ts` lowers every inline handler to a
1329
+ * metadata body BEFORE its parse (`lowerCallables`)
1330
+ * and judges the parsed result — REACHED, always was.
1331
+ * `os build` per-package same lowered `result.data`, re-entered one package
1332
+ * manifest at a time — REACHED, always was.
1333
+ * `os lint` hands the registry's `parsed` tier that same
1334
+ * lowered view — REACHED since #16095.
1335
+ * scaffold validate `runScaffoldAuthoringRules` (`os init` / `dev` over
1336
+ * a rendered template) lowers before it parses too —
1337
+ * REACHED, always was, and pinned since #16095.
1338
+ * `os validate` lowers before it parses since #16544 — the same
1339
+ * `lowerCallables` call, between its pre-parse
1340
+ * unknown-key lints and its parse — REACHED since
1341
+ * #16544. Measured NOT reached under #16095, when it
1342
+ * parsed the normalized stack without lowering while
1343
+ * the body-authored control fired; closing it was
1344
+ * its own accept/reject decision, taken on #16544.
1345
+ * direct call judges exactly the stack it is given — NOT reached
1346
+ * unless the caller lowers first; measured both ways.
1347
+ *
1348
+ * A handler the extractor refuses (forbidden token, free identifier,
1349
+ * unparseable) is left with no `body` on every door, so this rule stays silent
1350
+ * on it; the refusal itself is reported by `os lint`'s `hook-body/*` rules and
1351
+ * by `os build`'s warn-and-bundle line, never guessed at here.
1352
+ */
1353
+ declare function validateHookBodyWrites(stack: AnyRec$H): HookBodyWriteFinding[];
1354
+
1355
+ type ReadonlyHookWriteSeverity = 'error' | 'warning';
1356
+ interface ReadonlyHookWriteFinding {
1357
+ severity: ReadonlyHookWriteSeverity;
1358
+ rule: string;
1359
+ /** Human-readable location, e.g. `hook "touch_account" > body`. */
1360
+ where: string;
1361
+ /** Config path, e.g. `hooks[0].body.source`. */
1362
+ path: string;
1363
+ message: string;
1364
+ hint: string;
1365
+ }
1366
+ declare const HOOK_API_UPDATE_READONLY_FIELD = "hook-api-update-readonly-field";
1367
+ declare const HOOK_API_UPDATE_READONLY_WHEN_FIELD = "hook-api-update-readonly-when-field";
1368
+ /**
1369
+ * The `HOOK_BODY_WRITE_PATTERNS` shapes THIS rule consumes.
1370
+ *
1371
+ * Declared as data rather than implied by a branch, for the reason the sibling
1372
+ * rules declare theirs: a write with no `object` is not a single thing (both
1373
+ * `ctx.input` and `ctx.record` shapes carry none), so a future ledger addition
1374
+ * must not be able to land silently in a branch never written for it.
1375
+ */
1376
+ declare const READONLY_HOOK_WRITE_PATTERN_IDS: readonly string[];
1377
+ /** Ledger shapes this rule leaves alone, each with its reason. */
1378
+ declare const READONLY_HOOK_WRITE_EXCLUSIONS: readonly BodyWritePatternExclusion[];
1379
+ type AnyRec$G = Record<string, unknown>;
1380
+ /**
1381
+ * Validate L2 hook-body `ctx.api` writes against target-object readonly
1382
+ * declarations. Pure `(stack) => Finding[]` (ADR-0019); safe on pre- or
1383
+ * post-parse stacks.
1384
+ *
1385
+ * ## Which intakes reach a hook authored as an inline `handler` (#16095)
1386
+ *
1387
+ * This rule opens on `body.language === 'js'`. A hook written as
1388
+ * `handler: async (ctx) => { … }` carries no `body`, so whether it is judged
1389
+ * is a property of the DOOR — what each caller hands this function — not of
1390
+ * the rule. Measured (`packages/cli/test/lint-hook-rules-reach-handler-hooks*`):
1391
+ *
1392
+ * Every leg below was measured with the body-authored control beside it, so a
1393
+ * silent leg is a reading about that door and never about this rule. The doors
1394
+ * are the call sites of `runAuthoringRules`, enumerated — not the three `os *`
1395
+ * commands, which are fewer than the doors:
1396
+ *
1397
+ * `os build` union `compile.ts` lowers every inline handler to a
1398
+ * metadata body BEFORE its parse (`lowerCallables`)
1399
+ * and judges the parsed result — REACHED, always was.
1400
+ * `os build` per-package same lowered `result.data`, re-entered one package
1401
+ * manifest at a time — REACHED, always was.
1402
+ * `os lint` hands the registry's `parsed` tier that same
1403
+ * lowered view — REACHED since #16095.
1404
+ * scaffold validate `runScaffoldAuthoringRules` (`os init` / `dev` over
1405
+ * a rendered template) lowers before it parses too —
1406
+ * REACHED, always was, and pinned since #16095.
1407
+ * `os validate` lowers before it parses since #16544 — the same
1408
+ * `lowerCallables` call, between its pre-parse
1409
+ * unknown-key lints and its parse — REACHED since
1410
+ * #16544. Measured NOT reached under #16095, when it
1411
+ * parsed the normalized stack without lowering while
1412
+ * the body-authored control fired; closing it was
1413
+ * its own accept/reject decision, taken on #16544.
1414
+ * direct call judges exactly the stack it is given — NOT reached
1415
+ * unless the caller lowers first; measured both ways.
1416
+ *
1417
+ * A handler the extractor refuses (forbidden token, free identifier,
1418
+ * unparseable) is left with no `body` on every door, so this rule stays silent
1419
+ * on it; the refusal itself is reported by `os lint`'s `hook-body/*` rules and
1420
+ * by `os build`'s warn-and-bundle line, never guessed at here.
1421
+ */
1422
+ declare function validateReadonlyHookWrites(stack: AnyRec$G): ReadonlyHookWriteFinding[];
1423
+
1424
+ type ReadonlyActionWriteSeverity = 'warning';
1425
+ interface ReadonlyActionWriteFinding {
1426
+ /** Advisory by contract - the conditional strip is per record state. */
1427
+ severity: ReadonlyActionWriteSeverity;
1428
+ rule: string;
1429
+ /** Human-readable location, e.g. `action "freeze_invoice" > body`. */
1430
+ where: string;
1431
+ /** Config path, e.g. `actions[0].body.source`. */
1432
+ path: string;
1433
+ message: string;
1434
+ hint: string;
1435
+ }
1436
+ /** Rule id (registry entry). */
1437
+ declare const ACTION_API_UPDATE_READONLY_WHEN_FIELD = "action-api-update-readonly-when-field";
1438
+ /**
1439
+ * The `HOOK_BODY_WRITE_PATTERNS` shapes THIS rule consumes.
1440
+ *
1441
+ * Declared as data rather than implied by a branch, for the reason the sibling
1442
+ * rules declare theirs: a write with no `object` is not a single thing (both
1443
+ * `ctx.input` and `ctx.record` shapes carry none), so a future ledger addition
1444
+ * must not be able to land silently in a branch never written for it.
1445
+ */
1446
+ declare const READONLY_ACTION_WRITE_PATTERN_IDS: readonly string[];
1447
+ /** Ledger shapes this rule leaves alone, each with its reason. */
1448
+ declare const READONLY_ACTION_WRITE_EXCLUSIONS: readonly BodyWritePatternExclusion[];
1449
+ type AnyRec$F = Record<string, unknown>;
502
1450
  /**
503
- * Validate flow `update_record` writes against target-object readonly
504
- * declarations. Pure and dependency-free; safe on pre- or post-parse stacks.
1451
+ * Validate L2 action-body `ctx.api` writes against target-object `readonlyWhen`
1452
+ * declarations. Pure `(stack) => Finding[]` (ADR-0019); safe on pre- or
1453
+ * post-parse stacks.
505
1454
  */
506
- declare function validateReadonlyFlowWrites(stack: AnyRec$B): ReadonlyFlowWriteFinding[];
1455
+ declare function validateReadonlyActionWrites(stack: AnyRec$F): ReadonlyActionWriteFinding[];
507
1456
 
508
1457
  type ViewContainerSeverity = 'error' | 'warning';
509
1458
  interface ViewContainerFinding {
@@ -540,14 +1489,14 @@ declare const STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
540
1489
  declare const STYLE_RESPONSIVE_NO_BASE = "style-responsive-no-base";
541
1490
  declare const STYLE_UNKNOWN_CSS_PROPERTY = "style-unknown-css-property";
542
1491
  declare const STYLE_UNKNOWN_TOKEN = "style-unknown-token";
543
- type AnyRec$A = Record<string, unknown>;
1492
+ type AnyRec$E = Record<string, unknown>;
544
1493
  /**
545
1494
  * Validate every page's component tree for SDUI styling correctness (ADR-0065).
546
1495
  * Returns findings (empty = clean). `error` findings describe styles that are
547
1496
  * silently dropped and should fail validate/build; `warning` findings are
548
1497
  * advisory (typos, drift, footguns).
549
1498
  */
550
- declare function validateResponsiveStyles(stack: AnyRec$A): StyleFinding[];
1499
+ declare function validateResponsiveStyles(stack: AnyRec$E): StyleFinding[];
551
1500
 
552
1501
  type JsxPageSeverity = 'error' | 'warning';
553
1502
  interface JsxPageFinding {
@@ -560,8 +1509,8 @@ interface JsxPageFinding {
560
1509
  message: string;
561
1510
  hint: string;
562
1511
  }
563
- type AnyRec$z = Record<string, unknown>;
564
- declare function validateJsxPages(stack: AnyRec$z, opts?: {
1512
+ type AnyRec$D = Record<string, unknown>;
1513
+ declare function validateJsxPages(stack: AnyRec$D, opts?: {
565
1514
  manifest?: Manifest;
566
1515
  }): JsxPageFinding[];
567
1516
 
@@ -574,8 +1523,8 @@ interface ReactPageFinding {
574
1523
  message: string;
575
1524
  hint: string;
576
1525
  }
577
- type AnyRec$y = Record<string, unknown>;
578
- declare function validateReactPages(stack: AnyRec$y): ReactPageFinding[];
1526
+ type AnyRec$C = Record<string, unknown>;
1527
+ declare function validateReactPages(stack: AnyRec$C): ReactPageFinding[];
579
1528
 
580
1529
  type ReactPropSeverity = 'error' | 'warning';
581
1530
  interface ReactPropFinding {
@@ -586,7 +1535,7 @@ interface ReactPropFinding {
586
1535
  message: string;
587
1536
  hint: string;
588
1537
  }
589
- type AnyRec$x = Record<string, unknown>;
1538
+ type AnyRec$B = Record<string, unknown>;
590
1539
  /**
591
1540
  * The source could not be parsed, so the prop checks below read a partially
592
1541
  * recovered tree (#10653).
@@ -604,65 +1553,29 @@ type AnyRec$x = Record<string, unknown>;
604
1553
  * says the true thing: these checks did not get to run.
605
1554
  */
606
1555
  declare const REACT_PAGE_SOURCE_UNPARSEABLE = "react-page-source-unparseable";
1556
+ /**
1557
+ * [#11284] A prop written in a deprecated react-tier spelling (maintainer
1558
+ * ruling 2026-08-23: the react tier converges on the metadata-tier
1559
+ * vocabulary, deprecate-first). Warning, never error: the old spelling keeps
1560
+ * working for the whole deprecation window — this is the loud half of
1561
+ * "alias + loud deprecation", same shape as `approval-approver-type-deprecated`.
1562
+ */
1563
+ declare const REACT_PROP_DEPRECATED = "react-prop-deprecated";
1564
+ /**
1565
+ * [#14791] A prop written in a react-tier spelling the contract has RETIRED
1566
+ * (maintainer ruling 2026-09-07: `<ListView objectName>` / `viewType` go with
1567
+ * no deprecation window). Error, never warning: the contract no longer
1568
+ * publishes the spelling, and the finding carries the prescription — the
1569
+ * react-tier twin of a metadata schema's `retiredKey()` tombstone.
1570
+ */
1571
+ declare const REACT_PROP_RETIRED = "react-prop-retired";
607
1572
  declare const REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
608
1573
  declare const REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
609
1574
  declare const REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
610
1575
  declare const REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
611
1576
  declare const REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
612
1577
  declare const REACT_BLOCK_NEEDS_RECORD_CONTEXT = "react-block-needs-record-context";
613
- declare function validateReactPageProps(stack: AnyRec$x): ReactPropFinding[];
614
-
615
- /** What a caller needs to tell an author WHICH source went unread, and where. */
616
- interface SourceParseFailure {
617
- /** The first parse diagnostic, in the compiler's own wording, flattened to one line. */
618
- message: string;
619
- /** 1-based line, in the AUTHORED source's coordinates (see `synthesizedLinesBefore`). */
620
- line: number;
621
- /** 1-based column. */
622
- column: number;
623
- /** How many parse diagnostics in total — `message` is the first of `count`. */
624
- count: number;
625
- }
626
- /** A parse plus the verdict on whether it succeeded. `failure` absent ⇒ it parsed. */
627
- interface CheckedParse {
628
- /**
629
- * The tree, ALWAYS returned — including when `failure` is set. Error recovery
630
- * produces a partial tree, and a caller that already reports findings from it
631
- * keeps doing so: the fix here is the missing SIGNAL, not the removal of
632
- * whatever the recovered tree could still be read for.
633
- */
634
- sourceFile: ts.SourceFile;
635
- /** Set when the parser reported at least one syntax diagnostic. */
636
- failure?: SourceParseFailure;
637
- }
638
- interface CheckedParseOptions {
639
- target: ts.ScriptTarget;
640
- setParentNodes: boolean;
641
- scriptKind: ts.ScriptKind;
642
- /**
643
- * Lines the CALLER synthesised ahead of the authored source, subtracted from
644
- * the reported position so it lands in the author's coordinates.
645
- *
646
- * `validate-hook-body-writes.ts` parses an L2 hook body wrapped in
647
- * `async function __body(ctx) {\n…\n}` — the shape the runtime compiles it
648
- * into — so its diagnostics are one line low. The reported line is clamped to
649
- * at least 1, so a diagnostic that lands on a synthesised line is attributed
650
- * to the nearest AUTHORED line and never to a line the author did not write.
651
- */
652
- synthesizedLinesBefore?: number;
653
- }
654
- /**
655
- * The one wording every caller's message embeds, so three findings about the
656
- * same defect do not describe it three ways.
657
- */
658
- declare function describeParseFailure(failure: SourceParseFailure): string;
659
- /**
660
- * The hint every caller's finding carries. It says what the finding IS — a
661
- * statement about what the checker could read, not a second syntax verdict —
662
- * because a source that does not parse is not scored, and an author who reads
663
- * "no problems found" about it would be reading a green line that lied.
664
- */
665
- declare const PARSE_FAILURE_HINT: string;
1578
+ declare function validateReactPageProps(stack: AnyRec$B): ReactPropFinding[];
666
1579
 
667
1580
  type SourceStyleSeverity = 'error' | 'warning';
668
1581
  interface SourceStyleFinding {
@@ -674,8 +1587,8 @@ interface SourceStyleFinding {
674
1587
  hint: string;
675
1588
  }
676
1589
  declare const PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
677
- type AnyRec$w = Record<string, unknown>;
678
- declare function validatePageSourceStyling(stack: AnyRec$w): SourceStyleFinding[];
1590
+ type AnyRec$A = Record<string, unknown>;
1591
+ declare function validatePageSourceStyling(stack: AnyRec$A): SourceStyleFinding[];
679
1592
 
680
1593
  /**
681
1594
  * Build-time record-title diagnostics (ADR-0079).
@@ -719,14 +1632,14 @@ interface RecordTitleFinding {
719
1632
  /** How to fix it. */
720
1633
  hint: string;
721
1634
  }
722
- type AnyRec$v = Record<string, unknown>;
1635
+ type AnyRec$z = Record<string, unknown>;
723
1636
  /**
724
1637
  * Validate every object's record-title declaration. Returns the list of
725
1638
  * findings (empty = clean). Both rules are advisory (`warning`): the caller
726
1639
  * must never fail the build on them alone — auto-provision + the `Record #<id>`
727
1640
  * floor guarantee a resolvable title at runtime.
728
1641
  */
729
- declare function validateRecordTitle(stack: AnyRec$v): RecordTitleFinding[];
1642
+ declare function validateRecordTitle(stack: AnyRec$z): RecordTitleFinding[];
730
1643
 
731
1644
  declare const FIELD_GROUP_UNDECLARED = "field-group-undeclared";
732
1645
  declare const FIELD_GROUP_EMPTY = "field-group-empty";
@@ -748,16 +1661,23 @@ interface SemanticRoleFinding {
748
1661
  /** How to fix it. */
749
1662
  hint: string;
750
1663
  }
751
- type AnyRec$u = Record<string, unknown>;
1664
+ type AnyRec$y = Record<string, unknown>;
752
1665
  /**
753
1666
  * Validate every object's semantic-role pointers. Returns the list of
754
1667
  * findings (empty = clean). Advisory only — the caller must never fail the
755
1668
  * build on these alone.
756
1669
  */
757
- declare function validateSemanticRoles(stack: AnyRec$u): SemanticRoleFinding[];
1670
+ declare function validateSemanticRoles(stack: AnyRec$y): SemanticRoleFinding[];
758
1671
 
759
1672
  declare const FORM_FIELD_UNKNOWN = "form-field-unknown";
760
1673
  declare const FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
1674
+ /**
1675
+ * [#13855] A section's `group` names a field group the bound object does not
1676
+ * declare. The reference form inherits the section's whole membership from that
1677
+ * group, so a dangling key leaves the section with no members and it does not
1678
+ * render at all. Advisory like both rules above — see `object-field-groups.ts`.
1679
+ */
1680
+ declare const FORM_SECTION_GROUP_UNKNOWN = "form-section-group-unknown";
761
1681
  type FormLayoutSeverity = 'error' | 'warning';
762
1682
  interface FormLayoutFinding {
763
1683
  /** Always `warning` today — both rules are advisory (see module note). */
@@ -773,12 +1693,12 @@ interface FormLayoutFinding {
773
1693
  /** How to fix it. */
774
1694
  hint: string;
775
1695
  }
776
- type AnyRec$t = Record<string, unknown>;
1696
+ type AnyRec$x = Record<string, unknown>;
777
1697
  /**
778
1698
  * Validate authored form-view layout. Returns findings (empty = clean).
779
1699
  * Advisory only — the caller must never fail the build on these alone.
780
1700
  */
781
- declare function validateFormLayout(stack: AnyRec$t): FormLayoutFinding[];
1701
+ declare function validateFormLayout(stack: AnyRec$x): FormLayoutFinding[];
782
1702
 
783
1703
  declare const VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
784
1704
  declare const VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
@@ -791,6 +1711,15 @@ declare const VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
791
1711
  * consequence is identical, the FIX is not, and `--json` consumers key on the id.
792
1712
  */
793
1713
  declare const VISIBILITY_PREDICATE_OVER_BUDGET = "visibility-predicate-over-budget";
1714
+ /**
1715
+ * A predicate calling a function the CEL environment does not register —
1716
+ * #13594, the scoped supersession of this file's parse-only ruling (see the
1717
+ * module note's §Function existence). A separate id from
1718
+ * {@link VISIBILITY_PREDICATE_SYNTAX} for the same reason `over-budget` is one:
1719
+ * the source parses perfectly, so the dialect prescription cannot succeed on it
1720
+ * and `--json` consumers key on the id.
1721
+ */
1722
+ declare const VISIBILITY_PREDICATE_UNKNOWN_FUNCTION = "visibility-predicate-unknown-function";
794
1723
  type VisibilitySeverity = 'error' | 'warning';
795
1724
  /**
796
1725
  * Which binding environment the linted surface belongs to (ADR-0089 §Context):
@@ -818,9 +1747,9 @@ interface VisibilityOptions {
818
1747
  interface VisibilityFinding {
819
1748
  /**
820
1749
  * `warning` for the ADR-0089 D3b advisory (`visibility-root-mislayered`);
821
- * `error` for the three rules that gate — `visibility-predicate-syntax`,
822
- * `visibility-predicate-over-budget` and `visibility-bare-identifier` (see
823
- * module note).
1750
+ * `error` for the four rules that gate — `visibility-predicate-syntax`,
1751
+ * `visibility-predicate-over-budget`, `visibility-predicate-unknown-function`
1752
+ * and `visibility-bare-identifier` (see module note).
824
1753
  */
825
1754
  severity: VisibilitySeverity;
826
1755
  /** Diagnostic rule id, e.g. `visibility-root-mislayered`. */
@@ -834,7 +1763,7 @@ interface VisibilityFinding {
834
1763
  /** How to fix it. */
835
1764
  hint: string;
836
1765
  }
837
- type AnyRec$s = Record<string, unknown>;
1766
+ type AnyRec$w = Record<string, unknown>;
838
1767
  /**
839
1768
  * Validate conditional-visibility predicates across authored views and pages.
840
1769
  *
@@ -847,8 +1776,10 @@ type AnyRec$s = Record<string, unknown>;
847
1776
  *
848
1777
  * Returns findings (empty = clean). `visibility-root-mislayered` is advisory
849
1778
  * (`warning`); `visibility-predicate-syntax` (#6253),
850
- * `visibility-predicate-over-budget` (#7217) and `visibility-bare-identifier`
851
- * (#6128) are `error` and the caller is expected to fail the build on them.
1779
+ * `visibility-predicate-over-budget` (#7217),
1780
+ * `visibility-predicate-unknown-function` (#13594) and
1781
+ * `visibility-bare-identifier` (#6128) are `error` and the caller is expected to
1782
+ * fail the build on them.
852
1783
  *
853
1784
  * The binding-root check is layer-directional (ADR-0089 D3). A form view that
854
1785
  * declares `data: { provider: 'schema', schemaId }` is judged at `metadata` on
@@ -857,9 +1788,11 @@ type AnyRec$s = Record<string, unknown>;
857
1788
  * predicate is flagged), or leave it at the `'runtime'` default for `*.view.ts` /
858
1789
  * `*.page.ts` surfaces (so a `data.`-rooted predicate is flagged). The syntax and
859
1790
  * bare-identifier checks are layer-agnostic — but the ROOT their hints prescribe
860
- * is not, which is the second half of what #7815 fixes.
1791
+ * is not, which is the second half of what #7815 fixes. The unknown-function
1792
+ * check (#13594) is layer-agnostic in both halves: it prescribes a NAME, and the
1793
+ * environment registers the same names on every layer.
861
1794
  */
862
- declare function validateVisibilityPredicates(stack: AnyRec$s, opts?: VisibilityOptions): VisibilityFinding[];
1795
+ declare function validateVisibilityPredicates(stack: AnyRec$w, opts?: VisibilityOptions): VisibilityFinding[];
863
1796
 
864
1797
  declare const PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
865
1798
  declare const PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
@@ -902,7 +1835,7 @@ interface PredicatePathOptions {
902
1835
  */
903
1836
  resolveSchema?: (schemaId: string) => unknown;
904
1837
  }
905
- type AnyRec$r = Record<string, unknown>;
1838
+ type AnyRec$v = Record<string, unknown>;
906
1839
  /**
907
1840
  * Refuse a metadata-form predicate that names a path the target schema does not
908
1841
  * declare (#7010).
@@ -931,7 +1864,7 @@ type AnyRec$r = Record<string, unknown>;
931
1864
  *
932
1865
  * Returns findings (empty = clean).
933
1866
  */
934
- declare function validatePredicatePathRefs(stack: AnyRec$r, opts?: PredicatePathOptions): PredicatePathFinding[];
1867
+ declare function validatePredicatePathRefs(stack: AnyRec$v, opts?: PredicatePathOptions): PredicatePathFinding[];
935
1868
 
936
1869
  declare const CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
937
1870
  type CapabilityRefSeverity = 'error' | 'warning';
@@ -949,12 +1882,12 @@ interface CapabilityRefFinding {
949
1882
  /** How to fix it. */
950
1883
  hint: string;
951
1884
  }
952
- type AnyRec$q = Record<string, unknown>;
1885
+ type AnyRec$u = Record<string, unknown>;
953
1886
  /**
954
1887
  * Validate every capability reference in a stack. Returns findings (empty =
955
1888
  * clean). Advisory only — callers must not fail the build on these alone.
956
1889
  */
957
- declare function validateCapabilityReferences(stack: AnyRec$q): CapabilityRefFinding[];
1890
+ declare function validateCapabilityReferences(stack: AnyRec$u): CapabilityRefFinding[];
958
1891
 
959
1892
  declare const APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
960
1893
  declare const APPROVAL_APPROVER_TYPE_DEPRECATED = "approval-approver-type-deprecated";
@@ -980,12 +1913,12 @@ interface ApprovalApproverFinding {
980
1913
  /** How to fix it. */
981
1914
  hint: string;
982
1915
  }
983
- type AnyRec$p = Record<string, unknown>;
1916
+ type AnyRec$t = Record<string, unknown>;
984
1917
  /**
985
1918
  * Validate the approvers of every Approval node in the stack's flows.
986
1919
  * Returns findings (empty = clean).
987
1920
  */
988
- declare function validateApprovalApprovers(stack: AnyRec$p): ApprovalApproverFinding[];
1921
+ declare function validateApprovalApprovers(stack: AnyRec$t): ApprovalApproverFinding[];
989
1922
 
990
1923
  type SeedReplaySafetySeverity = 'error' | 'warning';
991
1924
  interface SeedReplaySafetyFinding {
@@ -999,7 +1932,7 @@ interface SeedReplaySafetyFinding {
999
1932
  hint: string;
1000
1933
  }
1001
1934
  declare const SEED_INSERT_MODE_DUPLICATES_ON_REPLAY = "seed-insert-mode-duplicates-on-replay";
1002
- type AnyRec$o = Record<string, unknown>;
1935
+ type AnyRec$s = Record<string, unknown>;
1003
1936
  /**
1004
1937
  * Flag every seed dataset declared with `mode: 'insert'` — the one non-idempotent
1005
1938
  * mode, which duplicates its rows on every replay boot (framework#3434). Returns
@@ -1009,7 +1942,7 @@ type AnyRec$o = Record<string, unknown>;
1009
1942
  * Reads `stack.data` (the `SeedSchema[]` fixtures). Safe on any shape — a stack
1010
1943
  * with no `data` array yields no findings.
1011
1944
  */
1012
- declare function validateSeedReplaySafety(stack: AnyRec$o): SeedReplaySafetyFinding[];
1945
+ declare function validateSeedReplaySafety(stack: AnyRec$s): SeedReplaySafetyFinding[];
1013
1946
 
1014
1947
  type SeedStateMachineSeverity = 'warning';
1015
1948
  interface SeedStateMachineFinding {
@@ -1023,7 +1956,7 @@ interface SeedStateMachineFinding {
1023
1956
  hint: string;
1024
1957
  }
1025
1958
  declare const SEED_VALUE_OUTSIDE_STATE_MACHINE = "seed-value-outside-state-machine";
1026
- type AnyRec$n = Record<string, unknown>;
1959
+ type AnyRec$r = Record<string, unknown>;
1027
1960
  /**
1028
1961
  * Flag every seed record whose `state_machine`-governed field carries a value
1029
1962
  * the machine does not declare (framework#3433 follow-up). Returns the findings
@@ -1036,7 +1969,7 @@ type AnyRec$n = Record<string, unknown>;
1036
1969
  * unresolved `cel` Expression envelope, a number) is skipped: it cannot be
1037
1970
  * statically compared to the declared-state set.
1038
1971
  */
1039
- declare function validateSeedStateMachine(stack: AnyRec$n): SeedStateMachineFinding[];
1972
+ declare function validateSeedStateMachine(stack: AnyRec$r): SeedStateMachineFinding[];
1040
1973
 
1041
1974
  declare const SECURITY_OWD_UNSET = "security-owd-unset";
1042
1975
  declare const SECURITY_OWD_ALIAS = "security-owd-alias";
@@ -1048,9 +1981,11 @@ declare const SECURITY_BOOK_AUDIENCE_UNKNOWN_SET = "security-book-audience-unkno
1048
1981
  declare const SECURITY_PRIVATE_NO_READSCOPE = "security-private-no-readscope";
1049
1982
  declare const SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
1050
1983
  declare const SECURITY_FLS_UNQUALIFIED_KEY = "security-fls-unqualified-key";
1984
+ declare const SECURITY_FLS_UNKNOWN_FIELD = "security-fls-unknown-field";
1051
1985
  declare const SECURITY_GRANT_EXPIRED_AT_AUTHORING = "security-grant-expired-at-authoring";
1052
1986
  declare const SECURITY_DELEGATION_MISSING_REASON = "security-delegation-missing-reason";
1053
1987
  declare const SECURITY_CBP_NO_RELATION = "security-controlled-by-parent-no-relation";
1988
+ declare const SECURITY_CBP_AMBIGUOUS_RELATION = "security-controlled-by-parent-ambiguous-relation";
1054
1989
  type SecuritySeverity = 'error' | 'warning' | 'info';
1055
1990
  interface SecurityFinding {
1056
1991
  severity: SecuritySeverity;
@@ -1065,7 +2000,7 @@ interface SecurityFinding {
1065
2000
  /** How to fix it. */
1066
2001
  hint: string;
1067
2002
  }
1068
- type AnyRec$m = Record<string, unknown>;
2003
+ type AnyRec$q = Record<string, unknown>;
1069
2004
  /**
1070
2005
  * Validate the security posture of a stack. Returns findings (empty = clean).
1071
2006
  * `error` findings gate the build in `os compile`; `info` is advisory.
@@ -1073,7 +2008,7 @@ type AnyRec$m = Record<string, unknown>;
1073
2008
  * `opts.nowMs` injects the clock for the ADR-0091 authoring-time expiry rule
1074
2009
  * (tests); production callers omit it.
1075
2010
  */
1076
- declare function validateSecurityPosture(stack: AnyRec$m, opts?: {
2011
+ declare function validateSecurityPosture(stack: AnyRec$q, opts?: {
1077
2012
  nowMs?: number;
1078
2013
  }): SecurityFinding[];
1079
2014
  /**
@@ -1104,94 +2039,8 @@ declare function validateSecurityPosture(stack: AnyRec$m, opts?: {
1104
2039
  * carries `positions`/`apps` and both types are gated — at which point it
1105
2040
  * crosses whole, in one edit, as its own entry.
1106
2041
  */
1107
- declare function validateSecurityRoleWord(stack: AnyRec$m): SecurityFinding[];
1108
-
1109
- /**
1110
- * [ADR-0105 D6] The two organization-axis red lines, enforced at authoring time.
1111
- *
1112
- * ADR-0105 gives organizations a reporting/grouping dimension
1113
- * (`sys_organization.parent_organization_id`, sibling ordering). That dimension
1114
- * is load-bearing for consolidated reporting — and dangerous, because it LOOKS
1115
- * like a permission hierarchy. Two lines keep it from becoming one:
1116
- *
1117
- * | Rule | Red line |
1118
- * |-----------------------------------------|-----------------------------------|
1119
- * | org-axis-permission-inheritance (error) | D6 ①: no inheritance along the org tree |
1120
- * | org-axis-cross-org-bu-grant (error) | D6 ②: business-unit trees stay org-internal |
1121
- *
1122
- * **① No permission inheritance along the org axis.** Cross-organization
1123
- * visibility comes from membership union (`accessible_org_ids`, ADR-0105 D2) —
1124
- * the engine's own Layer 0 wall — never from walking a parent reference. An RLS
1125
- * policy or sharing rule that reads `parent_organization_id` builds a SECOND
1126
- * permission hierarchy beside the business-unit tree: exactly the dual-hierarchy
1127
- * mistake ADR-0057 D5 retired and ADR-0090 D3 finalized for positions. It also
1128
- * silently outranks the wall it sits behind, since a Layer-1 policy cannot widen
1129
- * Layer 0 (W1) — so the author gets a rule that appears to grant access and
1130
- * does not. Fail at authoring, not in a support ticket.
1131
- *
1132
- * **② Business-unit trees remain org-internal.** `sys_business_unit` is
1133
- * org-scoped and every BU mechanism (`unit_and_subordinates` sharing,
1134
- * `adminScope` delegation, depth scopes) resolves within ONE organization. A
1135
- * business-unit sharing rule on a PLATFORM-GLOBAL object (`tenancy.enabled:
1136
- * false`) has no organization column to scope against, so the grant spans every
1137
- * organization in the database — a cross-org BU grant by construction, and the
1138
- * "cross-org BU mega-tree" the ADR rejected, arrived at by accident. It covers
1139
- * BOTH business-unit recipients, `business_unit` and `unit_and_subordinates`
1140
- * — see {@link BU_TREE_RECIPIENT_TYPES} for the word list and its deliberate
1141
- * complement.
1142
- *
1143
- * Both are `error`, per ADR-0049 discipline: each mirrors a real enforcement
1144
- * property (the Layer 0 wall's independence; the org-predicated BU resolver),
1145
- * so the lint moves the failure from silent-wrong-answer to author-time fix-it.
1146
- *
1147
- * Pure `(stack) => Finding[]`; accepts the NORMALIZED stack input.
1148
- *
1149
- * ## Scope — the keys this rule reads, and the ones it deliberately does not
1150
- *
1151
- * Every key below is one `@objectstack/spec` DECLARES. That is a contract, not
1152
- * a style preference: the rule is registered `input: 'parsed'`, so what it sees
1153
- * is what `ObjectStackSchema` returned. An undeclared key never survives to be
1154
- * read — the stack root strips it, and the `.strict()` sub-schemas reject the
1155
- * whole stack outright — so a branch keyed on one is inert for every stack an
1156
- * author can actually ship (#4984, #5009).
1157
- *
1158
- * | Read | Declared by |
1159
- * |---------------------------------------|------------------------------------|
1160
- * | `permissions[]` | `ObjectStackSchema` |
1161
- * | `permissions[].rowLevelSecurity[]` | `PermissionSetSchema` |
1162
- * | `…[].using` / `…[].check` | `RowLevelSecurityPolicySchema` |
1163
- * | `sharingRules[]` | `ObjectStackSchema` |
1164
- * | `sharingRules[].condition` / `.sharedWith` / `.object` | `SharingRuleSchema` |
1165
- * | `objects[].tenancy` / `.systemFields` | `ObjectSchema` |
1166
- *
1167
- * NOT read, and each for a reason that is a schema fact:
1168
- *
1169
- * - `permissionSets` / `sharing` — not declared on the stack root. The root
1170
- * STRIPS them, so after parse they are `undefined` no matter what the author
1171
- * wrote. The declared spellings are `permissions` and `sharingRules`.
1172
- * - `sharingRules[].objectName` — `SharingRuleSchema` is `.strict()` and knows
1173
- * it only as a rejected name; `object` is required, so the canonical read can
1174
- * never be missing on a rule that parsed.
1175
- * - `objects[].rowLevelSecurity` / `objects[].rls` — **object-level RLS is not
1176
- * an authoring surface at all.** `ObjectSchema` declares neither key (nor
1177
- * does `authorable-surface.json` list one: the sole entry is
1178
- * `security/PermissionSet:rowLevelSecurity`), and `ObjectSchema` is
1179
- * `.strict()`, so a stack carrying one does not parse — it is refused with
1180
- * "Unrecognized key(s) on this object". Until #5009 this file walked that
1181
- * non-existent surface for ~20 lines, complete with an `objects[N].
1182
- * rowLevelSecurity[M].using` diagnostic path. Nothing could reach it, and the
1183
- * cost was never the missed finding: the next author to read this rule (human
1184
- * or AI) came away believing object-level RLS was a real authorization
1185
- * surface and wrote more code against it (#5008 nearly did). RLS policies
1186
- * live on a permission set; that is the branch above.
1187
- *
1188
- * Alias tolerance belongs at the schema's refusal, not in a consumer (Prime
1189
- * Directive #12) — where it also converts a loud, named rejection into a
1190
- * silently-inert gate. `validate-org-axis-red-lines.test.ts` pins all of this
1191
- * structurally: every key read here is checked against the declaring schema's
1192
- * own `.shape`, and every `findings.push` site must be reachable by a fixture
1193
- * that passed `safeParse`.
1194
- */
2042
+ declare function validateSecurityRoleWord(stack: AnyRec$q): SecurityFinding[];
2043
+
1195
2044
  declare const ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
1196
2045
  declare const ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
1197
2046
  type OrgAxisSeverity = 'error' | 'warning';
@@ -1273,86 +2122,6 @@ interface RlsPredicateFinding {
1273
2122
  */
1274
2123
  declare function validateRlsPredicateEnforceability(stack: unknown): RlsPredicateFinding[];
1275
2124
 
1276
- /**
1277
- * [ADR-0049 — references] Reference-integrity for dashboard header action
1278
- * targets (issue #3367).
1279
- *
1280
- * ADR-0049 established the "enforce-or-remove" gate for spec *properties*: a
1281
- * declared property the runtime does not honour is a false promise and must be
1282
- * enforced, marked experimental, or removed. This rule applies the SAME honesty
1283
- * principle to *references*. A dashboard header action names a target — a
1284
- * `script`/`modal` action, or a `url` route — that must actually resolve. A
1285
- * dangling target ships a button that renders and, on click, silently does
1286
- * nothing: a false affordance, exactly the failure ADR-0049 exists to prevent,
1287
- * just for a reference rather than a property.
1288
- *
1289
- * Nothing in the protocol schema can express this: `actionUrl` is a free string,
1290
- * so `{ actionType: 'script', actionUrl: 'export_dashboard_pdf' }` parses and
1291
- * ships even when no such action is defined anywhere in the stack.
1292
- *
1293
- * Surfaces checked:
1294
- * - dashboard `header.actions[]` — each `{ actionType, actionUrl }`
1295
- *
1296
- * ## The widget branch, and why it is gone (#5010)
1297
- *
1298
- * This rule used to check `widgets[].actionUrl` too, describing it as "the
1299
- * per-widget button" and claiming in this docblock that it "mirrors the objectui
1300
- * runtime dispatch". It did not: no renderer in either repo has ever drawn a
1301
- * per-widget action button — all 14 `actionUrl` reads in `DashboardRenderer` are
1302
- * scoped to `header.actions[]`. So the strictest arm of this rule (a dangling
1303
- * `script`/`modal` target is an ERROR, i.e. a failed build) was enforcing
1304
- * referential integrity for a button that could not render. An author could be
1305
- * blocked from shipping because a control that does not exist pointed at an
1306
- * action that also did not.
1307
- *
1308
- * That inversion — a rule written to delete false affordances, itself sustaining
1309
- * one — is why the widget keys were retired rather than the check merely
1310
- * relaxed: `widgets[].actionUrl` / `actionType` / `actionIcon` are now tombstoned
1311
- * in `@objectstack/spec` 17.0.0, so authoring one is a `tsc` error and a parse
1312
- * error carrying the prescription. There is no widget target left to resolve.
1313
- *
1314
- * Resolution mirrors the objectui runtime dispatch (`DashboardRenderer`
1315
- * hands the target to the SHARED `useActionModal` since objectui#4782) so the
1316
- * lint flags exactly what would fail to resolve at runtime:
1317
- *
1318
- * actionType 'script' → `actionUrl` must name a DEFINED action (`stack.actions`
1319
- * or any `object.actions`, by `name`). A script target that names no
1320
- * defined action fails open at runtime ("action not found"). → ERROR.
1321
- *
1322
- * actionType 'modal' → `actionUrl` names a declared PAGE (`stack.pages`), and
1323
- * only a page — maintainer ruling objectstack#6739-A (2026-08-09). This
1324
- * rule used to accept a defined action name, a bare object name, and the
1325
- * `<verb>_<object>` convention (create_/new_/add_/edit_/update_ + object),
1326
- * on the claim that it mirrored `DashboardView`'s own modal handler. That
1327
- * handler — the convention's last live copy — was deleted by objectui#4782
1328
- * (after objectui#4764 retired the object fallback in `useActionModal`):
1329
- * the runtime resolves a string modal target against page metadata and
1330
- * REFUSES everything else, so each retired limb here blessed a button that
1331
- * dispatches to a named refusal at runtime. The ruling explicitly declined
1332
- * the middle shape (keep the prefix, reject bare object names): a target
1333
- * names the page `create_opportunity`, or it names nothing. Opening an
1334
- * object's form is `actionType: 'form'`. Otherwise → ERROR.
1335
- *
1336
- * actionType 'url' → a relative in-app path. WARN when a recognizable
1337
- * `<collection>/<name>` segment (objects/reports/dashboards/pages/views)
1338
- * names an entity that does not exist in this stack. External URLs
1339
- * (`http(s)://`, `//`), interpolated targets (`${…}`), and opaque routes
1340
- * (no recognized collection segment) are skipped — they cannot be resolved
1341
- * statically and may be host/app/plugin routes. → WARNING.
1342
- *
1343
- * actionType 'flow' | 'api' — not checked: flow targets resolve against the
1344
- * automation engine / other packages, and api targets are opaque endpoints.
1345
- * Out of scope for #3367.
1346
- *
1347
- * Severity split follows the issue's acceptance criteria: an undefined
1348
- * `script`/`modal` target FAILS validation (a genuine dead reference that fails
1349
- * open at runtime as a dead button); an unresolved `url` route is advisory
1350
- * (route resolution is app-context-dependent, and a path may be served by
1351
- * another installed package or a host/console route). External, interpolated,
1352
- * convention, and opaque targets are exempted to keep false positives near zero
1353
- * — the same conservative posture as the sibling `lint-view-refs` and
1354
- * `validate-capability-references` rules.
1355
- */
1356
2125
  declare const DASHBOARD_ACTION_TARGET_UNDEFINED = "dashboard-action-target-undefined";
1357
2126
  declare const DASHBOARD_ACTION_ROUTE_UNRESOLVED = "dashboard-action-route-unresolved";
1358
2127
  type DashboardActionRefSeverity = 'error' | 'warning';
@@ -1370,13 +2139,13 @@ interface DashboardActionRefFinding {
1370
2139
  /** How to fix it. */
1371
2140
  hint: string;
1372
2141
  }
1373
- type AnyRec$l = Record<string, unknown>;
2142
+ type AnyRec$p = Record<string, unknown>;
1374
2143
  /**
1375
2144
  * Validate every dashboard header action reference in a stack. Returns
1376
2145
  * findings (empty = clean). `script`/`modal` dead targets are errors; `url`
1377
2146
  * unresolved routes are warnings.
1378
2147
  */
1379
- declare function validateDashboardActionRefs(stack: AnyRec$l): DashboardActionRefFinding[];
2148
+ declare function validateDashboardActionRefs(stack: AnyRec$p): DashboardActionRefFinding[];
1380
2149
 
1381
2150
  /**
1382
2151
  * Build-time filter-placeholder diagnostics (issue #3574).
@@ -1460,9 +2229,82 @@ interface FilterTokenFinding {
1460
2229
  */
1461
2230
  declare function validateFilterTokens(stack: Record<string, unknown> | undefined | null): FilterTokenFinding[];
1462
2231
 
2232
+ /** Diagnostic rule id. */
2233
+ declare const FLOW_FILTER_TOKEN_UNKNOWN = "flow-filter-token-unknown";
2234
+ interface FlowFilterTokenFinding {
2235
+ /** Always `error` — the node throws a guard refusal instead of running. */
2236
+ severity: 'error';
2237
+ rule: string;
2238
+ /** Human-readable location, e.g. `flow "opportunity_stagnation"`. */
2239
+ where: string;
2240
+ /** Config path, e.g. `flows[2].nodes[1].config.filter.close_date.$lt`. */
2241
+ path: string;
2242
+ message: string;
2243
+ hint: string;
2244
+ }
2245
+ /**
2246
+ * Validate flow-node filter placeholders across a schema-parsed stack.
2247
+ *
2248
+ * Pure `(stack) => Finding[]`; no I/O.
2249
+ */
2250
+ declare function validateFlowFilterTokens(stack: Record<string, unknown> | undefined | null): FlowFilterTokenFinding[];
2251
+
2252
+ /** The two names legal only as a whole token (`{TODAY() + 7}`), never in a call. */
2253
+ declare const FLOW_TEMPLATE_DATE_FUNCTIONS: readonly string[];
2254
+ /**
2255
+ * The value-expression function table — the CEL stdlib's numeric six, by the
2256
+ * #11060 ruling. Mirrors `EXPRESSION_FUNCTION_ARITY`'s key set.
2257
+ */
2258
+ declare const FLOW_TEMPLATE_VALUE_FUNCTIONS: readonly string[];
2259
+ /** What the flow template dialect does with one whole-string `{…}` token. */
2260
+ type FlowTemplateTokenVerdict =
2261
+ /** `{NOW()}` / `{TODAY() - 45}` — the evaluator resolves it. Legitimate. */
2262
+ {
2263
+ kind: 'date-function';
2264
+ name: string;
2265
+ }
2266
+ /** `{$User.Id}` — the evaluator resolves it from the run context. */
2267
+ | {
2268
+ kind: 'user-context';
2269
+ }
2270
+ /**
2271
+ * `{recordId}` / `{record.id}` — a `VariableMap` lookup, and the position
2272
+ * from which an unresolved name is handed to the filter dialect. OPEN: not
2273
+ * decidable from authored metadata, so never a finding.
2274
+ */
2275
+ | {
2276
+ kind: 'variable-path';
2277
+ head: string;
2278
+ }
2279
+ /**
2280
+ * A call to a name in NEITHER table. `resolveToken` throws
2281
+ * `FlowExpressionFunctionError` here (a guard refusal — a `fault` edge must
2282
+ * not swallow it), so the node cannot run. THIS is the finding.
2283
+ */
2284
+ | {
2285
+ kind: 'unknown-function';
2286
+ name: string;
2287
+ }
2288
+ /**
2289
+ * Anything else — junk shapes (`{30 days ago}`) and arithmetic over names
2290
+ * this module cannot resolve. `resolveToken` answers `undefined` and the
2291
+ * CRUD collapse guard (#3810) reports it at run time. Open, not judged.
2292
+ */
2293
+ | {
2294
+ kind: 'unresolvable-shape';
2295
+ };
2296
+ /**
2297
+ * Classify the INSIDE of one whole-string `{…}` filter token — `inner` is the
2298
+ * text between the braces, exactly as authored.
2299
+ *
2300
+ * Mirrors `resolveToken`'s dispatch order (see the module header). Holds no
2301
+ * severity and knows nothing about where the token was found.
2302
+ */
2303
+ declare function classifyFlowTemplateToken(inner: string): FlowTemplateTokenVerdict;
2304
+
1463
2305
  /**
1464
- * Build-time refusal of a bare dashboard date-range PRESET name in an ordering
1465
- * comparand (#8793 the ruled C half of #8690).
2306
+ * Build-time refusal of a bare dashboard date-range PRESET name authored as a
2307
+ * filter comparand — two arms, one wording.
1466
2308
  *
1467
2309
  * `last_7_days` / `last_30_days` / `last_90_days` and their ten calendar
1468
2310
  * siblings are REAL declared names (`DATE_RANGE_PRESETS`,
@@ -1488,10 +2330,10 @@ declare function validateFilterTokens(stack: Record<string, unknown> | undefined
1488
2330
  * (`['created_at', '>=', …]`) — and reports with the located `where` / `path`
1489
2331
  * the CLI commands render.
1490
2332
  *
1491
- * ## The boundary — ordering positions only, in all three shapes
2333
+ * ## Arm 1 — ordering positions, FIELD-AGNOSTIC (#8793)
1492
2334
  *
1493
- * This rule is field-agnostic, so the judgement rides on POSITION, exactly as
1494
- * the schema door's #8793 note lays out at length:
2335
+ * The judgement rides on POSITION alone, exactly as the schema door's #8793
2336
+ * note lays out at length:
1495
2337
  *
1496
2338
  * - **Judged:** `$gt` / `$gte` / `$lt` / `$lte` comparands and `$between`
1497
2339
  * endpoints (Mongo shape); `>` / `>=` / `<` / `<=` / `between` triples and
@@ -1499,17 +2341,105 @@ declare function validateFilterTokens(stack: Record<string, unknown> | undefined
1499
2341
  * `greater_than` / `less_than` / `greater_than_or_equal` /
1500
2342
  * `less_than_or_equal` / `before` / `after` / `between` view filter rules
1501
2343
  * and every alias `normalizeFilterOperator` folds onto them. An ORDERED
1502
- * comparison against a declared preset name has no legitimate reading.
1503
- * - **Not judged:** equality and membership (`=`, `equals`, `$eq`, `$ne`,
1504
- * `$in`, `$nin`, …). A select/picklist column legitimately stores values
1505
- * that collide with preset names (`GlobalFilterSchema`'s own pins protect
1506
- * `type: 'select', defaultValue: 'this_quarter'`), and on a declared
1507
- * temporal field the engine door still refuses these at query time with the
1508
- * field's type in hand.
2344
+ * comparison against a declared preset name has no legitimate reading on
2345
+ * ANY column, so no field type is needed to refuse it.
1509
2346
  * - **Only the 13 declared names.** A near-miss (`last_60_days`) is not this
1510
2347
  * rule's business — on a temporal field the engine's field-typed door
1511
2348
  * catches it; judging undeclared strings here would be a guessed superset.
1512
2349
  *
2350
+ * ## Arm 2 — equality and membership positions, FIELD-TYPED (#16106)
2351
+ *
2352
+ * Equality (`{ period: 'this_quarter' }`, `$eq`, `$ne`) and membership (`$in`
2353
+ * / `$nin`) CANNOT be judged blind: a select/picklist column legitimately
2354
+ * stores values that collide with preset names (`GlobalFilterSchema`'s own
2355
+ * pins protect `type: 'select', defaultValue: 'this_quarter'`), and equality
2356
+ * against such a stored value is a working filter. That is why the schema
2357
+ * door — field-agnostic by construction — keeps its ordering-only boundary,
2358
+ * and why this arm exists HERE, where the stack's object metadata is in hand:
2359
+ * maintainer-ruled 2026-09-06 (#16106, comment 5557019138, adopting
2360
+ * recommendation 1′): *at a layer that holds the object metadata, a declared
2361
+ * `date` / `datetime` field refuses one of the 13 declared preset names in
2362
+ * EVERY comparand position — bare (implicit equality), `$eq` / `$ne`, `$in` /
2363
+ * `$nin` and their view-rule and triple spellings, alongside the ordering
2364
+ * positions already judged.* Same message, same prescription (the #5240
2365
+ * convention): the window the preset already means is exactly what an author
2366
+ * writing `close_date == 'last_30_days'` intended.
2367
+ *
2368
+ * Measured first (ruling item 3), as real queries on a declared `date` field
2369
+ * (`close_date`) and a declared `datetime` sibling, on two real drivers —
2370
+ * `@objectstack/driver-memory` and `@objectstack/driver-sqlite-wasm` — 30 rows
2371
+ * seeded / 20 inside a 30-day window; both drivers answered identically:
2372
+ *
2373
+ * ```
2374
+ * close_date: 'last_30_days' REFUSED INVALID_FILTER / 400 (bare)
2375
+ * close_date: { $eq: 'last_30_days' } REFUSED INVALID_FILTER / 400
2376
+ * close_date: { $in: ['last_30_days'] } REFUSED INVALID_FILTER / 400
2377
+ * close_date: { $ne / $nin … } REFUSED INVALID_FILTER / 400
2378
+ * close_date: { $gte: '{30_days_ago}' } 200 count=20 <- positive control
2379
+ * close_date: '2026-09-03' 200 count=1 <- equality works on a date
2380
+ * stage: 'this_quarter' (select column) 200 count=10 <- the picklist case, alive
2381
+ * ```
2382
+ *
2383
+ * So at QUERY time the engine door already refuses every residue position on a
2384
+ * declared temporal field — the residue was purely an AUTHORING-time gap:
2385
+ * `objectstack lint` passed and `defineStack` accepted a filter the runtime
2386
+ * then refused with a 400 on first render. This arm closes that gap where the
2387
+ * AI author's correction loop can see it.
2388
+ *
2389
+ * ### Which object a filter is judged against
2390
+ *
2391
+ * `walkAuthoredFilters` finds the subtrees; this arm re-walks each subtree's
2392
+ * config path back through its ancestors and binds the filter to the NEAREST
2393
+ * ancestor that declares an object, in the spellings the carriers actually
2394
+ * use (each one the same read a sibling rule already makes at that position):
2395
+ *
2396
+ * - a literal `object` / `objectName` — a dataset, a summary field's child
2397
+ * object, a global filter's `optionsFrom`, a page data source, a
2398
+ * `record:related_list`'s `properties`, a time-relative trigger;
2399
+ * - a `dataset` name, resolved to that dataset's `object` — dashboard widgets
2400
+ * (`validate-widget-bindings`, #14148), reports and report blocks;
2401
+ * - `data: { provider: 'object', object }` — standalone views and list views
2402
+ * (`validate-list-view-field-refs`, #14107); any OTHER provider names no
2403
+ * object, so the position is unjudged;
2404
+ * - `config.objectName` / `config.object` — flow CRUD nodes
2405
+ * (`validate-flow-node-writes`), a templated `{…}` value skipped;
2406
+ * - `dataSource.object`, then `properties.object` / `properties.objectName`
2407
+ * — page components (`validate-page-field-bindings`);
2408
+ * - `publicPicker.object`, else the enclosing form field's `reference`
2409
+ * resolved on the view's object, else NOTHING — a form field's public-lookup
2410
+ * picker (`FormFieldPublicPickerSchema`) queries the REFERENCED object, so
2411
+ * its `filter` must never fall through to the parent form object (#16106
2412
+ * review finding B1: that fall-through was a false refusal wherever the two
2413
+ * objects share a field name with differing types);
2414
+ * - and, under `objects`, the object itself — its list views, tabs and
2415
+ * `relatedListFilter` (the filter runs over the CHILD rows, i.e. the object
2416
+ * that owns the field).
2417
+ *
2418
+ * The field key (a bare name, or a dotted relationship path) is then resolved
2419
+ * through `resolveFieldPath` (`object-graph.ts`), and the comparand is judged
2420
+ * only when the LEAF resolves to an author-declared field of type `date` or
2421
+ * `datetime` — the two types the ruling names.
2422
+ *
2423
+ * ### What this arm deliberately does NOT judge
2424
+ *
2425
+ * Every one of these is a MISSED CATCH (the engine door still refuses it at
2426
+ * query time, field type in hand), never a false build error:
2427
+ *
2428
+ * - a position no ancestor binds (an app-level filter, a dashboard-level
2429
+ * filter outside a widget), a dataset or object the stack does not declare,
2430
+ * an object with no readable field map, a view on a non-`object` provider;
2431
+ * - a leaf that resolves only as a registry-INJECTED column (`created_at`,
2432
+ * `updated_at`, …): the object graph carries no type for those — their type
2433
+ * is registry-owned and invisible here (`FieldPathVerdict`'s own contract);
2434
+ * - a `time` field: the ruling names `date` / `datetime`, and a wall-clock
2435
+ * column has no preset-shaped authoring slip worth a rule of its own;
2436
+ * - a field the object does not declare at all (a typo) — that is the
2437
+ * `*-filter-field-unknown` rules' finding, not a second one here.
2438
+ *
2439
+ * The implicit-equality position is reported under the operator it LOWERS to,
2440
+ * `$eq`, at the path of the field itself (`…filter.close_date`, no operator
2441
+ * segment), so the located path still says exactly what was authored.
2442
+ *
1513
2443
  * Both vocabularies' `{placeholder}` spellings never collide with a preset
1514
2444
  * name (a preset carries no braces), so this rule and `validate-filter-tokens`
1515
2445
  * cannot double-report one value.
@@ -1533,9 +2463,10 @@ interface PresetComparandFinding {
1533
2463
  /**
1534
2464
  * Validate every authored filter across a stack for bare preset comparands.
1535
2465
  *
1536
- * Pure `(stack) => Finding[]`; no I/O. Needs no resolution context the
1537
- * judgement is on the filter literal alone which is what qualifies it for
1538
- * the runtime publish gate's per-write snapshot.
2466
+ * Pure `(stack) => Finding[]`; no I/O. Arm 1 judges the filter literal alone;
2467
+ * arm 2 additionally reads the stack's own `objects` (and `datasets`, to bind
2468
+ * a widget or report) — both collections the runtime publish gate's per-write
2469
+ * snapshot carries — and stays silent wherever they are absent.
1539
2470
  */
1540
2471
  declare function validatePresetComparands(stack: Record<string, unknown> | undefined | null): PresetComparandFinding[];
1541
2472
 
@@ -1585,12 +2516,12 @@ interface ObjectRefFinding {
1585
2516
  /** How to fix it. */
1586
2517
  hint: string;
1587
2518
  }
1588
- type AnyRec$k = Record<string, unknown>;
2519
+ type AnyRec$o = Record<string, unknown>;
1589
2520
  /**
1590
2521
  * Validate every object-name reference on the surfaces listed in the module
1591
2522
  * header. Returns findings (empty = clean).
1592
2523
  */
1593
- declare function validateObjectReferences(stack: AnyRec$k): ObjectRefFinding[];
2524
+ declare function validateObjectReferences(stack: AnyRec$o): ObjectRefFinding[];
1594
2525
 
1595
2526
  type ReferenceIntegritySeverity = 'error' | 'warning';
1596
2527
  /**
@@ -1748,6 +2679,90 @@ type NavTargetRefFinding = ReferenceIntegrityFinding;
1748
2679
  declare const NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
1749
2680
  declare function validateNavTargetRefs(stack: unknown): NavTargetRefFinding[];
1750
2681
 
2682
+ /**
2683
+ * [#13216 — reference resolvability] A `type: 'page'` list view's `pageName`
2684
+ * must name a page the stack actually declares.
2685
+ *
2686
+ * ## What the schema already settles, and what it cannot
2687
+ *
2688
+ * `ListViewSchema`'s `checkListViewPageMount` refuses a `page` view with no
2689
+ * `pageName`, refuses `pageName` on any other view type, and refuses a
2690
+ * `pageName` outside `SnakeCaseIdentifierSchema`'s grammar — so by the time a
2691
+ * body reaches here the key is present, well-formed, and on the one view type
2692
+ * that reads it. What a per-body parse cannot ask is whether the page EXISTS,
2693
+ * because the answer lives in a sibling collection the body never sees. That is
2694
+ * this rule's whole question, and it is the same question
2695
+ * `validate-nav-target-refs` asks about the identical reference one surface
2696
+ * over — `{ type: 'page', pageName }` on an app navigation item.
2697
+ *
2698
+ * ## Severity: warning, for its twin's reason
2699
+ *
2700
+ * `validate-nav-target-refs` explains why the honest ceiling for a page
2701
+ * reference is advisory: unlike objects, pages have no curated
2702
+ * cross-package registry, so "unresolved here" cannot be told apart from
2703
+ * "provided by a package this stack cannot see". Nothing about mounting the
2704
+ * same page on a view changes that, so the verdict matches its twin rather
2705
+ * than inventing a second severity for one reference kind. `defineStack`'s
2706
+ * `validateCrossReferences` still hard-fails first whenever the stack DOES
2707
+ * declare pages — this rule is what speaks when that check has switched itself
2708
+ * off (`pageNames.size > 0`), which is exactly the state a stack is in when
2709
+ * the target was never written.
2710
+ *
2711
+ * ## Why it runs at the runtime publish gate too (`runtimeTypes: view`)
2712
+ *
2713
+ * The mount this rule guards is reachable in exactly the way #13100 measured:
2714
+ * an agent publishes a page through the metadata API, then writes a view that
2715
+ * mounts it. Both writes go through `PUT /api/v1/meta/view` — no CLI is
2716
+ * involved anywhere on that path, so a build-time-only rule would never speak
2717
+ * to the author who needs it. Crossing the member onto `view` snapshots is
2718
+ * therefore the point, not a bonus.
2719
+ *
2720
+ * It is safe to cross ONLY because the per-write snapshot now carries `pages`
2721
+ * (`RuntimeStackContext.pages`, added with this rule). Without that collection
2722
+ * the member would not go quiet — it would report EVERY page mount as dead,
2723
+ * which is the missing-collection false-positive channel
2724
+ * `ReferenceIntegrityRule.runtimeTypes` exists to keep closed, and the reason
2725
+ * `validateNavTargetRefs` is NOT crossed: nothing carries a snapshot's `apps`.
2726
+ *
2727
+ * ## The rungs, and why the list matches the sort rule's
2728
+ *
2729
+ * A `pageName` can be authored on every shape that carries a list view, and the
2730
+ * #9313 lesson is that declaring `runtimeTypes` is necessary and NOT sufficient
2731
+ * — the WALK has to reach the flattened top-level shape the write door actually
2732
+ * carries, or the crossing is a silent no-op that reads as coverage. So the
2733
+ * rungs are `validate-sortable-fields`' rungs, one key over:
2734
+ *
2735
+ * - `objects[].listViews.<key>` — built-in named list views;
2736
+ * - `views[]` itself on a FLATTENED LIST OVERLAY (`viewKind: 'list'`, no
2737
+ * nested `config`) — the shape the runtime gate snapshots as `views: [item]`;
2738
+ * - `views[].config` on a standalone ViewItem RECORD (`viewKind: 'list'` with
2739
+ * a record-shaped `config`) — the shape a Studio-saved view round-trips as;
2740
+ * - `views[].list` — a `defineView` aggregate's default list;
2741
+ * - `views[].listViews.<key>` — its named list views.
2742
+ *
2743
+ * ## Not covered, deliberately
2744
+ *
2745
+ * An interpolated target (`${…}`) resolves at render time and is skipped — the
2746
+ * conservative exemption `validate-nav-target-refs` and
2747
+ * `validate-object-references` both use (ADR-0072 D1). It cannot currently
2748
+ * arise (the schema's snake_case grammar refuses `$` and `{`), and is kept so
2749
+ * this rule does not become the thing that has to change if the grammar ever
2750
+ * widens.
2751
+ *
2752
+ * The view's `type` is NOT re-checked here. A body reaching a CLI command has
2753
+ * been parsed; a body reaching `os lint` may not have been, and in that state
2754
+ * reading `pageName` wherever it is written is the more useful answer — a
2755
+ * `pageName` on a `grid` view is a schema refusal, not this rule's business,
2756
+ * and reporting the dead reference underneath it as well helps rather than
2757
+ * misleads.
2758
+ */
2759
+
2760
+ type ViewPageRefSeverity = 'error' | 'warning';
2761
+ type ViewPageRefFinding = ReferenceIntegrityFinding;
2762
+ /** Emitted when a `type: 'page'` view mounts a page the stack cannot resolve. */
2763
+ declare const VIEW_PAGE_UNRESOLVED = "view-page-unresolved";
2764
+ declare function validateViewPageRefs(stack: unknown): ViewPageRefFinding[];
2765
+
1751
2766
  type NavObjectServabilityFinding = ReferenceIntegrityFinding;
1752
2767
  /** Emitted when a nav entry targets an object whose `enable` block cannot serve a list. */
1753
2768
  declare const NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
@@ -1903,7 +2918,7 @@ interface SearchableFieldFinding {
1903
2918
  * runtime would refuse are flagged (#4830).
1904
2919
  */
1905
2920
  type SearchableFieldRole = 'canonical' | 'narrowing';
1906
- type AnyRec$j = Record<string, unknown>;
2921
+ type AnyRec$n = Record<string, unknown>;
1907
2922
  /**
1908
2923
  * The slice of an object the search checks resolve against: the authored
1909
2924
  * field map (existence + the `type`/`hidden` meta search resolution reads),
@@ -1934,7 +2949,7 @@ interface ObjectSearchTarget {
1934
2949
  * `validate-react-page-props` — the gate that already parses that source —
1935
2950
  * runs the same `checkSearchableFieldList` core on it (#4329).
1936
2951
  */
1937
- declare function validateSearchableFields(stack: AnyRec$j): SearchableFieldFinding[];
2952
+ declare function validateSearchableFields(stack: AnyRec$n): SearchableFieldFinding[];
1938
2953
 
1939
2954
  declare const SORT_FIELD_UNKNOWN = "sort-field-unknown";
1940
2955
  declare const SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
@@ -1960,7 +2975,7 @@ interface SortableFieldFinding {
1960
2975
  /** How to fix it. */
1961
2976
  hint: string;
1962
2977
  }
1963
- type AnyRec$i = Record<string, unknown>;
2978
+ type AnyRec$m = Record<string, unknown>;
1964
2979
  /**
1965
2980
  * Check ONE authored `sort` declaration against the object it is bound to —
1966
2981
  * the shared core behind every list-view surface that declares an ordering.
@@ -1983,7 +2998,65 @@ declare function checkSortDeclaration(declared: unknown, objectName: string | un
1983
2998
  * over the bound object's own columns. See the module note for the four
1984
2999
  * sort-carrying surfaces that were checked and deliberately left out.
1985
3000
  */
1986
- declare function validateSortableFields(stack: AnyRec$i): SortableFieldFinding[];
3001
+ declare function validateSortableFields(stack: AnyRec$m): SortableFieldFinding[];
3002
+
3003
+ /** A list-view field reference that resolves to no field on the bound object. */
3004
+ declare const LIST_VIEW_FIELD_UNKNOWN = "list-view-field-unknown";
3005
+ /**
3006
+ * [#14282] A list-view field reference written as a DOTTED path at a position
3007
+ * whose name reaches a query door — where the door refuses it by name. See the
3008
+ * second-class section on this module for the scope and the measurements.
3009
+ */
3010
+ declare const LIST_VIEW_FIELD_DOTTED = "list-view-field-dotted";
3011
+ type ListViewFieldRefSeverity = 'error' | 'warning';
3012
+ interface ListViewFieldRefFinding {
3013
+ /** See the two-tier note on this module. */
3014
+ severity: ListViewFieldRefSeverity;
3015
+ /** Diagnostic rule id. */
3016
+ rule: string;
3017
+ /** Human-readable location, e.g. `view "task" › list › kanban`. */
3018
+ where: string;
3019
+ /** Config path, e.g. `views[0].list.kanban.groupByField`. */
3020
+ path: string;
3021
+ /** What is wrong. */
3022
+ message: string;
3023
+ /** How to fix it. */
3024
+ hint: string;
3025
+ }
3026
+ type AnyRec$l = Record<string, unknown>;
3027
+ /**
3028
+ * Validate every list view's field references against the object graph.
3029
+ * Returns findings (empty = clean). Pure `(stack) => Finding[]`; no I/O, and
3030
+ * safe on both the schema-parsed stack and the raw config the `lint` path
3031
+ * carries.
3032
+ */
3033
+ declare function validateListViewFieldRefs(stack: AnyRec$l): ListViewFieldRefFinding[];
3034
+
3035
+ /** An object-level field-name list entry that resolves to no field on the object. */
3036
+ declare const OBJECT_FIELD_REF_UNKNOWN = "object-field-ref-unknown";
3037
+ type ObjectFieldRefSeverity = 'error' | 'warning';
3038
+ interface ObjectFieldRefFinding {
3039
+ /** Always `error` — see the severity note on this module. */
3040
+ severity: ObjectFieldRefSeverity;
3041
+ /** Diagnostic rule id. */
3042
+ rule: string;
3043
+ /** Human-readable location, e.g. `object "proj_task" › highlightFields`. */
3044
+ where: string;
3045
+ /** Config path, e.g. `objects[0].highlightFields[1]`. */
3046
+ path: string;
3047
+ /** What is wrong. */
3048
+ message: string;
3049
+ /** How to fix it. */
3050
+ hint: string;
3051
+ }
3052
+ type AnyRec$k = Record<string, unknown>;
3053
+ /**
3054
+ * Validate every object's own field-name lists against the object graph.
3055
+ * Returns findings (empty = clean). Pure `(stack) => Finding[]`; no I/O, and
3056
+ * safe on both the schema-parsed stack and the raw config the `lint` path
3057
+ * carries.
3058
+ */
3059
+ declare function validateObjectFieldRefs(stack: AnyRec$k): ObjectFieldRefFinding[];
1987
3060
 
1988
3061
  declare const ACTION_NAME_UNDEFINED = "action-name-undefined";
1989
3062
  type ActionNameRefSeverity = 'error' | 'warning';
@@ -2001,62 +3074,13 @@ interface ActionNameRefFinding {
2001
3074
  /** How to fix it. */
2002
3075
  hint: string;
2003
3076
  }
2004
- type AnyRec$h = Record<string, unknown>;
3077
+ type AnyRec$j = Record<string, unknown>;
2005
3078
  /**
2006
3079
  * Validate every name-bound action reference in a stack. Returns findings
2007
3080
  * (empty = clean).
2008
3081
  */
2009
- declare function validateActionNameRefs(stack: AnyRec$h): ActionNameRefFinding[];
2010
-
2011
- /**
2012
- * [ADR-0078 Phase 3 — Tier-A `action-locations`] An action nobody placed.
2013
- *
2014
- * `locations` is an action's placement declaration. An action that omits it —
2015
- * and that no view names in `bulkActions` / `bulkActionDefs` / `rowActions` —
2016
- * has no surface at all: it parses, it publishes, Setup lists it, and no user
2017
- * can ever click it. ADR-0078 names this shape in its opening paragraph ("a
2018
- * `summary` with no `summaryOperations`; **an `action` with no `locations`**;
2019
- * … Each parses, 'renders', reports success — and does nothing") and Phase 3
2020
- * asks for exactly this rule, one verified shape at a time.
2021
- *
2022
- * The renderer half is now unambiguous: objectui#3142 collapsed four
2023
- * disagreeing renderers onto one predicate — an action renders at a location
2024
- * only if it DECLARES that location. Before that, `action:bar` and the record
2025
- * header showed an undeclared action *everywhere*, which is what made this
2026
- * shape look alive; it is measurably inert as of objectui 17.1.
2027
- *
2028
- * ## What is NOT flagged, and why
2029
- *
2030
- * **`locations: []` — a deliberate headless action.** `content/docs/ui/
2031
- * actions.mdx` ("Headless actions: declare it, then hide it") documents the
2032
- * empty array as a first-class shape: the action stays callable over REST /
2033
- * MCP / AI and keeps its capability gate, param contract and audit trail,
2034
- * while claiming no UI surface. ADR-0110 D3 refuses an *undeclared* handler,
2035
- * so a headless declaration is the only legal way to expose such an action —
2036
- * flagging it would fight that ADR. The distinction this rule draws is
2037
- * therefore between an author who said "nowhere, deliberately" (`[]`) and one
2038
- * who never said anything at all (key absent).
2039
- *
2040
- * **Actions a view places by NAME.** Naming an action in a list view's
2041
- * `bulkActions` or `bulkActionDefs` IS its placement — the selection bar is
2042
- * driven by the view, never by `locations` (that is what the retired
2043
- * `action.bulkEnabled` tombstone prescribes, and what objectui#3139's
2044
- * aggregate bulk mode relies on: an action that only makes sense over a
2045
- * selection has no single-record location by construction). `rowActions` is
2046
- * exempted on the same zero-false-positive posture (ADR-0072 D1): it is the
2047
- * same field pair on the same container, and an author who named an action
2048
- * there has stated an intent — a name that resolves to nothing is already
2049
- * `action-name-undefined`'s job, not this rule's.
2050
- *
2051
- * Scope note: this rule asks only "did anyone place this action?". It
2052
- * deliberately does NOT check that a declared location is one a renderer
2053
- * actually serves, nor that a view's named action belongs to that view's
2054
- * object — distinct classes with their own rules. Cross-package placement (a
2055
- * view in another installed package naming this action) is the one legitimate
2056
- * miss, which is why this is a **warning**: like every other "declared but
2057
- * does nothing" finding in this package (`validateSemanticRoles`,
2058
- * `lintLivenessProperties`), it is high-signal and never fatal.
2059
- */
3082
+ declare function validateActionNameRefs(stack: AnyRec$j): ActionNameRefFinding[];
3083
+
2060
3084
  declare const ACTION_NO_PLACEMENT = "action-no-placement";
2061
3085
  type ActionLocationsSeverity = 'error' | 'warning';
2062
3086
  interface ActionLocationsFinding {
@@ -2073,12 +3097,12 @@ interface ActionLocationsFinding {
2073
3097
  /** How to fix it. */
2074
3098
  hint: string;
2075
3099
  }
2076
- type AnyRec$g = Record<string, unknown>;
3100
+ type AnyRec$i = Record<string, unknown>;
2077
3101
  /**
2078
3102
  * Flag every action that declares no placement and that no view places by
2079
3103
  * name. Returns findings (empty = clean).
2080
3104
  */
2081
- declare function validateActionLocations(stack: AnyRec$g): ActionLocationsFinding[];
3105
+ declare function validateActionLocations(stack: AnyRec$i): ActionLocationsFinding[];
2082
3106
 
2083
3107
  /**
2084
3108
  * Shared page-component traversal for the lint rules that inspect
@@ -2104,11 +3128,11 @@ declare function validateActionLocations(stack: AnyRec$g): ActionLocationsFindin
2104
3128
  * author never wrote, so those pages are skipped here and covered by
2105
3129
  * `validate-jsx-pages` / `validate-react-page-props` instead.
2106
3130
  */
2107
- type AnyRec$f = Record<string, unknown>;
3131
+ type AnyRec$h = Record<string, unknown>;
2108
3132
  /** A visited component plus everything needed to locate and bind it. */
2109
3133
  interface WalkedComponent {
2110
3134
  /** The component record itself. */
2111
- component: AnyRec$f;
3135
+ component: AnyRec$h;
2112
3136
  /** Config path, e.g. `pages[0].regions[1].components[2]`. */
2113
3137
  path: string;
2114
3138
  /**
@@ -2119,14 +3143,92 @@ interface WalkedComponent {
2119
3143
  objectName?: string;
2120
3144
  }
2121
3145
  /** Is this page authored as `source` (so its `regions` must not be linted)? */
2122
- declare function isSourceAuthoredPage(page: AnyRec$f): boolean;
3146
+ declare function isSourceAuthoredPage(page: AnyRec$h): boolean;
2123
3147
  /**
2124
3148
  * Walk every component on a page, depth-first, yielding each with its config
2125
3149
  * path and resolved object binding. Source-authored pages yield nothing.
2126
3150
  *
2127
3151
  * `pagePath` is the caller's path prefix for the page (e.g. `pages[3]`).
3152
+ *
3153
+ * The descent is cycle-safe. Every composition slot below is `z.array(z.unknown())`
3154
+ * authored data, so a component that contains itself — directly, or through a
3155
+ * chain of containers — is LEGAL input, and an unguarded walk recurses until the
3156
+ * stack dies. That crash is not scoped to one rule: this is the one shared
3157
+ * traversal under every page-shaped lint rule and the CLI's i18n object-sections
3158
+ * pass, so it takes all of them down in the same process.
3159
+ *
3160
+ * The guard is an ANCESTOR set, not a visited set, and the difference is
3161
+ * load-bearing rather than stylistic — it is the same predicate
3162
+ * `translatePage` (`packages/spec/src/system/i18n-resolver.ts`) settled on. A
3163
+ * component object reused twice as a SIBLING is two legitimate placements at two
3164
+ * distinct config paths, and every rule built on this walk must see both; a
3165
+ * visited set would yield the first and silently drop the second, converting a
3166
+ * crash into missing lint coverage. Only a node that is its own ancestor is a
3167
+ * cycle.
3168
+ *
3169
+ * A cycle stops the descent SILENTLY — the repeated node is not yielded a second
3170
+ * time and no finding is produced. The guard is a safety property of the walk,
3171
+ * not a verdict about the document: deciding that a self-referential page is
3172
+ * itself an authoring error would be new reject behaviour on authored input, and
3173
+ * that is a contract call, not this walk's to make.
3174
+ *
3175
+ * Deliberately NO depth cap, which is a different instrument (`translatePage`
3176
+ * carries both). A cap bounds a legal-but-absurd document; on a resolver it
3177
+ * leaves copy untranslated, but on a LINT walk it would drop real components
3178
+ * from the walk output and every rule would go quiet about them — a silent loss
3179
+ * of coverage that looks exactly like a clean page. With the cycle guard the
3180
+ * descent is bounded by the document's own finite nesting, so the cap would only
3181
+ * ever fire on acyclic input, which is precisely the input it must not truncate.
2128
3182
  */
2129
- declare function walkPageComponents(page: AnyRec$f, pagePath: string): WalkedComponent[];
3183
+ declare function walkPageComponents(page: AnyRec$h, pagePath: string): WalkedComponent[];
3184
+
3185
+ /**
3186
+ * object name → its DECLARED field-group keys.
3187
+ *
3188
+ * Declared, not derived: `deriveFieldGroupLayout` drops a group no visible field
3189
+ * joins, and a reference to a declared-but-empty group is a different (and
3190
+ * data-dependent) finding from a reference to a group that was never declared.
3191
+ * This index answers only the second question — the one with a closed oracle.
3192
+ *
3193
+ * `fieldGroups` is an ARRAY on `ObjectSchema`, and `recordsOf` additionally
3194
+ * resolves the name-keyed map shape that raw (non-`defineStack`) metadata can
3195
+ * carry, the same tolerance every other index in this package extends.
3196
+ */
3197
+ declare function indexObjectFieldGroups(stack: unknown): Map<string, Set<string>>;
3198
+ /** One `section.group` reference, with the path that located it. */
3199
+ interface SectionGroupRef {
3200
+ /** The referenced field-group key, as authored. */
3201
+ key: string;
3202
+ /** Config path of the `group` key itself. */
3203
+ path: string;
3204
+ }
3205
+ /**
3206
+ * Pull `group` references out of a `sections`-shaped value.
3207
+ *
3208
+ * `sep` joins the index onto `basePath` the way the calling surface addresses
3209
+ * itself — `.` for metadata config paths, and the react surface's ` › ` if it
3210
+ * ever grows a section-bearing block.
3211
+ */
3212
+ declare function sectionGroupRefs(sections: unknown, basePath: string, sep?: string): SectionGroupRef[];
3213
+ /** The finding shape both host rules already declare, structurally. */
3214
+ interface SectionGroupFinding {
3215
+ severity: 'warning';
3216
+ rule: string;
3217
+ where: string;
3218
+ path: string;
3219
+ message: string;
3220
+ hint: string;
3221
+ }
3222
+ /**
3223
+ * Check one batch of group references against `objectName`'s declared groups.
3224
+ *
3225
+ * Bails out when the object is not defined in this stack — it may come from
3226
+ * another installed package, and a group cannot be judged on a schema we cannot
3227
+ * see. That is the same skip {@link checkFieldRefs} takes, and for the same
3228
+ * reason: silence about an unknowable object beats a finding the author cannot
3229
+ * act on.
3230
+ */
3231
+ declare function checkSectionGroupRefs(refs: readonly SectionGroupRef[], objectName: string | undefined, objectFieldGroups: ReadonlyMap<string, Set<string>>, where: string, rule: string): SectionGroupFinding[];
2130
3232
 
2131
3233
  /**
2132
3234
  * [ADR-0078 — completeness] Field-reference integrity for page components
@@ -2193,6 +3295,14 @@ declare const PAGE_FIELD_UNKNOWN = "page-field-unknown";
2193
3295
  * #8116's severity reasoning, unchanged.
2194
3296
  */
2195
3297
  declare const PAGE_FIELD_UNPROVISIONED = "page-field-unprovisioned";
3298
+ /**
3299
+ * [#13855] A section's `group` names a field group the bound object does not
3300
+ * declare. The reference form inherits the section's whole membership from that
3301
+ * group, so a dangling key leaves the section with no members and it does not
3302
+ * render at all. Always `warning` — see `object-field-groups.ts` for why this
3303
+ * sits with the family's advisory findings rather than gating.
3304
+ */
3305
+ declare const PAGE_SECTION_GROUP_UNKNOWN = "page-section-group-unknown";
2196
3306
  type PageFieldSeverity = 'error' | 'warning';
2197
3307
  interface PageFieldFinding {
2198
3308
  /**
@@ -2214,7 +3324,59 @@ interface PageFieldFinding {
2214
3324
  hint: string;
2215
3325
  }
2216
3326
 
2217
- declare function validatePageFieldBindings(stack: AnyRec$f): PageFieldFinding[];
3327
+ declare function validatePageFieldBindings(stack: AnyRec$h): PageFieldFinding[];
3328
+
3329
+ declare const PAGE_VISUALIZATION_WITHOUT_BINDING = "page/visualization-without-binding";
3330
+ type PageVisualizationSeverity = 'error' | 'warning';
3331
+ interface PageVisualizationFinding {
3332
+ /**
3333
+ * `error` when the entry leads `allowedVisualizations` — it becomes the
3334
+ * forced `viewType` and the runtime reaches its renderer's refusal screen.
3335
+ * `warning` otherwise — the type is filtered out of the switcher silently.
3336
+ */
3337
+ severity: PageVisualizationSeverity;
3338
+ /** Diagnostic rule id. */
3339
+ rule: string;
3340
+ /** Human-readable location, e.g. `page "showcase_task_calendar" · interfaceConfig.appearance`. */
3341
+ where: string;
3342
+ /** Config path, e.g. `pages[0].interfaceConfig.appearance.allowedVisualizations[1]`. */
3343
+ path: string;
3344
+ /** What is wrong. */
3345
+ message: string;
3346
+ /** How to fix it. */
3347
+ hint: string;
3348
+ }
3349
+ /**
3350
+ * One row of the renderer's derivation, transcribed.
3351
+ *
3352
+ * `types` is the TYPE predicate (checked first, in the object's declaration
3353
+ * order); `namePattern` is the NAME fallback's regex SOURCE, or `null` where
3354
+ * the renderer has no name fallback for that visualization. `viewBlocks` are
3355
+ * the keys on the referenced view that supply the binding directly — read
3356
+ * ahead of any derivation, exactly as `view.<viz> ?? derived` does.
3357
+ */
3358
+ interface VisualizationPredicate {
3359
+ visualization: string;
3360
+ /** The binding key the renderer would produce (what the message names). */
3361
+ binding: string;
3362
+ types: readonly string[];
3363
+ namePattern: string | null;
3364
+ viewBlocks: readonly string[];
3365
+ }
3366
+ /**
3367
+ * The mirrored table — objectui `f0f774b0`,
3368
+ * `packages/app-shell/src/views/InterfaceListPage.tsx`:
3369
+ * `SELECT_TYPES`/`DATE_TYPES`/`IMAGE_TYPES`/`LOCATION_TYPES` at `:149-151`
3370
+ * and `:203`, `defaultKanbanFromObject` `:153`, `defaultDateField` `:163`,
3371
+ * `defaultCalendarFromObject` `:170`, `defaultGalleryFromObject` `:175`,
3372
+ * `defaultGanttFromObject` `:185`, `defaultMapFromObject` `:253`.
3373
+ *
3374
+ * ⛔ Pinned verbatim by the fixture test. Editing a row here without editing
3375
+ * that pin is the drift this table's whole purpose is to make loud.
3376
+ */
3377
+ declare const OBJECTUI_DERIVATION_PREDICATES: readonly VisualizationPredicate[];
3378
+ type AnyRec$g = Record<string, unknown>;
3379
+ declare function validatePageVisualizationBindings(stack: AnyRec$g): PageVisualizationFinding[];
2218
3380
 
2219
3381
  /** A key authored in `properties` that the type's props schema does not declare. */
2220
3382
  declare const COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
@@ -2240,7 +3402,24 @@ interface ComponentPropsFinding {
2240
3402
  /** How to fix it. */
2241
3403
  hint: string;
2242
3404
  }
2243
- declare function validateComponentProps(stack: AnyRec$f): ComponentPropsFinding[];
3405
+ declare function validateComponentProps(stack: AnyRec$h): ComponentPropsFinding[];
3406
+
3407
+ /** A component `type` inside a spec-reserved namespace that the vocabulary does not declare. */
3408
+ declare const COMPONENT_TYPE_UNKNOWN = "component-type-unknown";
3409
+ interface ComponentTypeFinding {
3410
+ severity: 'error';
3411
+ /** Diagnostic rule id. */
3412
+ rule: string;
3413
+ /** Human-readable location, e.g. `page "app_launcher" · global:serch`. */
3414
+ where: string;
3415
+ /** Config path, e.g. `pages[0].regions[1].components[0].type`. */
3416
+ path: string;
3417
+ /** What is wrong. */
3418
+ message: string;
3419
+ /** How to fix it. */
3420
+ hint: string;
3421
+ }
3422
+ declare function validateComponentTypes(stack: AnyRec$h): ComponentTypeFinding[];
2244
3423
 
2245
3424
  /**
2246
3425
  * [ADR-0021 — semantic layer] Chart-binding integrity for the surfaces the
@@ -2277,6 +3456,132 @@ declare function validateComponentProps(stack: AnyRec$f): ComponentPropsFinding[
2277
3456
  * rule has no business loading. It is checked by `validate-react-page-props`
2278
3457
  * instead, against the naming convention `chartAggregateResultKeys`
2279
3458
  * (`@objectstack/spec/ui`) now pins down (#3701).
3459
+ *
3460
+ * ## Which positions are BINDINGS, and which are presentation (#15575)
3461
+ *
3462
+ * `chart-measure-unknown`'s message names a QUERY consequence — *"this series
3463
+ * comes back empty"* — and that is only true where the position feeds the
3464
+ * dataset query. Read at the pinned `@object-ui` revision (`.objectui-sha`),
3465
+ * the three surfaces do not agree, so the tier and the consequence are per
3466
+ * POSITION rather than per rule:
3467
+ *
3468
+ * - **Report charts.** The chart runs its OWN query out of the two axis
3469
+ * strings — `useDatasetRows(dataset, [xAxis], [yAxis], …)` in
3470
+ * `plugin-report/src/DatasetReportRenderer.tsx`, and in that file's own
3471
+ * words *"the embedded chart queries only `chart.xAxis` × `chart.yAxis`"*.
3472
+ * So `chart.yAxis` IS the binding and keeps `error`. `chart.series[]` is
3473
+ * not: it is *"The author's per-chart override for ONE measure's display
3474
+ * name — the entry of `chart.series[]` whose `name` IS that measure"*,
3475
+ * lowered through `mergeAuthoredSeries`, for which *"an authored entry
3476
+ * naming a measure that is NOT in the dataset selection is **ignored** —
3477
+ * membership belongs to the dataset"* (`@object-ui/core`
3478
+ * `src/utils/chart-presentation.ts`).
3479
+ * - **List-view charts.** `ListChartConfigSchema` is a STRICT object of
3480
+ * `chartType` / `dataset` / `dimensions` / `values`: it declares no
3481
+ * `series` and no `yAxis`, so this rule has no presentation position on
3482
+ * that surface at all. Its one measure position is `values[]`, which
3483
+ * `app-shell/src/views/ObjectView.tsx` hands to the chart component as the
3484
+ * dataset measures (and synthesises the series list FROM). Query binding,
3485
+ * `error`, unchanged.
3486
+ * - **Dataset-bound page chart components.** `plugin-charts/src/ObjectChart.tsx`
3487
+ * queries `{ dimensions: schema.dimensions, measures: schema.values }` and
3488
+ * then REPLACES the authored series wholesale —
3489
+ * `{ ...schema, data, xAxisKey, series: datasetChart.series }`, one derived
3490
+ * entry per selected measure. An authored `properties.series[].name` reaches
3491
+ * the renderer not at all, and an authored `properties.yAxis[].field` is
3492
+ * inert for the same reason (`normalizeChartSchema` synthesises series from
3493
+ * `yAxis[].field` only when there are none, which on this surface means an
3494
+ * empty `values` — a chart with no measures to plot either way). Both are
3495
+ * presentation; `dimensions` / `values` are the binding.
3496
+ *
3497
+ * So the presentation positions drop to `warning` and state what actually
3498
+ * happens, which is the resolution the maintainer ruling on
3499
+ * `chart-field-unknown` reached for the same question one rule over (see "The
3500
+ * three refused binding keys" in `validate-widget-bindings.ts`). Two things
3501
+ * follow that differ from that sibling, and are worth stating rather than
3502
+ * leaving to be re-derived:
3503
+ *
3504
+ * - **There is no per-position suppression here.** `suppressWarnings` is
3505
+ * declared on the dashboard WIDGET only (`spec/src/ui/dashboard.zod.ts`),
3506
+ * and none of these three surfaces carries the key. A `warning` is advisory
3507
+ * (the consumers split on `severity === 'error'`) but cannot be silenced
3508
+ * individually; declaring the key on these surfaces would be a schema
3509
+ * change, which this rule has no standing to make.
3510
+ * - **The page surface's axis refs and series refs are separate limbs.** They
3511
+ * used to be concatenated into one `series` array before the measure walk,
3512
+ * so every `yAxis[].field` took the SERIES message. Reading both shapes on
3513
+ * that surface is deliberate (the props bag mixes them); giving them one
3514
+ * message was not — the pin refuses the two for different reasons, so they
3515
+ * now carry different sentences.
3516
+ *
3517
+ * `chart-axis-not-selected` (declared measure, outside the selection) rides the
3518
+ * same walk and took the same one-size sentence — *"the query does not return
3519
+ * it, so the series plots nothing"*. That is the truth at a query position and
3520
+ * not at a presentation one, where no series is derived for the name in the
3521
+ * first place, so its consequence is per position too.
3522
+ *
3523
+ * ## Which SET `chart-axis-not-selected` resolves against (#15734)
3524
+ *
3525
+ * The consequence is per POSITION; the selection it is measured against is per
3526
+ * SURFACE — and on the report surface that selection is not `report.values`.
3527
+ * At the same pinned revision `DatasetReportRenderer.tsx` runs
3528
+ * `useDatasetRows(dataset, plan.kind === 'series' && xAxis ? [xAxis] : [],
3529
+ * wantsQuery && yAxis ? [yAxis] : [], …)` and derives the plotted series with
3530
+ * `buildChartSeries(…, [xAxis], [yAxis], …)` — *"the selection is exactly one
3531
+ * dimension × one measure, so this takes the helper's single-dimension branch
3532
+ * and returns ONE series"*. `report.values` is the selection of the TABLE
3533
+ * beneath the chart; the chart's own is the axis pair. Two things follow:
3534
+ *
3535
+ * - **Nothing to report at the report `chart.yAxis`.** That position IS the
3536
+ * chart's query, so it cannot fail to select itself. The warning that fired
3537
+ * for a declared measure outside `report.values` stated a query consequence
3538
+ * its own pin refutes: the chart asks for exactly that measure and plots
3539
+ * it. `chart-measure-unknown` at that position is untouched — an UNDECLARED
3540
+ * measure still returns no column, and still gates.
3541
+ * - **`chart.series[].name` resolves against the singleton `{ chart.yAxis }`.**
3542
+ * The override is paired with a DERIVED series and the chart derives one,
3543
+ * so that singleton — not `report.values` — is the set an entry can land
3544
+ * on. An entry naming `chart.yAxis` lands however the table is selected;
3545
+ * one naming any other declared measure lands on nothing.
3546
+ *
3547
+ * The list-view and page-component surfaces keep `values`: there it IS the
3548
+ * measure set the query asks for, so the existing resolution is correct.
3549
+ *
3550
+ * ## What a REPORT binds, chart or no chart (#16105)
3551
+ *
3552
+ * A report is dataset-bound in its own right — `ReportSchema` requires
3553
+ * `dataset` + `values` on every non-`joined` report and declares `rows` (the
3554
+ * down axis) and `columns` (the across axis a `matrix` pivots on, ADR-0021 D2)
3555
+ * as dimension names "from the dataset". The chart is optional decoration on
3556
+ * top of that binding, not the binding itself.
3557
+ *
3558
+ * The walk did not read it that way. Every report position reached the resolver
3559
+ * through one closure that opened `if (!isRec(chart)) return`, and that closure
3560
+ * was the only site `report.dataset` was ever passed to, so:
3561
+ *
3562
+ * - a report authored WITHOUT a chart was not checked at all — an unresolvable
3563
+ * `dataset`, an unknown `rows`/`columns` dimension and an unknown `values`
3564
+ * measure all published clean; and
3565
+ * - `rows`/`columns` were passed to the resolver on NO report, charted or not.
3566
+ * On one and the same charted report, `values` was resolved and the
3567
+ * dimension selection beside it was not.
3568
+ *
3569
+ * So the dataset is resolved once per report and once per block — before the
3570
+ * chart question is asked — and fed to two groups of positions: the report's
3571
+ * own selection (`rows`/`columns` → `chart-dimension-unknown`, `values` →
3572
+ * `chart-measure-unknown`) and, when a chart is present, its axis refs
3573
+ * exactly as before. Not a second "chartless reports too" pass after the early
3574
+ * return: one path, entered unconditionally, with the chart as the branch it
3575
+ * always was. Which is also why an unresolvable dataset on a charted report is
3576
+ * still ONE finding — `resolveDataset` runs once per surface, not once per
3577
+ * group.
3578
+ *
3579
+ * `rows`/`columns` take `chart-dimension-unknown` rather than an id of their
3580
+ * own: the position is a dataset DIMENSION reference resolved against the
3581
+ * dataset's declared dimensions, which is what that rule already means on the
3582
+ * list-view and page surfaces, and the spec's own words for the two report
3583
+ * keys are "down axis" and "across" — so the rule's existing sentence about an
3584
+ * axis rendering with no categories is true where it now fires.
2280
3585
  */
2281
3586
  declare const CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
2282
3587
  declare const CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
@@ -2297,7 +3602,265 @@ interface ChartBindingFinding {
2297
3602
  hint: string;
2298
3603
  }
2299
3604
 
2300
- declare function validateChartBindings(stack: AnyRec$f): ChartBindingFinding[];
3605
+ declare function validateChartBindings(stack: AnyRec$h): ChartBindingFinding[];
3606
+
3607
+ /** An `include[]` entry that does not name a joinable relationship path. */
3608
+ declare const DATASET_INCLUDE_UNKNOWN = "dataset-include-unknown";
3609
+ /** A `dimensions[].field` / `measures[].field` path that resolves to no column. */
3610
+ declare const DATASET_FIELD_UNKNOWN = "dataset-field-unknown";
3611
+ /** A field path whose relationship prefix was never declared in `include`. */
3612
+ declare const DATASET_FIELD_NOT_INCLUDED = "dataset-field-not-included";
3613
+ /** A filter KEY — on the dataset or a measure — that resolves to no column. */
3614
+ declare const DATASET_FILTER_FIELD_UNKNOWN = "dataset-filter-field-unknown";
3615
+ type DatasetRefSeverity = 'error' | 'warning';
3616
+ interface DatasetRefFinding {
3617
+ /** Always `error` today — see the severity note on this module. */
3618
+ severity: DatasetRefSeverity;
3619
+ /** Diagnostic rule id. */
3620
+ rule: string;
3621
+ /** Human-readable location, e.g. `dataset "sales" › dimension "region"`. */
3622
+ where: string;
3623
+ /** Config path, e.g. `datasets[0].dimensions[2].field`. */
3624
+ path: string;
3625
+ /** What is wrong. */
3626
+ message: string;
3627
+ /** How to fix it. */
3628
+ hint: string;
3629
+ }
3630
+ type AnyRec$f = Record<string, unknown>;
3631
+ /**
3632
+ * Validate every ADR-0021 dataset's references against the object graph.
3633
+ * Returns findings (empty = clean). Pure `(stack) => Finding[]`; no I/O, and
3634
+ * safe on both the schema-parsed stack and the raw config the `lint` path
3635
+ * carries.
3636
+ */
3637
+ declare function validateDatasetReferences(stack: AnyRec$f): DatasetRefFinding[];
3638
+
3639
+ /**
3640
+ * Field types that address ANOTHER object, so a `.<hop>` through one is a join
3641
+ * the query compiler derives rather than a column read.
3642
+ *
3643
+ * `user` and `tree` are members alongside the two obvious ones for the reason
3644
+ * `validate-flow-template-paths.ts` lists them: both store a foreign key and
3645
+ * both are traversed the same way. A dataset `include` naming one is joinable
3646
+ * exactly as a `lookup` is.
3647
+ */
3648
+ declare const RELATIONSHIP_FIELD_TYPES: ReadonlySet<string>;
3649
+ /** The slice of one field this module reads. */
3650
+ interface GraphField {
3651
+ /** Declared `type`, when the author wrote one. */
3652
+ type?: string;
3653
+ /**
3654
+ * The object a relationship field addresses.
3655
+ *
3656
+ * `reference` is the ONLY spelling `FieldSchema` declares — `referenceTo` /
3657
+ * `relatedTo` / `target` / `targetObject` / `lookupObject` are REJECTED
3658
+ * aliases the strict error map maps back to it (#5017), so a field spelling
3659
+ * one of them does not parse at all. Re-admitting an alias here would be the
3660
+ * tolerant consumer Prime Directive #12 refuses, so only `reference` is read.
3661
+ */
3662
+ reference?: string;
3663
+ /**
3664
+ * The declared `multiple: true` flag, when the author wrote one.
3665
+ *
3666
+ * Read here because the dotted-path verdict a caller may reach for
3667
+ * ({@link classifyDottedFilterHead} in `@objectstack/spec/data`) is a
3668
+ * function of BOTH `type` and `multiple`: an array-valued head is
3669
+ * deliberately unjudged there, since a numeric-index dotted path genuinely
3670
+ * reaches into it on two of three backends. A caller handed only `type`
3671
+ * would have to re-derive the flag from the raw stack, which is the second
3672
+ * copy this module exists to prevent. Additive (#14282): every existing
3673
+ * consumer that ignores the key keeps its verdicts byte-for-byte.
3674
+ */
3675
+ multiple?: boolean;
3676
+ }
3677
+ /**
3678
+ * One object's resolvable surface. `null` in the index marks an object that
3679
+ * declares no readable field map, keeping "declared nothing" distinguishable
3680
+ * from "not in this stack" — the same distinction
3681
+ * {@link indexObjectSearchTargets} draws for the search axis.
3682
+ */
3683
+ interface GraphObject {
3684
+ /** Authored field names. */
3685
+ names: ReadonlySet<string>;
3686
+ /** name → the slice above. */
3687
+ fields: ReadonlyMap<string, GraphField>;
3688
+ /** Registry-injected columns addressable on THIS object (skip 3). */
3689
+ injected: ReadonlySet<string>;
3690
+ }
3691
+ /** object name → its resolvable surface, or `null` (skip 2). */
3692
+ type ObjectGraph = ReadonlyMap<string, GraphObject | null>;
3693
+ /**
3694
+ * Index a stack's objects once. Callers that resolve many paths build this a
3695
+ * single time and hand it to every {@link resolveFieldPath} call.
3696
+ */
3697
+ declare function indexObjectGraph(stack: unknown): ObjectGraph;
3698
+ /** What a path resolved to, or precisely how it failed. */
3699
+ type FieldPathVerdict =
3700
+ /**
3701
+ * Every hop and the leaf resolved. `object` is the object the LEAF lives on.
3702
+ * `injected` marks a leaf resolved through skip 3 — a registry-injected
3703
+ * column, real at runtime, whose TYPE and relationship target are
3704
+ * registry-owned and invisible here. A caller asking a second question about
3705
+ * the leaf (is it a relationship? is it materialised?) must treat an
3706
+ * `injected` leaf as unanswerable rather than assume the absence of a
3707
+ * declared type means the absence of the property.
3708
+ */
3709
+ {
3710
+ kind: 'ok';
3711
+ object: string;
3712
+ field: string;
3713
+ meta?: GraphField;
3714
+ injected?: true;
3715
+ }
3716
+ /**
3717
+ * The graph cannot answer — one of the three skips. Never report this: it is
3718
+ * the absence of a judgement, not a passing one.
3719
+ */
3720
+ | {
3721
+ kind: 'unknowable';
3722
+ reason: 'object-not-in-stack' | 'no-field-map' | 'injected-hop';
3723
+ object: string;
3724
+ }
3725
+ /**
3726
+ * A hop names nothing on the object it was written against. `at` is the
3727
+ * 0-based segment index, `object` the object the hop was resolved against.
3728
+ */
3729
+ | {
3730
+ kind: 'hop-unknown';
3731
+ at: number;
3732
+ segment: string;
3733
+ object: string;
3734
+ candidates: ReadonlySet<string>;
3735
+ }
3736
+ /**
3737
+ * A hop resolves to a real field that is NOT a relationship, so there is
3738
+ * nothing to traverse through — the author wrote `amount.total` where
3739
+ * `amount` is a number.
3740
+ */
3741
+ | {
3742
+ kind: 'hop-not-relationship';
3743
+ at: number;
3744
+ segment: string;
3745
+ object: string;
3746
+ type?: string;
3747
+ }
3748
+ /**
3749
+ * A relationship hop resolves but declares no `reference`, so the target
3750
+ * object is unknown and the rest of the path cannot be judged. Treated as a
3751
+ * SKIP by callers for the same reason as `unknowable`.
3752
+ */
3753
+ | {
3754
+ kind: 'hop-untargeted';
3755
+ at: number;
3756
+ segment: string;
3757
+ object: string;
3758
+ }
3759
+ /** Every hop resolved; the LEAF names nothing on the object it landed on. */
3760
+ | {
3761
+ kind: 'field-unknown';
3762
+ object: string;
3763
+ field: string;
3764
+ candidates: ReadonlySet<string>;
3765
+ };
3766
+ /**
3767
+ * Resolve `path` against `objectName` in `graph`.
3768
+ *
3769
+ * A bare `field` is the one-segment case and resolves against the root object
3770
+ * directly. A dotted `a.b.field` walks `a`, then `b`, as to-one relationship
3771
+ * hops, and resolves the leaf on whatever object the last hop landed on.
3772
+ *
3773
+ * ⛔ This function answers EXISTENCE only. Whether a hop was DECLARED joinable
3774
+ * — an ADR-0021 dataset joins only what `Dataset.include` names — is a second
3775
+ * question the caller owns, because only the caller knows which declaration
3776
+ * governs the position. Conflating them here would make the seam unusable at
3777
+ * the positions that have no `include` (a list view's field path, #14107).
3778
+ */
3779
+ declare function resolveFieldPath(graph: ObjectGraph, objectName: string | undefined, path: string): FieldPathVerdict | undefined;
3780
+ /**
3781
+ * The relationship prefixes a document declared as joinable.
3782
+ *
3783
+ * ADR-0021: *"Declaring `a.b` implicitly includes the intermediate `a`."* So
3784
+ * every PREFIX of every declared path is joinable, not only the paths as
3785
+ * written — which is why this expands rather than reading `include` verbatim.
3786
+ *
3787
+ * Here rather than in a rule because the SAME `include` governs positions two
3788
+ * different rules judge: a dataset's own `dimensions[].field` / `measures[].field`
3789
+ * / filter keys (#14105), and a dashboard widget's `filter` keys (#14148), whose
3790
+ * condition is ANDed into that same dataset's compiled query as `runtimeFilter`.
3791
+ * Two copies of the prefix expansion would let the two positions drift apart on
3792
+ * a clause that is one sentence of one ADR.
3793
+ */
3794
+ declare function joinablePrefixes(include: unknown): ReadonlySet<string>;
3795
+ /** The two halves of a rendered verdict: the finding's message, and its detail. */
3796
+ interface FieldPathAccount {
3797
+ /** What is wrong, in prose, carrying the "did you mean" when there is one. */
3798
+ message: string;
3799
+ /** The supporting field list, for the finding's hint. */
3800
+ detail: string;
3801
+ }
3802
+ /**
3803
+ * Turn a resolution verdict into the message half of an existence finding, or
3804
+ * `undefined` when the verdict is one no rule may report.
3805
+ *
3806
+ * Shared by every position that resolves a field PATH — a dataset dimension, a
3807
+ * measure, a dataset filter key (#14105), a widget filter key (#14148) — so
3808
+ * they cannot drift into N different accounts of the same miss. The caller
3809
+ * supplies `subject` (how the position is named in prose) and owns the rule id,
3810
+ * the severity, the path and the hint's prescription; this function holds none
3811
+ * of them, matching the rest of this module.
3812
+ */
3813
+ declare function describeFieldPathVerdict(verdict: FieldPathVerdict, path: string, subject: string): FieldPathAccount | undefined;
3814
+ /**
3815
+ * True when the verdict is one no rule may report — the graph could not answer.
3816
+ * Callers spell the skip through this predicate rather than re-listing the
3817
+ * kinds, so a future verdict added to the union defaults to being reported
3818
+ * loudly (a missed skip is a visible false positive; a missed report is
3819
+ * silence, which is the failure mode this whole family exists to end).
3820
+ */
3821
+ declare function isUnjudgeable(verdict: FieldPathVerdict | undefined): boolean;
3822
+ /**
3823
+ * Nearest declared name for a typo'd reference, or `undefined` when nothing is
3824
+ * close enough. The sole shared helper behind every "Did you mean?" hint in
3825
+ * this package (issue #14268 consolidated the four hand-copied instances that
3826
+ * had drifted apart into this one).
3827
+ *
3828
+ * Containment is checked first, ahead of edit distance: the ADR-0021 cutover's
3829
+ * canonical drift is base column → prefixed measure name (`amount` →
3830
+ * `sum_amount`), which is far in edit distance but obvious to a human. A
3831
+ * candidate that contains the target (or vice versa, when both are at least 3
3832
+ * characters) scores on the length delta instead and always wins over an
3833
+ * edit-distance match; otherwise the target must be within budget —
3834
+ * `max(2, floor(len/3))` — of a candidate to be offered at all.
3835
+ */
3836
+ declare function nearestName(target: string, known: Iterable<string>): string | undefined;
3837
+ /** ` Did you mean "x"?`, or the empty string — the platform's message shape. */
3838
+ declare function suggestName(target: string, known: Iterable<string>): string;
3839
+ /** A readable field list for a hint, or `(none)`. */
3840
+ declare function listNames(names: Iterable<string>): string;
3841
+
3842
+ /** One field position inside an authored filter. */
3843
+ interface FilterFieldKey {
3844
+ /**
3845
+ * The field the condition filters BY, exactly as authored — a bare name
3846
+ * (`status`), or a dotted relationship path (`account.region`, whether
3847
+ * spelled that way or reached by descending a nested condition object).
3848
+ */
3849
+ field: string;
3850
+ /** Config path of the position, e.g. `datasets[1].measures[1].filter.last_update_at`. */
3851
+ path: string;
3852
+ }
3853
+ /**
3854
+ * Emit every FIELD KEY inside one authored filter subtree, whatever shape it
3855
+ * was authored in — the key half of what `validate-preset-comparands.ts` does
3856
+ * for values, and the traversal `filter-token-unknown` already performs while
3857
+ * reasoning only about the strings it finds.
3858
+ *
3859
+ * Holds no judgement: it does not know which object the filter is bound to and
3860
+ * emits no findings. Resolution is {@link resolveFieldPath}'s job
3861
+ * (`object-graph.ts`) and the verdict is the caller's.
3862
+ */
3863
+ declare function walkFilterFieldKeys(node: unknown, path: string, visit: (key: FilterFieldKey) => void, depth?: number): void;
2301
3864
 
2302
3865
  type RuleCompilabilitySeverity = 'error';
2303
3866
  interface RuleCompilabilityFinding {
@@ -2417,6 +3980,55 @@ type AnyRec$d = Record<string, unknown>;
2417
3980
  */
2418
3981
  declare function validateNavAccess(stack: AnyRec$d): NavAccessFinding[];
2419
3982
 
3983
+ declare const FIELD_NO_CONSUMERS = "field-no-consumers";
3984
+ type FieldConsumerSeverity = 'warning';
3985
+ /** Why the field is reported: carriers only, or nothing at all. */
3986
+ type FieldConsumerVerdict = 'inert' | 'carrier-only';
3987
+ interface FieldConsumerFinding {
3988
+ /** Always `warning` — a consumer may live outside the stack (see module note). */
3989
+ severity: FieldConsumerSeverity;
3990
+ /** Diagnostic rule id. */
3991
+ rule: string;
3992
+ /** Human-readable location, e.g. `object "crm_product" · field "tax_rate"`. */
3993
+ where: string;
3994
+ /** Config path of the DECLARATION, e.g. `objects[3].fields.tax_rate` (map shape) or `objects[3].fields[2]` (array shape). */
3995
+ path: string;
3996
+ /** What is wrong. */
3997
+ message: string;
3998
+ /** How to fix it. */
3999
+ hint: string;
4000
+ /** The declaring object. */
4001
+ object: string;
4002
+ /** The field name. */
4003
+ field: string;
4004
+ /** `carrier-only` when carrier sites exist, `inert` when no site of any kind names the field. */
4005
+ verdict: FieldConsumerVerdict;
4006
+ /** Config paths of the carrier sites a removal must clean (empty for `inert`). */
4007
+ carriers: string[];
4008
+ /** The stack roots this verdict was measured over — consumer roots then carrier roots. */
4009
+ rootsScanned: readonly string[];
4010
+ }
4011
+ type AnyRec$c = Record<string, unknown>;
4012
+ /**
4013
+ * Roots whose contents can CONSUME a field, walked with an object context.
4014
+ * `objects` is here for what an object carries besides its field map —
4015
+ * formulas, roll-ups, validations, built-in list views, hooks, actions,
4016
+ * indexes, `highlightFields`, `searchableFields`. Order is report order.
4017
+ */
4018
+ declare const CONSUMER_ROOTS: readonly string[];
4019
+ /**
4020
+ * Roots whose contents carry a field without reading it. A locale row is a
4021
+ * label for a field, not a consumer of one; a seed VALUE nothing reads is the
4022
+ * shape being hunted; an import column and a field-level permission grant are
4023
+ * customer-facing surfaces a removal must clean, not evidence of a reader.
4024
+ */
4025
+ declare const CARRIER_ROOTS: readonly string[];
4026
+ /**
4027
+ * Report every declared field that nothing in the stack reads or displays.
4028
+ * Returns findings (empty = clean). Pure; safe on pre- or post-parse stacks.
4029
+ */
4030
+ declare function validateFieldConsumers(stack: AnyRec$c): FieldConsumerFinding[];
4031
+
2420
4032
  declare const TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
2421
4033
  declare const TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
2422
4034
  type TranslationRefSeverity = 'warning';
@@ -2434,12 +4046,12 @@ interface TranslationRefFinding {
2434
4046
  /** How to fix it. */
2435
4047
  hint: string;
2436
4048
  }
2437
- type AnyRec$c = Record<string, unknown>;
4049
+ type AnyRec$b = Record<string, unknown>;
2438
4050
  /**
2439
4051
  * Validate every reference a translation bundle makes against the metadata it
2440
4052
  * claims to translate. Returns findings (empty = clean).
2441
4053
  */
2442
- declare function validateTranslationReferences(stack: AnyRec$c): TranslationRefFinding[];
4054
+ declare function validateTranslationReferences(stack: AnyRec$b): TranslationRefFinding[];
2443
4055
 
2444
4056
  declare const TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
2445
4057
  type TranslatableSectionSeverity = 'warning';
@@ -2457,40 +4069,13 @@ interface TranslatableSectionFinding {
2457
4069
  /** How to fix it. */
2458
4070
  hint: string;
2459
4071
  }
2460
- type AnyRec$b = Record<string, unknown>;
4072
+ type AnyRec$a = Record<string, unknown>;
2461
4073
  /**
2462
4074
  * Report every form/detail section that declares a heading but no `name`, on an
2463
4075
  * object the stack actually translates. Returns findings (empty = clean).
2464
4076
  */
2465
- declare function validateTranslatableSections(stack: AnyRec$b): TranslatableSectionFinding[];
4077
+ declare function validateTranslatableSections(stack: AnyRec$a): TranslatableSectionFinding[];
2466
4078
 
2467
- /**
2468
- * [ADR-0064 §3] Skill ↔ agent surface affinity (issue #3820).
2469
- *
2470
- * An agent binds a product surface (`'ask'` | `'build'`, ADR-0063 §1) and a
2471
- * skill declares which surface it belongs to (`'ask'` | `'build'` | `'both'`,
2472
- * ADR-0063 §3). A skill may only attach to an agent whose surface it matches —
2473
- * `'both'` attaches to either. The runtime treats a violation as a FAST LOAD
2474
- * ERROR: `resolveActiveSkills()` throws on the first incompatible binding, so
2475
- * an agent shipping one mismatched skill reference fails at **chat time** with
2476
- * a 500 — after parse, after validate, after deploy.
2477
- *
2478
- * Both sides of the check are declared in the same stack, so the contradiction
2479
- * is statically provable and this rule carries severity **error** with zero
2480
- * false positives by construction. Both sides default to `'ask'` when the
2481
- * `surface` field is absent (mirroring the runtime's defaults), so the rule is
2482
- * safe on the raw/normalized config the `lint` path carries as well as the
2483
- * schema-parsed stack.
2484
- *
2485
- * Scope note: this rule deliberately does NOT check that `agent.skills[]`
2486
- * names resolve at all. Kernel skills (`schema_reader`, the `ask`/`build`
2487
- * bundles) are runtime-registered and statically invisible, and whether
2488
- * app-stack tool/skill namespaces get a platform-name registry is an open
2489
- * decision (#3820 D0/D2) — resolving names against `stack.skills` alone would
2490
- * flag every kernel-skill reference. An unresolved name is therefore skipped
2491
- * here; only a reference that resolves in-stack AND contradicts the affinity
2492
- * contract is reported.
2493
- */
2494
4079
  declare const AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
2495
4080
  type AiSurfaceAffinitySeverity = 'error' | 'warning';
2496
4081
  interface AiSurfaceAffinityFinding {
@@ -2507,12 +4092,12 @@ interface AiSurfaceAffinityFinding {
2507
4092
  /** How to fix it. */
2508
4093
  hint: string;
2509
4094
  }
2510
- type AnyRec$a = Record<string, unknown>;
4095
+ type AnyRec$9 = Record<string, unknown>;
2511
4096
  /**
2512
4097
  * Validate every in-stack agent→skill binding against the ADR-0064 §3 surface
2513
4098
  * affinity contract. Returns findings (empty = clean).
2514
4099
  */
2515
- declare function validateAiSurfaceAffinity(stack: AnyRec$a): AiSurfaceAffinityFinding[];
4100
+ declare function validateAiSurfaceAffinity(stack: AnyRec$9): AiSurfaceAffinityFinding[];
2516
4101
 
2517
4102
  declare const AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
2518
4103
  type AiToolRefSeverity = 'error' | 'warning';
@@ -2530,59 +4115,18 @@ interface AiToolRefFinding {
2530
4115
  /** How to fix it. */
2531
4116
  hint: string;
2532
4117
  }
2533
- type AnyRec$9 = Record<string, unknown>;
4118
+ type AnyRec$8 = Record<string, unknown>;
2534
4119
  /**
2535
4120
  * Validate every `skill.tools[]` reference in a stack. Returns findings
2536
4121
  * (empty = clean).
2537
4122
  */
2538
- declare function validateAiToolReferences(stack: AnyRec$9): AiToolRefFinding[];
2539
-
2540
- /**
2541
- * [ADR-0063 §2] `stack.agents` is a platform-internal slot (issue #3820).
2542
- *
2543
- * ADR-0063 §2 withdrew tenant/app-package custom agents: the kernel ships
2544
- * exactly two agents (`ask`, `build`), the surface the user is in binds one,
2545
- * and third parties extend the platform by authoring **skills**, never
2546
- * `*.agent.ts`. The `agent` metadata type carries the decision
2547
- * (`allowRuntimeCreate: false, allowOrgOverride: false`), and the runtime
2548
- * enforces it on both paths — `listAgents()` filters non-platform records out
2549
- * of the catalog, and `loadAgent()` refuses them outright (cloud#904), so a
2550
- * stack-authored agent 404s on chat and cannot be pinned via
2551
- * `app.defaultAgent`.
2552
- *
2553
- * What was missing is the AUTHORING-time signal. `defineStack` still accepts
2554
- * an `agents` array, so an app package could declare agents that parse,
2555
- * validate, and build into the artifact — and then do nothing at runtime.
2556
- * HotCRM shipped two of them for months. That is the ADR-0078 shape this rule
2557
- * closes: loud at the producer, tolerant at the consumer (Prime Directive
2558
- * #12).
2559
- *
2560
- * Severity is **warning**, not error, for one reason: the platform's own
2561
- * packages legitimately author agent records, and this rule cannot tell a
2562
- * platform package from an app package by reading the stack alone. A warning
2563
- * that names the runtime consequence is honest for both readers; the runtime
2564
- * is what actually gates. Deliberately NOT a Zod refine — an existing stack
2565
- * must keep parsing (ADR-0078 non-goal #1).
2566
- *
2567
- * ## The value half (issue #6041)
2568
- *
2569
- * The rule above catches a stack that *declares* a withdrawn agent record.
2570
- * It never looked at `app.defaultAgent` — a plain
2571
- * `SnakeCaseIdentifierSchema` string, so any snake_case value parses, builds,
2572
- * and passes `os:check` even when it names nothing the runtime will ever
2573
- * resolve. #5985 measured the blind spot directly: replaying the bad example
2574
- * `defaultAgent: 'sales_copilot'` left `check:skill-examples` at 208 green,
2575
- * EXIT=0. `app.defaultAgent` silently falls back to the platform default at
2576
- * runtime (ADR-0063 §1) instead of crashing, which is why this limb is
2577
- * **warning**, not error, same as the rule above — the maintainer ruling on
2578
- * #6041 (2026-08-07, reaffirmed 2026-08-09) is option A: add the value check
2579
- * at warning tier, reusing `PLATFORM_AGENT_NAMES` rather than narrowing the
2580
- * schema to an enum (a breaking authoring change ADR-0063 already walked
2581
- * back once).
2582
- */
4123
+ declare function validateAiToolReferences(stack: AnyRec$8): AiToolRefFinding[];
4124
+
2583
4125
  declare const AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
2584
4126
  /** `app.defaultAgent` names something outside the platform agent roster. */
2585
4127
  declare const DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
4128
+ /** `app.defaultAgent` spells a platform agent by its RETIRED alias (#14461). */
4129
+ declare const DEFAULT_AGENT_LEGACY_ALIAS = "default-agent-legacy-alias";
2586
4130
  type AiAgentAuthoringSeverity = 'error' | 'warning';
2587
4131
  interface AiAgentAuthoringFinding {
2588
4132
  /** Always `warning` — the runtime is the gate; this is the authoring-time signal. */
@@ -2598,158 +4142,12 @@ interface AiAgentAuthoringFinding {
2598
4142
  /** How to fix it. */
2599
4143
  hint: string;
2600
4144
  }
2601
- type AnyRec$8 = Record<string, unknown>;
4145
+ type AnyRec$7 = Record<string, unknown>;
2602
4146
  /**
2603
4147
  * Flag every agent declared in a stack. Returns findings (empty = clean,
2604
4148
  * which is what every app package should be).
2605
4149
  */
2606
- declare function validateAiAgentAuthoring(stack: AnyRec$8): AiAgentAuthoringFinding[];
2607
-
2608
- type HookBodyWriteSeverity = 'warning';
2609
- interface HookBodyWriteFinding {
2610
- /** v1 is advisory-only by contract — the type says so. */
2611
- severity: HookBodyWriteSeverity;
2612
- rule: string;
2613
- /** Human-readable location, e.g. `hook "normalize_lead" › body`. */
2614
- where: string;
2615
- /** Config path, e.g. `hooks[0].body.source`. */
2616
- path: string;
2617
- message: string;
2618
- hint: string;
2619
- }
2620
- declare const HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
2621
- /**
2622
- * [#10653] The body did not parse, so its write set is whatever error recovery
2623
- * left readable.
2624
- *
2625
- * Reported rather than skipped for the reason this rule exists at all: a
2626
- * mistake must be visible where it is MADE. An unparseable body reached the
2627
- * extractor, produced fewer matches, and came back as a hook with nothing to
2628
- * report — the same silence the undeclared write itself has at run time, this
2629
- * time wearing the checker's badge. `warning` because the whole rule is
2630
- * advisory and never gates (the severity type admits nothing else).
2631
- */
2632
- declare const HOOK_BODY_SOURCE_UNPARSEABLE = "hook-body-source-unparseable";
2633
- /**
2634
- * [#8663] The write-axis twin of `flow-template-field-unprovisioned` (#8340):
2635
- * the body writes a field {@link IMPLICIT_FIELDS} exempts, but on THIS target
2636
- * the platform registered that anchor without provisioning storage for it.
2637
- *
2638
- * A separate id at `warning` severity rather than a reclassification of
2639
- * {@link HOOK_BODY_WRITE_UNKNOWN_FIELD}, matching #8340's precedent exactly:
2640
- * the existence verdict is unchanged (the name IS addressable), and what is
2641
- * added is a second, independently suppressible finding on the path where the
2642
- * existence check stays silent.
2643
- */
2644
- declare const HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
2645
- /** One syntactic write shape the extractor recognizes. */
2646
- interface HookBodyWritePattern {
2647
- /** Stable pattern id, carried on every extracted write. */
2648
- readonly id: string;
2649
- /** Author-facing syntax summary (for docs/diagnostics, not matching). */
2650
- readonly syntax: string;
2651
- /** Reconciliation fixture: extracting `source` must yield exactly `writes`. */
2652
- readonly example: {
2653
- readonly source: string;
2654
- readonly writes: ReadonlyArray<{
2655
- field: string;
2656
- object?: string;
2657
- }>;
2658
- };
2659
- }
2660
- declare const HOOK_BODY_WRITE_PATTERNS: readonly HookBodyWritePattern[];
2661
- /** A ledger pattern a given rule does NOT consume, and why. */
2662
- interface BodyWritePatternExclusion {
2663
- /** The {@link HOOK_BODY_WRITE_PATTERNS} entry id being excluded. */
2664
- readonly id: string;
2665
- /** Why the shape does not mean the same thing on this rule's surface. */
2666
- readonly reason: string;
2667
- }
2668
- /**
2669
- * The ledger shapes THIS rule consumes.
2670
- *
2671
- * Declared rather than implied: before the ledger carried a shape the hook
2672
- * surface does not have, every write with no `object` was necessarily a
2673
- * `ctx.input` write, and the rule could branch on that alone. It no longer can
2674
- * — a `record-property-assign` write also carries no object, and would have
2675
- * been reported as "the hook writes 'stage' to its input", which is false.
2676
- * Each consumer declaring its own subset is what stops the next added shape
2677
- * from silently landing in a branch that was never written for it.
2678
- */
2679
- declare const HOOK_BODY_WRITE_PATTERN_IDS: readonly string[];
2680
- /** Ledger shapes this rule leaves alone, each with its reason. */
2681
- declare const HOOK_BODY_WRITE_EXCLUSIONS: readonly BodyWritePatternExclusion[];
2682
- type AnyRec$7 = Record<string, unknown>;
2683
- /** One statically-extracted field write found in an L2 body. */
2684
- interface ExtractedHookBodyWrite {
2685
- /** Which {@link HOOK_BODY_WRITE_PATTERNS} entry matched. */
2686
- patternId: string;
2687
- /** Target object name; `undefined` = the hook's own target object(s). */
2688
- object?: string;
2689
- /** The `ctx.api` method for diagnostics (`insert`/`create`/`update`/`updateById`). */
2690
- method?: string;
2691
- field: string;
2692
- }
2693
- /** Everything one parse of an L2 body yields. */
2694
- interface ExtractedHookBodyWriteSet {
2695
- /** Every literal write the {@link HOOK_BODY_WRITE_PATTERNS} ledger declares. */
2696
- writes: ExtractedHookBodyWrite[];
2697
- /**
2698
- * `ctx.record` is handed to something as a VALUE somewhere in the body — an
2699
- * argument, an assignment RHS, a spread, a return — rather than only having
2700
- * its properties read and written, or being truthiness/type tested.
2701
- *
2702
- * The action rule needs this to tell a dead snapshot write from a live one:
2703
- * `ctx.record.stage = 'won'; await ctx.api.object('d').update(ctx.record)`
2704
- * builds a payload and persists it, so the assignment is not a no-op. When
2705
- * this is true, no record write in the body can be judged, and none is
2706
- * reported. (One-level aliasing — `const r = ctx.record` — reads as an
2707
- * escape too, which is the safe direction: it suppresses findings.)
2708
- */
2709
- ctxRecordEscapes: boolean;
2710
- /**
2711
- * [#10653] Set when the body did not parse, so `writes` is whatever error
2712
- * recovery left readable rather than the body's actual write set.
2713
- *
2714
- * Absent means one of two things, and they are not the same: the body parsed,
2715
- * or the cheap pre-filter above rejected it before any parse. The filter is a
2716
- * raw-text scan for `ctx` / `Object`, and a body containing neither cannot
2717
- * match any pattern however it parses — so a skipped parse claims nothing and
2718
- * hides nothing.
2719
- *
2720
- * ## Whose fault an unparseable body is — asked, not assumed
2721
- *
2722
- * The body is parsed inside a synthesised wrapper (`async function __body(ctx)
2723
- * { … }`) because that is the shape the runtime compiles it into
2724
- * (`new AsyncFunction('ctx', source)`). So a parse failure here could in
2725
- * principle be the WRAPPER's fault rather than the author's, and blaming the
2726
- * author for the checker's own bug is the failure this whole change is about.
2727
- * It cannot be: the wrapper is a constant, and `validate-hook-body-writes.
2728
- * test.ts` pins that it parses clean around an empty body and around every
2729
- * example in the pattern ledger. Any diagnostic therefore comes from the
2730
- * body — and its position is reported in the BODY's own coordinates (the
2731
- * wrapper's line is subtracted, and the result is clamped so it can never
2732
- * point at a line the author did not write).
2733
- */
2734
- parseFailure?: SourceParseFailure;
2735
- }
2736
- /**
2737
- * Extract every literal field write the pattern ledger declares from an L2
2738
- * body's source. Parse-only (the source is never executed), error-tolerant
2739
- * (a body with syntax errors simply yields fewer matches), and lazy: the
2740
- * TypeScript compiler is not loaded when no pattern can possibly match.
2741
- *
2742
- * Thin projection of {@link extractHookBodyWriteSet} — use that one when the
2743
- * `ctx.record` liveness signal matters, so the body is parsed once, not twice.
2744
- */
2745
- declare function extractHookBodyWrites(source: string): ExtractedHookBodyWrite[];
2746
- /** {@link extractHookBodyWrites} plus the `ctx.record` liveness signal, one parse. */
2747
- declare function extractHookBodyWriteSet(source: string): ExtractedHookBodyWriteSet;
2748
- /**
2749
- * Validate L2 hook-body writes against target-object field declarations.
2750
- * Pure `(stack) => Finding[]` (ADR-0019); safe on pre- or post-parse stacks.
2751
- */
2752
- declare function validateHookBodyWrites(stack: AnyRec$7): HookBodyWriteFinding[];
4150
+ declare function validateAiAgentAuthoring(stack: AnyRec$7): AiAgentAuthoringFinding[];
2753
4151
 
2754
4152
  type ActionBodyWriteSeverity = 'warning';
2755
4153
  interface ActionBodyWriteFinding {
@@ -2966,6 +4364,40 @@ declare const FLOW_INERT_NODE_CONDITION = "flow-inert-node-condition";
2966
4364
  * how it divides labour with the #3810 run-time guard.
2967
4365
  */
2968
4366
  declare const FLOW_MULTI_WRITE_UNFILTERED = "flow-multi-write-unfiltered";
4367
+ /**
4368
+ * #13681 / #14394 — a `loop` body that runs a node which can fail, with no
4369
+ * `try_catch` between the loop and that node. The first failing item kills the
4370
+ * whole sweep: `loop-node.ts:123-135` iterates with a bare `await` and no
4371
+ * `try`/`catch` anywhere in the file, so the body's failure propagates out of
4372
+ * the container and the remaining items are never processed.
4373
+ *
4374
+ * A **warning**, per the severity policy at the top of this file: a loop whose
4375
+ * body is deliberately allowed to die (fail fast on the first bad row) is a
4376
+ * legitimate reading, and the rule cannot prove the author did not mean it. What
4377
+ * it can do is make the choice visible at authoring time — today it is silent,
4378
+ * and the measured consequence is a 5-row sweep that processes 3 rows and
4379
+ * reports a run that "completed" nothing unusual.
4380
+ *
4381
+ * See {@link scanUncontainedLoopBodies} for the containment judgement, and
4382
+ * {@link FALLIBLE_NODE_TYPES} for what counts as fallible and why.
4383
+ */
4384
+ declare const FLOW_LOOP_BODY_UNCONTAINED = "flow-loop-body-uncontained";
4385
+ /**
4386
+ * #13681 / #14394 — the near-miss shape: a `try_catch` that declares no `catch`
4387
+ * region. `catch` is optional in the schema (`control-flow.zod.ts:315`) and
4388
+ * omitting it makes the container **fail** (`try-catch-node.ts:190`), so the
4389
+ * wrapped region dies exactly like an unwrapped one — measured side by side, the
4390
+ * no-`catch` run and the no-wrapper control produce identical output.
4391
+ *
4392
+ * This is the family's first target for the containment pair, not an extra: an
4393
+ * author who reaches for `try_catch` has recognised the hazard and stopped one
4394
+ * key short, and before this rule got **zero containment and zero diagnostics**.
4395
+ * A warning rather than an error under the same policy — a `retry`-only
4396
+ * `try_catch` (retry the region, then fail loudly) is a legitimate reading.
4397
+ *
4398
+ * See {@link scanTryCatchWithoutCatch}.
4399
+ */
4400
+ declare const FLOW_TRY_CATCH_WITHOUT_CATCH = "flow-try-catch-without-catch";
2969
4401
  /**
2970
4402
  * Lint every flow for known authoring anti-patterns — its own graph AND every
2971
4403
  * nested ADR-0031 region (#5383). Returns a (possibly empty) list of findings;
@@ -2982,13 +4414,44 @@ interface LivenessLintFinding {
2982
4414
  }
2983
4415
  declare const LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
2984
4416
  declare const LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
4417
+ declare const LIVENESS_PLANNED_PROPERTY = "liveness-planned-property";
4418
+ declare const LIVENESS_LIVE_ELSEWHERE_PROPERTY = "liveness-live-elsewhere-property";
2985
4419
  type AnyRec$2 = Record<string, unknown>;
4420
+ /**
4421
+ * The property paths of `type`'s ledger that warn an author for authoring them
4422
+ * — i.e. exactly the set {@link lintLivenessProperties} raises a finding on for
4423
+ * that type, exposed as a decision procedure.
4424
+ *
4425
+ * WHY THIS IS EXPORTED RATHER THAN RE-DERIVED. A second tool wanted the same
4426
+ * fact and the first attempt at it re-read the ledger itself, which is how the
4427
+ * two halves of one `os lint` run came to point in opposite directions: the
4428
+ * CLI's i18n coverage walker demanded `flows.*` translation keys for a group
4429
+ * whose ledger row is `planned` + `authorWarn`, so omitting the keys drew
4430
+ * `i18n/missing-flow` and authoring them drew `liveness-planned-property` — in
4431
+ * the SAME run, with no third option for the author. A demand side and a warn
4432
+ * side that disagree can only be kept honest by reading ONE verdict, so this
4433
+ * returns the very map `checkItem` iterates rather than a parallel reading of
4434
+ * the same JSON.
4435
+ *
4436
+ * Keys are the ledger's own property paths, `children` flattened one level as
4437
+ * `parent.child` — the shape `checkItem` resolves. Unreadable or absent ledger
4438
+ * ⇒ the empty set, which is also the state in which `lintLivenessProperties`
4439
+ * warns on nothing: the two sides go quiet together rather than one of them
4440
+ * going quiet alone.
4441
+ *
4442
+ * Deliberately NOT memoized, for the same reason `lintLivenessProperties`
4443
+ * re-reads on every call: a cached verdict outlives the ledger edit that
4444
+ * changes it, and a stale "nothing warns here" is the silent state this whole
4445
+ * family exists to prevent.
4446
+ */
4447
+ declare function authorWarnedProperties(type: string): ReadonlySet<string>;
2986
4448
  /**
2987
4449
  * Lint the compiled stack for authored properties the liveness ledger flags as
2988
4450
  * misleading. Advisory only — returns findings, never throws. Covers every
2989
4451
  * governed metadata type: objects (incl. `enable.*`) and their fields walk
2990
- * bespoke nesting; the remaining types are flat stack collections. Container
2991
- * properties fan out over arrays (each flow node, each dataset measure). The
4452
+ * bespoke nesting, and translation bundles walk their locale entries (#11288);
4453
+ * the remaining types are flat stack collections. Container properties fan out
4454
+ * over arrays (each flow node, each dataset measure). The
2992
4455
  * mechanism stays ledger-driven — coverage grows by marking more entries
2993
4456
  * `authorWarn` rather than touching this code.
2994
4457
  */
@@ -3023,23 +4486,9 @@ type AnyRec = Record<string, any>;
3023
4486
  declare const VIEW_KEY_COLLISION = "view-key-collision";
3024
4487
  declare const VIEW_REF_FORM_TARGET_MISSING = "view-ref-form-target-missing";
3025
4488
  declare const VIEW_REF_FORM_TARGET_KIND = "view-ref-form-target-kind";
4489
+ declare const VIEW_REF_NAV_VIEW_MISSING = "view-ref-nav-view-missing";
3026
4490
  declare function lintViewRefs(stack: AnyRec): ViewRefFinding[];
3027
4491
 
3028
- /**
3029
- * Data-model best-practice lint rules.
3030
- *
3031
- * These rules encode the relationship / master-detail / roll-up conventions the
3032
- * platform ships (see the objectstack-data and objectstack-ui skills, ADR-0035).
3033
- * They run over the normalized object set and flag anti-patterns that an
3034
- * author — human OR an AI generator — commonly produces. They are intentionally
3035
- * heuristic: structural problems are `error`, likely-wrong choices are
3036
- * `warning`, and "you probably want this" nudges are `suggestion`. None of them
3037
- * block on a judgement call.
3038
- *
3039
- * The same rules double as the automated rubric for the metadata-generation
3040
- * eval (see `score.ts`): a generated stack scores well exactly when it is
3041
- * schema-valid AND lint-clean here.
3042
- */
3043
4492
  type Severity = 'error' | 'warning' | 'suggestion';
3044
4493
  interface LintIssue {
3045
4494
  severity: Severity;
@@ -3184,4 +4633,64 @@ declare function lintLegacyOrganizationComposites(objects: any[]): LocatedLintIs
3184
4633
  */
3185
4634
  declare function lintDataModel(objects: any[]): LintIssue[];
3186
4635
 
3187
- export { ACTION_BODY_SOURCE_UNPARSEABLE, ACTION_BODY_WRITE_EXCLUSIONS, ACTION_BODY_WRITE_PATTERNS, ACTION_BODY_WRITE_PATTERN_IDS, ACTION_BODY_WRITE_UNKNOWN_FIELD, ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR, ACTION_NAME_UNDEFINED, ACTION_NO_PLACEMENT, ACTION_RECORD_WRITE_DISCARDED, ACTION_RECORD_WRITE_PATTERNS, ACTION_RECORD_WRITE_PATTERN_IDS, AGENT_AUTHORING_WITHDRAWN, AI_SKILL_SURFACE_MISMATCH, AI_SKILL_TOOL_UNRESOLVED, APPROVAL_APPROVERS_MAY_RESOLVE_EMPTY, APPROVAL_APPROVER_CROSS_ORG_UNSUPPORTED, APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER, APPROVAL_APPROVER_TYPE_DEPRECATED, APPROVAL_APPROVER_TYPE_UNKNOWN, APPROVAL_APPROVER_TYPE_UNSUPPORTED, APPROVAL_DECISION_OUTPUTS_RESERVED, APPROVAL_ESCALATION_REASSIGN_NO_TARGET, APPROVAL_EXPRESSION_INVALID, APPROVAL_EXPRESSION_NO_EMPTY_POLICY, AUTONUMBER_LITERAL_TOKEN, AUTONUMBER_OPTIONAL_FIELD, AUTONUMBER_SELF_REFERENCE, AUTONUMBER_UNKNOWN_FIELD, type ActionBodyWriteExclusion, type ActionBodyWriteFinding, type ActionBodyWriteSeverity, type ActionLocationsFinding, type ActionLocationsSeverity, type ActionNameRefFinding, type ActionNameRefSeverity, type AiAgentAuthoringFinding, type AiAgentAuthoringSeverity, type AiSurfaceAffinityFinding, type AiSurfaceAffinitySeverity, type AiToolRefFinding, type AiToolRefSeverity, type ApprovalApproverFinding, type ApprovalApproverSeverity, type AutonumberLintFinding, type BodyWritePatternExclusion, CAPABILITY_REFERENCE_UNKNOWN, CHART_AXIS_NOT_SELECTED, CHART_CONFIG_MISSING, CHART_DATASET_UNKNOWN, CHART_DIMENSION_UNKNOWN, CHART_FIELD_UNKNOWN, CHART_MEASURE_UNKNOWN, COMPONENT_PROPS_INVALID, COMPONENT_PROPS_UNKNOWN_KEY, type CapabilityRefFinding, type CapabilityRefSeverity, type ChartBindingFinding, type ChartBindingSeverity, type CheckedParse, type CheckedParseOptions, type ComponentPropsFinding, type ComponentPropsSeverity, DASHBOARD_ACTION_ROUTE_UNRESOLVED, DASHBOARD_ACTION_TARGET_UNDEFINED, DASHBOARD_FILTER_FIELD_UNKNOWN, DASHBOARD_FILTER_FIELD_UNPROVISIONED, DEFAULT_AGENT_OUTSIDE_ROSTER, type DashboardActionRefFinding, type DashboardActionRefSeverity, type EmptyCombinatorFinding, type EmptyCombinatorSeverity, type ExprIssue, type ExtractedHookBodyWrite, type ExtractedHookBodyWriteSet, FIELD_GROUP_EMPTY, FIELD_GROUP_SHADOWED, FIELD_GROUP_UNDECLARED, FILTER_EMPTY_COMBINATOR, FILTER_EMPTY_NODE, FILTER_PRESET_COMPARAND, FILTER_TOKEN_UNKNOWN, FLOW_APPROVAL_REVISE_DEAD_END, FLOW_APPROVAL_REVISE_DISABLED, FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED, FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE, FLOW_BARE_DOLLAR_REF, FLOW_BRANCH_LABEL_UNMATCHED, FLOW_DATE_EQUALITY_FILTER, FLOW_DECISION_UNCONDITIONAL_BRANCH, FLOW_DEFAULT_EDGE_WITH_CONDITION, FLOW_DOUBLE_BRACE_INTERP, FLOW_DRAFT_STATUS_AMBIGUOUS, FLOW_ERROR_LABEL_NOT_FAULT, FLOW_INERT_NODE_CONDITION, FLOW_MULTIPLE_DEFAULT_EDGES, FLOW_MULTI_WRITE_UNFILTERED, FLOW_NODE_WRITE_UNKNOWN_FIELD, FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR, FLOW_PHANTOM_AGGREGATION, FLOW_RUNAS_UNSCOPED, FLOW_TEMPLATE_FIELD_UNPROVISIONED, FLOW_TEMPLATE_LOOKUP_TRAVERSAL, FLOW_TEMPLATE_UNKNOWN_FIELD, FLOW_TIME_RELATIVE_ANTIPATTERN, FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID, FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE, FLOW_TRIGGER_UNKNOWN_EVENT, FLOW_TRIGGER_UNKNOWN_OBJECT, FLOW_TRIGGER_UNROUTABLE, FLOW_UPDATE_READONLY_FIELD, FLOW_UPDATE_READONLY_WHEN_FIELD, FLOW_WRITE_NODE_TYPES, FLOW_WRITE_NODE_TYPES_DEFERRED, FORM_COLSPAN_ABSOLUTE, FORM_FIELD_UNKNOWN, type FilterTokenFinding, type FilterTokenSeverity, type FlowLintFinding, type FlowNodeWriteFinding, type FlowNodeWriteSeverity, type FlowTemplatePathFinding, type FlowTemplatePathSeverity, type FlowTriggerReadinessFinding, type FlowTriggerReadinessSeverity, type FlowWriteNodeDeferral, type FormLayoutFinding, type FormLayoutSeverity, type FunctionalCompletenessFinding, type FunctionalCompletenessSeverity, HOOK_BODY_SOURCE_UNPARSEABLE, HOOK_BODY_WRITE_EXCLUSIONS, HOOK_BODY_WRITE_PATTERNS, HOOK_BODY_WRITE_PATTERN_IDS, HOOK_BODY_WRITE_UNKNOWN_FIELD, HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR, type HookBodyWriteFinding, type HookBodyWritePattern, type HookBodyWriteSeverity, type JsxPageFinding, type JsxPageSeverity, LIST_VIEW_FILTERS_IN_VIEWS_MODE, LIVENESS_DEAD_PROPERTY, LIVENESS_EXPERIMENTAL_PROPERTY, type LintIssue, type ListViewModeFinding, type ListViewModeSeverity, type LivenessLintFinding, type LocatedLintIssue, MANAGED_API_METHOD_UNAFFORDABLE, MAX_SCHEMA_WALK_DEPTH, MEASURE_AGGREGATE_INCOHERENT, type ManagedApiMethodFinding, NAV_OBJECT_UNGRANTED, NAV_OBJECT_UNSERVABLE, NAV_TARGET_UNRESOLVED, NULL_GUARD_HINT, type NavAccessFinding, type NavAccessSeverity, type NavObjectServabilityFinding, type NavTargetRefFinding, type NavTargetRefSeverity, type NullGuardFinding, type NullGuardOptions, OBJECT_REFERENCE_UNKNOWN, OBJECT_REFERENCE_UNREGISTERED_PLATFORM, OPEN_VOCABULARY_PROBES, ORG_AXIS_CROSS_ORG_BU_GRANT, ORG_AXIS_PERMISSION_INHERITANCE, type ObjectRefFinding, type ObjectRefSeverity, type OrgAxisFinding, type OrgAxisSeverity, PAGE_FIELD_UNKNOWN, PAGE_FIELD_UNPROVISIONED, PAGE_SOURCE_CLASSNAME, PARSE_FAILURE_HINT, PREDICATE_PATH_UNRESOLVED, PREDICATE_PATH_UNROOTED, PREDICATE_RHS_PATH_SHAPED, PRE_SEAL_PHASES, type PageFieldFinding, type PageFieldSeverity, type PredicatePathFinding, type PredicatePathOptions, type PredicatePathSeverity, type PresetComparandFinding, type PresetComparandSeverity, REACT_BLOCK_NEEDS_RECORD_CONTEXT, REACT_CHART_AGGREGATE_INVALID, REACT_CHART_AXIS_UNKNOWN, REACT_CHART_DRILLDOWN_INVALID, REACT_CHART_FIELD_UNKNOWN, REACT_CHART_FIELD_UNPROVISIONED, REACT_PAGE_SOURCE_UNPARSEABLE, REFERENCE_INTEGRITY_RULES, RLS_PREDICATE_OVER_BUDGET, RLS_PREDICATE_UNENFORCEABLE, RLS_PREDICATE_UNPARSEABLE, RUNTIME_AJV_OPTIONS, type ReactPageFinding, type ReactPageSeverity, type ReactPropFinding, type ReactPropSeverity, type ReadonlyFlowWriteFinding, type ReadonlyFlowWriteSeverity, type RecordTitleFinding, type RecordTitleSeverity, type ReferenceIntegrityFinding, type ReferenceIntegrityRule, type ReferenceIntegritySeverity, type RlsPredicateFinding, type RlsPredicateSeverity, type RuleCompilabilityFinding, type RuleCompilabilitySeverity, type RuleSchemaFormatFinding, type RuleSchemaFormatSeverity, SEAL_MARKERS, SEARCHABLE_FIELD_UNKNOWN, SEARCHABLE_FIELD_UNPROVISIONED, SEARCHABLE_FIELD_UNSEARCHABLE, SECURITY_ANCHOR_HIGH_PRIVILEGE, SECURITY_BOOK_AUDIENCE_UNKNOWN_SET, SECURITY_CBP_NO_RELATION, SECURITY_DELEGATION_MISSING_REASON, SECURITY_EXTERNAL_WIDER, SECURITY_FLS_UNQUALIFIED_KEY, SECURITY_GRANT_EXPIRED_AT_AUTHORING, SECURITY_MASTER_DETAIL_UNGRANTED, SECURITY_OWD_ALIAS, SECURITY_OWD_UNSET, SECURITY_PRIVATE_NO_READSCOPE, SECURITY_ROLE_WORD, SECURITY_WILDCARD_VAMA, SEED_INSERT_MODE_DUPLICATES_ON_REPLAY, SEED_VALUE_OUTSIDE_STATE_MACHINE, SEMANTIC_ROLE_FIELD_UNKNOWN, SEMANTIC_ROLE_FIELD_UNPROVISIONED, SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT, SHARING_RULE_OBJECT_NOT_SHAREABLE, SHARING_RULE_RUNTIME_VARIABLE_CONDITION, SHARING_RULE_UNLOWERABLE_CONDITION, SORT_FIELD_UNKNOWN, SORT_FIELD_UNPROVISIONED, SORT_FIELD_UNSORTABLE, STARTUP_OPEN_VOCABULARY_VERDICT, STARTUP_SOURCE_UNPARSEABLE, STARTUP_VERDICT_ASSERTIVE_WORDING, STARTUP_VERDICT_HINT, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SearchableFieldFinding, type SearchableFieldRole, type SearchableFieldSeverity, type SecurityFinding, type SecuritySeverity, type SeedReplaySafetyFinding, type SeedReplaySafetySeverity, type SeedStateMachineFinding, type SeedStateMachineSeverity, type SemanticRoleFinding, type SemanticRoleSeverity, type Severity, type SharingRuleEnforceabilityFinding, type SharingRuleEnforceabilitySeverity, type SortableFieldFinding, type SortableFieldSeverity, type SourceParseFailure, type SourceStyleFinding, type SourceStyleSeverity, type StartupRegistryVerdictFinding, type StartupRegistryVerdictOptions, type StartupRegistryVerdictSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, TRANSLATION_OPTION_KEY_UNKNOWN, TRANSLATION_SECTION_NAME_MISSING, TRANSLATION_TARGET_UNKNOWN, type TranslatableSectionFinding, type TranslatableSectionSeverity, type TranslationRefFinding, type TranslationRefSeverity, UNIQUE_DOUBLE_DECLARATION, UNIQUE_LEGACY_ORGANIZATION_COMPOSITE, UNIQUE_UNSCOPED_DECLARED_INDEX, VALIDATION_RULE_REGEX_UNCOMPILABLE, VALIDATION_RULE_SCHEMA_UNCOMPILABLE, VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT, VIEW_CONTAINER_SHAPE, VIEW_KEY_COLLISION, VIEW_REF_FORM_TARGET_KIND, VIEW_REF_FORM_TARGET_MISSING, VISIBILITY_BARE_IDENTIFIER, VISIBILITY_PREDICATE_OVER_BUDGET, VISIBILITY_PREDICATE_SYNTAX, VISIBILITY_ROOT_MISLAYERED, type ViewContainerFinding, type ViewContainerSeverity, type ViewRefFinding, type VisibilityFinding, type VisibilityLayer, type VisibilityOptions, type VisibilitySeverity, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_LEGACY_ANALYTICS_SHAPE, WIDGET_LEGACY_ANALYTICS_UNRENDERABLE, WIDGET_MEASURE_UNKNOWN, type WalkedComponent, type WalkedValidationRule, type WidgetBindingFinding, type WidgetBindingSeverity, buildAccessMatrix, checkSortDeclaration, describeParseFailure, diffAccessMatrix, extractHookBodyWriteSet, extractHookBodyWrites, findStartupRegistryVerdicts, findUnguardedNullableOperands, isSourceAuthoredPage, lintAutonumberFormats, lintDataModel, lintFlowPatterns, lintLegacyOrganizationComposites, lintLivenessProperties, lintUniqueDeclarations, lintUnscopedDeclaredIndexes, lintViewRefs, nearestRegisteredFormat, nullGuardMessage, validateActionBodyWrites, validateActionLocations, validateActionNameRefs, validateAiAgentAuthoring, validateAiSurfaceAffinity, validateAiToolReferences, validateApprovalApprovers, validateCapabilityReferences, validateChartBindings, validateComponentProps, validateDashboardActionRefs, validateEmptyCombinators, validateFilterTokens, validateFlowNodeWrites, validateFlowTemplatePaths, validateFlowTriggerReadiness, validateFormLayout, validateFunctionalCompleteness, validateHookBodyWrites, validateJsxPages, validateListViewMode, validateManagedApiMethods, validateNavAccess, validateNavObjectServability, validateNavTargetRefs, validateObjectReferences, validateOrgAxisRedLines, validatePageFieldBindings, validatePageSourceStyling, validatePredicatePathRefs, validatePresetComparands, validateReactPageProps, validateReactPages, validateReadonlyFlowWrites, validateRecordTitle, validateReferenceIntegrity, validateResponsiveStyles, validateRlsPredicateEnforceability, validateRuleCompilability, validateRuleSchemaFormats, validateSearchableFields, validateSecurityPosture, validateSecurityRoleWord, validateSeedReplaySafety, validateSeedStateMachine, validateSemanticRoles, validateSharingRuleEnforceability, validateSortableFields, validateStackExpressions, validateTranslatableSections, validateTranslationReferences, validateViewContainers, validateVisibilityPredicates, validateWidgetBindings, walkPageComponents };
4636
+ /** Which parse produced a finding — see the door table in the module header. */
4637
+ type EnvelopeAuditDoor = 'PageSchema' | 'PageComponentSchema' | 'ComponentPropsMap';
4638
+ /** One bare-expression position: authored as a string, normalized by a parse. */
4639
+ interface BareExpressionFinding {
4640
+ /** The consumer's page label, e.g. `<ExportName> (<page.name>)`. */
4641
+ page: string;
4642
+ /** Authored path from the page root, e.g. `slots.alerts[0].visibleWhen`. */
4643
+ path: string;
4644
+ /** The bare string the author wrote — what reaches the wire verbatim. */
4645
+ authored: string;
4646
+ /** Set when the parse also RENAMED the key (deprecated alias). */
4647
+ normalizedTo?: string;
4648
+ door: EnvelopeAuditDoor;
4649
+ }
4650
+ /** The three-door union over one page, findings and door preconditions both. */
4651
+ interface PageEnvelopeAudit {
4652
+ /** Every bare-expression position any door found, deduped by path, sorted. */
4653
+ findings: BareExpressionFinding[];
4654
+ /** Door 1 could not run — the page itself does not parse. */
4655
+ pageParseError?: string;
4656
+ /** Door 2 could not run for these components. */
4657
+ componentParseErrors: {
4658
+ path: string;
4659
+ type: string;
4660
+ issues: string;
4661
+ }[];
4662
+ /** Door 3 has no schema to parse these components' `properties` with. */
4663
+ unmappedTypes: {
4664
+ path: string;
4665
+ type: string;
4666
+ }[];
4667
+ /** Door 3 could not run — the authored bag is refused by its props schema. */
4668
+ unreadableProps: {
4669
+ path: string;
4670
+ type: string;
4671
+ issues: string;
4672
+ }[];
4673
+ /** How many components the walk reached (coverage floor for doors 2 and 3). */
4674
+ componentCount: number;
4675
+ }
4676
+ /**
4677
+ * Run all three parse doors over one authored page and union their findings.
4678
+ *
4679
+ * Read `findings` for the verdict, and assert the four precondition channels
4680
+ * separately (see the module header — a door that could not open reports
4681
+ * there, never as a silently smaller `findings`).
4682
+ *
4683
+ * Exported findings are deduped by path: a top-level component's
4684
+ * `visibleWhen` is legitimately seen by doors 1 AND 2, and reporting it twice
4685
+ * would read as two defects.
4686
+ *
4687
+ * @param page - The authored page object, exactly as exported (never parsed
4688
+ * first — the audit's whole subject is what the raw export serves).
4689
+ * @param pageLabel - How findings should name the page; consumers use
4690
+ * `<ExportName> (<page.name>)`.
4691
+ */
4692
+ declare function auditPageExpressionEnvelopes(page: unknown, pageLabel: string): PageEnvelopeAudit;
4693
+ /** Render findings as the actionable red an author reads in CI. */
4694
+ declare function renderBareExpressionFindings(findings: readonly BareExpressionFinding[]): string;
4695
+
4696
+ export { ACTION_API_UPDATE_READONLY_WHEN_FIELD, ACTION_BODY_SOURCE_UNPARSEABLE, ACTION_BODY_WRITE_EXCLUSIONS, ACTION_BODY_WRITE_PATTERNS, ACTION_BODY_WRITE_PATTERN_IDS, ACTION_BODY_WRITE_UNKNOWN_FIELD, ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR, ACTION_NAME_UNDEFINED, ACTION_NO_PLACEMENT, ACTION_RECORD_WRITE_DISCARDED, ACTION_RECORD_WRITE_PATTERNS, ACTION_RECORD_WRITE_PATTERN_IDS, AGENT_AUTHORING_WITHDRAWN, AI_SKILL_SURFACE_MISMATCH, AI_SKILL_TOOL_UNRESOLVED, APPROVAL_APPROVERS_MAY_RESOLVE_EMPTY, APPROVAL_APPROVER_CROSS_ORG_UNSUPPORTED, APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER, APPROVAL_APPROVER_TYPE_DEPRECATED, APPROVAL_APPROVER_TYPE_UNKNOWN, APPROVAL_APPROVER_TYPE_UNSUPPORTED, APPROVAL_DECISION_OUTPUTS_RESERVED, APPROVAL_ESCALATION_REASSIGN_NO_TARGET, APPROVAL_EXPRESSION_INVALID, APPROVAL_EXPRESSION_NO_EMPTY_POLICY, AUTONUMBER_LITERAL_TOKEN, AUTONUMBER_OPTIONAL_FIELD, AUTONUMBER_SELF_REFERENCE, AUTONUMBER_UNKNOWN_FIELD, type ActionBodyWriteExclusion, type ActionBodyWriteFinding, type ActionBodyWriteSeverity, type ActionLocationsFinding, type ActionLocationsSeverity, type ActionNameRefFinding, type ActionNameRefSeverity, type AiAgentAuthoringFinding, type AiAgentAuthoringSeverity, type AiSurfaceAffinityFinding, type AiSurfaceAffinitySeverity, type AiToolRefFinding, type AiToolRefSeverity, type ApprovalApproverFinding, type ApprovalApproverSeverity, type AutonumberLintFinding, type BareExpressionFinding, type BodyWritePatternExclusion, CAPABILITY_REFERENCE_UNKNOWN, CHART_AXIS_NOT_SELECTED, CHART_CONFIG_MISSING, CHART_DATASET_UNKNOWN, CHART_DIMENSIONS_MISSING, CHART_DIMENSION_UNKNOWN, CHART_FIELD_UNKNOWN, CHART_MEASURES_MISSING, CHART_MEASURE_UNKNOWN, COMPONENT_PROPS_INVALID, COMPONENT_PROPS_UNKNOWN_KEY, COMPONENT_TYPE_UNKNOWN, type CapabilityRefFinding, type CapabilityRefSeverity, type ChartBindingFinding, type ChartBindingSeverity, type CheckedParse, type CheckedParseOptions, type ComponentPropsFinding, type ComponentPropsSeverity, type ComponentTypeFinding, DASHBOARD_ACTION_ROUTE_UNRESOLVED, DASHBOARD_ACTION_TARGET_UNDEFINED, DASHBOARD_FILTER_FIELD_NOT_INCLUDED, DASHBOARD_FILTER_FIELD_UNKNOWN, DASHBOARD_FILTER_FIELD_UNPROVISIONED, DATASET_FIELD_NOT_INCLUDED, DATASET_FIELD_UNKNOWN, DATASET_FILTER_FIELD_UNKNOWN, DATASET_INCLUDE_UNKNOWN, DEFAULT_AGENT_LEGACY_ALIAS, DEFAULT_AGENT_OUTSIDE_ROSTER, type DashboardActionRefFinding, type DashboardActionRefSeverity, type DatasetRefFinding, type DatasetRefSeverity, type EmptyCombinatorFinding, type EmptyCombinatorSeverity, type EnvelopeAuditDoor, type ExprIssue, type ExtractedHookBodyWrite, type ExtractedHookBodyWriteSet, CARRIER_ROOTS as FIELD_CARRIER_ROOTS, CONSUMER_ROOTS as FIELD_CONSUMER_ROOTS, FIELD_GROUP_EMPTY, FIELD_GROUP_SHADOWED, FIELD_GROUP_UNDECLARED, FIELD_NO_CONSUMERS, FIELD_RULE_BOUND_ROOTS, FILTER_EMPTY_COMBINATOR, FILTER_EMPTY_NODE, FILTER_PRESET_COMPARAND, FILTER_TOKEN_UNKNOWN, FLOW_APPROVAL_REVISE_DEAD_END, FLOW_APPROVAL_REVISE_DISABLED, FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED, FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE, FLOW_BARE_DOLLAR_REF, FLOW_BRANCH_LABEL_UNMATCHED, FLOW_DATE_EQUALITY_FILTER, FLOW_DECISION_UNCONDITIONAL_BRANCH, FLOW_DEFAULT_EDGE_WITH_CONDITION, FLOW_DOUBLE_BRACE_INTERP, FLOW_DRAFT_STATUS_AMBIGUOUS, FLOW_ERROR_LABEL_NOT_FAULT, FLOW_FILTER_TOKEN_UNKNOWN, FLOW_INERT_NODE_CONDITION, FLOW_LOOP_BODY_UNCONTAINED, FLOW_MULTIPLE_DEFAULT_EDGES, FLOW_MULTI_WRITE_UNFILTERED, FLOW_NODE_WRITE_UNKNOWN_FIELD, FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR, FLOW_PHANTOM_AGGREGATION, FLOW_RUNAS_UNSCOPED, FLOW_TEMPLATE_DATE_FUNCTIONS, FLOW_TEMPLATE_FIELD_UNPROVISIONED, FLOW_TEMPLATE_LOOKUP_TRAVERSAL, FLOW_TEMPLATE_UNKNOWN_FIELD, FLOW_TEMPLATE_VALUE_FUNCTIONS, FLOW_TIME_RELATIVE_ANTIPATTERN, FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID, FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE, FLOW_TRIGGER_UNKNOWN_EVENT, FLOW_TRIGGER_UNKNOWN_OBJECT, FLOW_TRIGGER_UNROUTABLE, FLOW_TRY_CATCH_WITHOUT_CATCH, FLOW_UPDATE_READONLY_FIELD, FLOW_UPDATE_READONLY_WHEN_FIELD, FLOW_WRITE_NODE_TYPES, FLOW_WRITE_NODE_TYPES_DEFERRED, FORM_COLSPAN_ABSOLUTE, FORM_FIELD_UNKNOWN, FORM_SECTION_GROUP_UNKNOWN, type FieldConsumerFinding, type FieldConsumerSeverity, type FieldConsumerVerdict, type FieldPathAccount, type FieldPathVerdict, type FilterFieldKey, type FilterTokenFinding, type FilterTokenSeverity, type FlowFilterTokenFinding, type FlowLintFinding, type FlowNodeWriteFinding, type FlowNodeWriteSeverity, type FlowTemplatePathFinding, type FlowTemplatePathSeverity, type FlowTemplateTokenVerdict, type FlowTriggerReadinessFinding, type FlowTriggerReadinessSeverity, type FlowWriteNodeDeferral, type FormLayoutFinding, type FormLayoutSeverity, type FunctionalCompletenessFinding, type FunctionalCompletenessSeverity, type GraphField, type GraphObject, HOOK_API_UPDATE_READONLY_FIELD, HOOK_API_UPDATE_READONLY_WHEN_FIELD, HOOK_BODY_SOURCE_UNPARSEABLE, HOOK_BODY_WRITE_EXCLUSIONS, HOOK_BODY_WRITE_PATTERNS, HOOK_BODY_WRITE_PATTERN_IDS, HOOK_BODY_WRITE_UNKNOWN_FIELD, HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR, type HookBodyWriteFinding, type HookBodyWritePattern, type HookBodyWriteSeverity, type JsxPageFinding, type JsxPageSeverity, LIST_VIEW_FIELD_DOTTED, LIST_VIEW_FIELD_UNKNOWN, LIST_VIEW_FILTERS_IN_VIEWS_MODE, LIVENESS_DEAD_PROPERTY, LIVENESS_EXPERIMENTAL_PROPERTY, LIVENESS_LIVE_ELSEWHERE_PROPERTY, LIVENESS_PLANNED_PROPERTY, type LintIssue, type ListViewFieldRefFinding, type ListViewFieldRefSeverity, type ListViewModeFinding, type ListViewModeSeverity, type LivenessLintFinding, type LocatedLintIssue, MANAGED_API_METHOD_UNAFFORDABLE, MAX_SCHEMA_WALK_DEPTH, MEASURE_AGGREGATE_INCOHERENT, type ManagedApiMethodFinding, NAV_OBJECT_UNGRANTED, NAV_OBJECT_UNSERVABLE, NAV_TARGET_UNRESOLVED, NULL_GUARD_HINT, type NavAccessFinding, type NavAccessSeverity, type NavObjectServabilityFinding, type NavTargetRefFinding, type NavTargetRefSeverity, type NullGuardFinding, type NullGuardOptions, OBJECTUI_DERIVATION_PREDICATES, OBJECT_FIELD_REF_UNKNOWN, OBJECT_REFERENCE_UNKNOWN, OBJECT_REFERENCE_UNREGISTERED_PLATFORM, OPEN_VOCABULARY_PROBES, ORG_AXIS_CROSS_ORG_BU_GRANT, ORG_AXIS_PERMISSION_INHERITANCE, type ObjectFieldRefFinding, type ObjectFieldRefSeverity, type ObjectGraph, type ObjectRefFinding, type ObjectRefSeverity, type OrgAxisFinding, type OrgAxisSeverity, PAGE_FIELD_UNKNOWN, PAGE_FIELD_UNPROVISIONED, PAGE_SECTION_GROUP_UNKNOWN, PAGE_SOURCE_CLASSNAME, PAGE_VISUALIZATION_WITHOUT_BINDING, PARSE_FAILURE_HINT, PREDICATE_PATH_UNRESOLVED, PREDICATE_PATH_UNROOTED, PREDICATE_RHS_PATH_SHAPED, PRE_SEAL_PHASES, type PageEnvelopeAudit, type PageFieldFinding, type PageFieldSeverity, type PageVisualizationFinding, type PageVisualizationSeverity, type PredicatePathFinding, type PredicatePathOptions, type PredicatePathSeverity, type PresetComparandFinding, type PresetComparandSeverity, REACT_BLOCK_NEEDS_RECORD_CONTEXT, REACT_CHART_AGGREGATE_INVALID, REACT_CHART_AXIS_UNKNOWN, REACT_CHART_DRILLDOWN_INVALID, REACT_CHART_FIELD_UNKNOWN, REACT_CHART_FIELD_UNPROVISIONED, REACT_PAGE_SOURCE_UNPARSEABLE, REACT_PROP_DEPRECATED, REACT_PROP_RETIRED, READONLY_ACTION_WRITE_EXCLUSIONS, READONLY_ACTION_WRITE_PATTERN_IDS, READONLY_HOOK_WRITE_EXCLUSIONS, READONLY_HOOK_WRITE_PATTERN_IDS, REFERENCE_INTEGRITY_RULES, RELATIONSHIP_FIELD_TYPES, RLS_PREDICATE_OVER_BUDGET, RLS_PREDICATE_UNENFORCEABLE, RLS_PREDICATE_UNPARSEABLE, RUNTIME_AJV_OPTIONS, type ReactPageFinding, type ReactPageSeverity, type ReactPropFinding, type ReactPropSeverity, type ReadonlyActionWriteFinding, type ReadonlyActionWriteSeverity, type ReadonlyFlowWriteFinding, type ReadonlyFlowWriteSeverity, type ReadonlyHookWriteFinding, type ReadonlyHookWriteSeverity, type RecordTitleFinding, type RecordTitleSeverity, type ReferenceIntegrityFinding, type ReferenceIntegrityRule, type ReferenceIntegritySeverity, type RlsPredicateFinding, type RlsPredicateSeverity, type RuleCompilabilityFinding, type RuleCompilabilitySeverity, type RuleSchemaFormatFinding, type RuleSchemaFormatSeverity, SEAL_MARKERS, SEARCHABLE_FIELD_UNKNOWN, SEARCHABLE_FIELD_UNPROVISIONED, SEARCHABLE_FIELD_UNSEARCHABLE, SECURITY_ANCHOR_HIGH_PRIVILEGE, SECURITY_BOOK_AUDIENCE_UNKNOWN_SET, SECURITY_CBP_AMBIGUOUS_RELATION, SECURITY_CBP_NO_RELATION, SECURITY_DELEGATION_MISSING_REASON, SECURITY_EXTERNAL_WIDER, SECURITY_FLS_UNKNOWN_FIELD, SECURITY_FLS_UNQUALIFIED_KEY, SECURITY_GRANT_EXPIRED_AT_AUTHORING, SECURITY_MASTER_DETAIL_UNGRANTED, SECURITY_OWD_ALIAS, SECURITY_OWD_UNSET, SECURITY_PRIVATE_NO_READSCOPE, SECURITY_ROLE_WORD, SECURITY_WILDCARD_VAMA, SEED_INSERT_MODE_DUPLICATES_ON_REPLAY, SEED_VALUE_OUTSIDE_STATE_MACHINE, SEMANTIC_ROLE_FIELD_UNKNOWN, SEMANTIC_ROLE_FIELD_UNPROVISIONED, SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT, SHARING_RULE_OBJECT_NOT_SHAREABLE, SHARING_RULE_RUNTIME_VARIABLE_CONDITION, SHARING_RULE_UNLOWERABLE_CONDITION, SORT_FIELD_UNKNOWN, SORT_FIELD_UNPROVISIONED, SORT_FIELD_UNSORTABLE, STARTUP_OPEN_VOCABULARY_VERDICT, STARTUP_SOURCE_UNPARSEABLE, STARTUP_VERDICT_ASSERTIVE_WORDING, STARTUP_VERDICT_HINT, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SearchableFieldFinding, type SearchableFieldRole, type SearchableFieldSeverity, type SectionGroupFinding, type SectionGroupRef, type SecurityFinding, type SecuritySeverity, type SeedReplaySafetyFinding, type SeedReplaySafetySeverity, type SeedStateMachineFinding, type SeedStateMachineSeverity, type SemanticRoleFinding, type SemanticRoleSeverity, type Severity, type SharingRuleEnforceabilityFinding, type SharingRuleEnforceabilitySeverity, type SortableFieldFinding, type SortableFieldSeverity, type SourceParseFailure, type SourceStyleFinding, type SourceStyleSeverity, type StartupRegistryVerdictFinding, type StartupRegistryVerdictOptions, type StartupRegistryVerdictSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, TRANSLATION_OPTION_KEY_UNKNOWN, TRANSLATION_SECTION_NAME_MISSING, TRANSLATION_TARGET_UNKNOWN, type TranslatableSectionFinding, type TranslatableSectionSeverity, type TranslationRefFinding, type TranslationRefSeverity, UNIQUE_DOUBLE_DECLARATION, UNIQUE_LEGACY_ORGANIZATION_COMPOSITE, UNIQUE_UNSCOPED_DECLARED_INDEX, VALIDATION_RULE_REGEX_UNCOMPILABLE, VALIDATION_RULE_SCHEMA_UNCOMPILABLE, VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT, VIEW_CONTAINER_SHAPE, VIEW_KEY_COLLISION, VIEW_PAGE_UNRESOLVED, VIEW_REF_FORM_TARGET_KIND, VIEW_REF_FORM_TARGET_MISSING, VIEW_REF_NAV_VIEW_MISSING, VISIBILITY_BARE_IDENTIFIER, VISIBILITY_PREDICATE_OVER_BUDGET, VISIBILITY_PREDICATE_SYNTAX, VISIBILITY_PREDICATE_UNKNOWN_FUNCTION, VISIBILITY_ROOT_MISLAYERED, type ViewContainerFinding, type ViewContainerSeverity, type ViewPageRefFinding, type ViewPageRefSeverity, type ViewRefFinding, type VisibilityFinding, type VisibilityLayer, type VisibilityOptions, type VisibilitySeverity, type VisualizationPredicate, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_FILTER_FIELD_NOT_INCLUDED, WIDGET_FILTER_FIELD_UNKNOWN, WIDGET_LEGACY_ANALYTICS_SHAPE, WIDGET_LEGACY_ANALYTICS_UNRENDERABLE, WIDGET_MEASURES_MISSING, WIDGET_MEASURE_UNKNOWN, WIDGET_SORTBY_UNSELECTED, type WalkedComponent, type WalkedValidationRule, type WidgetBindingFinding, type WidgetBindingSeverity, auditPageExpressionEnvelopes, authorWarnedProperties, buildAccessMatrix, checkSectionGroupRefs, checkSortDeclaration, classifyFlowTemplateToken, describeFieldPathVerdict, describeParseFailure, diffAccessMatrix, extractHookBodyWriteSet, extractHookBodyWrites, fieldRuleRootIssue, findStartupRegistryVerdicts, findUnguardedNullableOperands, indexObjectFieldGroups, indexObjectGraph, isSourceAuthoredPage, isUnjudgeable, joinablePrefixes, lintAutonumberFormats, lintDataModel, lintFlowPatterns, lintLegacyOrganizationComposites, lintLivenessProperties, lintUniqueDeclarations, lintUnscopedDeclaredIndexes, lintViewRefs, listNames, nearestName, nearestRegisteredFormat, nullGuardMessage, renderBareExpressionFindings, resolveFieldPath, sectionGroupRefs, suggestName, validateActionBodyWrites, validateActionLocations, validateActionNameRefs, validateAiAgentAuthoring, validateAiSurfaceAffinity, validateAiToolReferences, validateApprovalApprovers, validateCapabilityReferences, validateChartBindings, validateComponentProps, validateComponentTypes, validateDashboardActionRefs, validateDatasetReferences, validateEmptyCombinators, validateFieldConsumers, validateFilterTokens, validateFlowFilterTokens, validateFlowNodeWrites, validateFlowTemplatePaths, validateFlowTriggerReadiness, validateFormLayout, validateFunctionalCompleteness, validateHookBodyWrites, validateJsxPages, validateListViewFieldRefs, validateListViewMode, validateManagedApiMethods, validateNavAccess, validateNavObjectServability, validateNavTargetRefs, validateObjectFieldRefs, validateObjectReferences, validateOrgAxisRedLines, validatePageFieldBindings, validatePageSourceStyling, validatePageVisualizationBindings, validatePredicatePathRefs, validatePresetComparands, validateReactPageProps, validateReactPages, validateReadonlyActionWrites, validateReadonlyFlowWrites, validateReadonlyHookWrites, validateRecordTitle, validateReferenceIntegrity, validateResponsiveStyles, validateRlsPredicateEnforceability, validateRuleCompilability, validateRuleSchemaFormats, validateSearchableFields, validateSecurityPosture, validateSecurityRoleWord, validateSeedReplaySafety, validateSeedStateMachine, validateSemanticRoles, validateSharingRuleEnforceability, validateSortableFields, validateStackExpressions, validateTranslatableSections, validateTranslationReferences, validateViewContainers, validateViewPageRefs, validateVisibilityPredicates, validateWidgetBindings, walkFilterFieldKeys, walkPageComponents };