@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/CHANGELOG.md +3072 -0
- package/dist/index.cjs +9549 -6558
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2120 -611
- package/dist/index.d.ts +2120 -611
- package/dist/index.js +8062 -5133
- package/dist/index.js.map +1 -1
- package/dist/{runtime-cV-l_vtC.d.cts → runtime-S3OZQDZV.d.cts} +37 -2
- package/dist/{runtime-cV-l_vtC.d.ts → runtime-S3OZQDZV.d.ts} +37 -2
- package/dist/runtime.cjs +4341 -1681
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +4237 -1566
- package/dist/runtime.js.map +1 -1
- package/package.json +24 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,3077 @@
|
|
|
1
1
|
# @objectstack/lint
|
|
2
2
|
|
|
3
|
+
## 17.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 954cb0b: feat(service-automation): an `assignment` value may be a CEL envelope — evaluated at run time, validated at `registerFlow`, `objectstack validate` and the runtime publish gate (#15137, the executor half of #14149)
|
|
8
|
+
|
|
9
|
+
<!-- adr-0087: not-required (no-migration-prescription) No authorable key is renamed, retired or re-typed: the `assignments` map and every value form it accepted still parse. The only newly refused shape is a malformed CEL value envelope, a spelling declared one day earlier in #15113 and offered by no authoring surface before it, so `objectstack migrate meta` has nothing to rewrite and this changeset carries no rewrite instructions for a consumer to follow. -->
|
|
10
|
+
|
|
11
|
+
**BREAKING** in the accept-set sense, landing in the launch window as `minor`
|
|
12
|
+
(the lockstep convention; the level also follows the 2026-09-04 bump ruling —
|
|
13
|
+
this adds `AutomationEngine.evaluateValueEnvelope` to a published surface, and an
|
|
14
|
+
additive widening is at least `minor`). No ADR-0087 conversion: no authorable key
|
|
15
|
+
is renamed or retired, and the shape this refuses was never a shape any surface
|
|
16
|
+
offered.
|
|
17
|
+
|
|
18
|
+
The maintainer's 2026-09-02 ruling on #14149 made an assignment value able to be
|
|
19
|
+
a CEL **value** expression, so the declared stdlib (`joinNonEmpty`, `map`, `size`
|
|
20
|
+
…) is finally reachable from metadata — until now CEL was only ever asked for a
|
|
21
|
+
boolean. The spec half landed the contract (PR #15113); this is the half that
|
|
22
|
+
makes it do something.
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
# before: written into the variable verbatim, and rendered by `notify` as
|
|
26
|
+
# {"dialect":"cel","source":"joinNonEmpty(...)"}
|
|
27
|
+
# now: evaluated — digest is "Renewal due\nInvoice overdue"
|
|
28
|
+
assignments:
|
|
29
|
+
digest: { dialect: cel, source: 'joinNonEmpty(rows.map(r, r.subject), "\n")' }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- **Evaluated at run time.** The built-in `assignment` executor evaluates a
|
|
33
|
+
`value`-role envelope with the expression engine and assigns the result, in the
|
|
34
|
+
same CEL scope a flow predicate is evaluated in (one shared scope builder, so a
|
|
35
|
+
predicate and a value expression cannot disagree about what `rows` means). A
|
|
36
|
+
plain string keeps today's `{token}` interpolation, and every other literal is
|
|
37
|
+
still assigned as data.
|
|
38
|
+
- **Refused at three doors.** A malformed envelope now stops the flow registering
|
|
39
|
+
(`registerFlow` throws, the severity a malformed predicate gets) and surfaces as
|
|
40
|
+
a located `error` finding naming the node and the author's own variable —
|
|
41
|
+
`config.assignments.digest` — both at `objectstack validate` and at the runtime
|
|
42
|
+
publish gate a Studio / REST / MCP flow write goes through
|
|
43
|
+
(`validateStackExpressions` is registered `CLI_AND_RUNTIME`, `runtimeTypes:
|
|
44
|
+
['flow']`). Malformed is a composition, not a fixed list: whatever
|
|
45
|
+
`AssignmentValueSchema` refuses in the envelope's shape — among them a missing,
|
|
46
|
+
empty or non-string `source`, a dialect other than `cel`, a non-object `meta` —
|
|
47
|
+
and then CEL that does not parse. All three doors derive that set from the same
|
|
48
|
+
two published validators, so none refuses a shape the executor would have run,
|
|
49
|
+
and a registered flow never faults for a shape those validators judge malformed.
|
|
50
|
+
Two shapes sit outside what either validator can judge — an `ast`-only envelope
|
|
51
|
+
and a whitespace-only `source` (it passes `min(1)` and reads as "not authored"
|
|
52
|
+
to the validator, while the CEL engine parses it untrimmed) — and those fault
|
|
53
|
+
loudly at run time rather than assigning a value. Both are pinned and tracked in
|
|
54
|
+
#15430.
|
|
55
|
+
- **Only the canonical map.** The ledger declares `assignment.assignments.*` and
|
|
56
|
+
nothing else, so the two legacy shapes the executor still normalizes — the
|
|
57
|
+
`assignments: [{ variable, value }]` array and the bare `{ <variable>: <value> }`
|
|
58
|
+
config — keep every meaning they had, envelope-shaped values included.
|
|
59
|
+
`AssignmentConfigSchema` is deliberately NOT wired into `parseNodeConfig` for the
|
|
60
|
+
array form: refusing it would break flows that register today, and that refusal
|
|
61
|
+
is a maintainer ruling rather than a lane's call (#15137 ask 3).
|
|
62
|
+
|
|
63
|
+
**What changes silently, and how far it reaches.** A flow that today authors an
|
|
64
|
+
envelope-shaped object *as data* in the canonical `assignments` map now evaluates
|
|
65
|
+
it — no error on either side, a different value. The discriminator is the spec's
|
|
66
|
+
own `isExpressionEnvelopeShaped`: a plain object naming a **string** `dialect`,
|
|
67
|
+
in the declared map only. Data that names no `dialect`, names a non-string one,
|
|
68
|
+
nests the envelope one level down, or sits in either legacy shape is untouched
|
|
69
|
+
and byte-identical. The remaining overlap — a well-formed
|
|
70
|
+
`{ dialect: 'cel', source: … }` written as data in the canonical map — is exactly
|
|
71
|
+
the spelling the ruling reinterprets; every near-miss the two validators can
|
|
72
|
+
judge now refuses loudly at registration instead of changing value in silence.
|
|
73
|
+
- 36a16d0: Two new widget-binding rule ids for a chart widget with an empty selection
|
|
74
|
+
|
|
75
|
+
`validateWidgetBindings` reported nothing about two dataset-bound chart shapes that the
|
|
76
|
+
`@object-ui` revision this repo pins (`.objectui-sha`) visibly degrades. Both are now
|
|
77
|
+
warnings, suppressible per widget with `suppressWarnings: ['<rule-id>']`:
|
|
78
|
+
|
|
79
|
+
- `chart-measures-missing` — a chart-family widget selects no measures (`values` empty or
|
|
80
|
+
absent). `DatasetWidget.tsx:683` returns the authoring placeholder "Pick measures
|
|
81
|
+
(values) for this dataset widget." before any query runs, above every family branch, so
|
|
82
|
+
no chart is drawn at all.
|
|
83
|
+
- `chart-dimensions-missing` — a chart-family widget selects at least one measure but no
|
|
84
|
+
dimensions. `DatasetWidget.tsx:423` reads
|
|
85
|
+
`const isMetric = METRIC_TYPES.has(widgetType) || dimensions.length === 0;`, so the
|
|
86
|
+
widget renders as a single KPI number and the declared chart family is silently ignored.
|
|
87
|
+
The hint steers the author to a dimension, or to the `metric`/`kpi` family that matches
|
|
88
|
+
what actually renders.
|
|
89
|
+
|
|
90
|
+
Warning tier rather than error for both: an empty selection is a work-in-progress state a
|
|
91
|
+
build must tolerate, and erroring would gate the `sys_metadata` publish path on a
|
|
92
|
+
half-authored widget. Neither shape is folded into `chart-config-missing` — neither is
|
|
93
|
+
caused by, nor repairable with, `chartConfig`, which carries presentation only.
|
|
94
|
+
|
|
95
|
+
"Chart family" is derived, not hand-listed: every declared `ChartTypeSchema` option that
|
|
96
|
+
the pinned renderer routes to its chart branch — the taxonomy minus the renderer's own
|
|
97
|
+
`METRIC_TYPES` (`metric`, `kpi`, `gauge`, `solid-gauge`, `bullet`) and its `table`/`pivot`
|
|
98
|
+
tabular test. A `metric` tile with no dimensions, such as the shipped `system_overview`
|
|
99
|
+
board's own KPI tiles, is therefore not a finding.
|
|
100
|
+
- c01b3a6: `chart-field-unknown` drops to `warning` on the three `chartConfig` binding keys the pinned renderer refuses, and says what actually happens
|
|
101
|
+
|
|
102
|
+
The rule id covers exactly three positions, and the `@object-ui` revision this repo pins (`.objectui-sha`) refuses all three as bindings, so none of them can produce the data failure the messages described:
|
|
103
|
+
|
|
104
|
+
- `chartConfig.xAxis.field` — `axisPresentation` (`@object-ui/core` `src/utils/chart-presentation.ts`) builds the axis presentation **minus** its `field`. The x-axis key is `buildChartSeries`' `xAxisKey`, i.e. the widget's `dimensions[0]`; an authored `field` re-points nothing.
|
|
105
|
+
- `chartConfig.yAxis[].field` — the same call, per entry. The entry keeps its slot (the count is what turns on a secondary axis) and its scale and chrome; only the binding is dropped.
|
|
106
|
+
- `chartConfig.series[].name` — `mergeAuthoredSeries` pairs an authored entry with the derived series whose `dataKey` it equals, one per entry of `values`. An entry naming no derived series is ignored whole, so the presentation hung on it — the mark, the colour, the stack, the axis side — lands on nothing.
|
|
107
|
+
|
|
108
|
+
The renderer pins this by name in `DatasetWidget.chartConfig.test.tsx` ("ignores an authored axis `field` and keeps the derived axis binding", "ignores an authored series and keeps one derived series per measure").
|
|
109
|
+
|
|
110
|
+
So the old message — "the query result will not contain it" — named a query failure that never happens, and `error` blocked a build and a Studio publish for a key that changes nothing at runtime. That is the class `widget-legacy-analytics-shape` reports at `warning` in the same file ("the dashboard renderer ignores them … a silent no-op"), and this id now carries the same tier, the same suppressibility (`suppressWarnings: ['chart-field-unknown']` per widget) and the same kind of sentence. Each message states its own consequence, because the axis positions and the series position are refused for different reasons.
|
|
111
|
+
|
|
112
|
+
The finding is **kept**, not deleted: unlike the `chart-config-missing` over-reach this measurement came from, the metadata really is wrong — the author wrote a binding and believes it is in force.
|
|
113
|
+
|
|
114
|
+
## Migration
|
|
115
|
+
|
|
116
|
+
**A publish that used to be refused now succeeds.** Ruled 2026-08-15, `validateWidgetBindings` put its whole error set on the `sys_metadata` publish door (Studio / REST `/meta` / MCP) as one "this board cannot render" reference-integrity class. That class was six ids and is now five — `chart-field-unknown` has left it. A dashboard write whose only reference-integrity problem is a refused `chartConfig` binding key is no longer a 422 `INVALID_METADATA`; it publishes, and the finding rides the non-blocking `advisories` channel on the 2xx response instead. The other five (`widget-dataset-unknown`, `widget-dimension-unknown`, `widget-measure-unknown`, `widget-legacy-analytics-unrenderable`, `dashboard-filter-field-unknown`) are unchanged.
|
|
117
|
+
|
|
118
|
+
Same direction on the CLI: `os validate` / `os build` / `os lint` report the finding at `warning`, so a stack that used to fail the build over one of these keys now exits 0 with an advisory. If you were relying on the build to stop on it, add the key to your own gate, or fix the binding — the fix has not changed:
|
|
119
|
+
|
|
120
|
+
- point `xAxis.field` at a dimension the widget selects (or drop the key — `xAxis` carries presentation only);
|
|
121
|
+
- point `yAxis[].field` at a selected measure (or drop it — `yAxis[]` carries presentation only);
|
|
122
|
+
- name a selected measure in `series[].name`, remembering that post-cutover (ADR-0021) result rows are keyed by the dataset's measure **name** (`sum_amount`), not the base column (`amount`).
|
|
123
|
+
|
|
124
|
+
A deliberately inert key can be silenced per widget with `suppressWarnings: ['chart-field-unknown']`.
|
|
125
|
+
- 56fe8c2: A flow predicate authored as a CEL envelope is now refused at build time, instead of running unread by either validator.
|
|
126
|
+
|
|
127
|
+
A `predicate`-role expression slot holds **bare CEL text** — `DecisionConditionSchema.expression` is declared `z.string()`, and so is a screen field's `visibleWhen`. An author who instead wrote the `{ dialect, source }` expression *envelope* there reached a shape nothing could see: a flow node's `config` is an open `z.record(z.unknown())` that no Zod schema is parsed against, the unknown-key walk exempts the schemaless node types on purpose (`decision` publishes no descriptor `configSchema`), and the expression ledger's `predicate` arm skipped every non-string as "a type violation for the schema pass to report" — a schema pass that, for those node types, does not exist. `registerFlow` accepted the flow, `objectstack validate` reported nothing, and the evaluator was the only layer that ever read the predicate.
|
|
128
|
+
|
|
129
|
+
- `resolveFlowNodeExpressions` now emits a non-string sitting in a `predicate` slot, and the new `predicateSlotRefusal` / `PREDICATE_SLOT_STRING_REFUSAL` say why it is refused — one notion, derived once, read by both validators so build time and author time cannot disagree about the shape. `flow-template` slots keep the old rule: no validator implements that dialect, so a finding there is one nobody could judge.
|
|
130
|
+
- `registerFlow` throws, naming the node, the slot and the index, and attributing the finding to the envelope's own `source`. `objectstack validate` reports the same refusal as a located `error`.
|
|
131
|
+
|
|
132
|
+
**String predicates are untouched, deliberately.** A whitespace-only string still means "not authored" on both sides, exactly as before; what a non-empty string *says* is still judged by `validateExpression('predicate', …)`, brace trap and all. Only the shape moved.
|
|
133
|
+
|
|
134
|
+
An app that authored an envelope in one of these slots now fails to register with a message naming the slot; the fix is to write the predicate as bare CEL text (`record.rating >= 4`). The `{ dialect, source }` envelope remains the `value`-role spelling, on the `assignment` node's `assignments` map.
|
|
135
|
+
- d61bad0: New gating rule `flow-filter-token-unknown`: a `{…}` filter token in a flow node's `config.filter` that NEITHER `{…}` dialect can resolve is now an authoring-time `error`.
|
|
136
|
+
|
|
137
|
+
`filter-token-unknown` walks seven presentation collections and not `flows`, so `{TOMORROW()}` in a list view's filter failed the build while the identical string in a flow node's `config.filter` was silent — even though this package's other filter rules (`empty-combinator`, the preset-comparand rules) have reached flows all along.
|
|
138
|
+
|
|
139
|
+
The gap was not an oversight to close by adding a root. A flow node's filter is interpolated by the automation template evaluator **before** ObjectQL sees it, and only what that evaluator cannot resolve is handed on. Judging a flow filter against the ObjectQL vocabulary — the obvious one-line fix — reports every legitimate `{record.id}` and `{recordId}`: measured at **7 findings, all 7 false positives**, on this repo's own example apps. So the new rule is a second rule id with the flow dialect as its reference set, and `filter-token-unknown`'s surface list is untouched.
|
|
140
|
+
|
|
141
|
+
Reported (`error`): a call to a name in neither table — `{TOMORROW()}`, `{ROUND(x)}`, `{Math.round(x)}`, `{DATEADD(day, -45)}`. The flow template dialect's function vocabulary is closed (`round` / `floor` / `ceil` / `abs` / `min` / `max`, plus the whole-token `NOW()` / `TODAY()` with an optional `± N` day offset), and the evaluator already raises a guard refusal on anything else — so the node cannot run at all, and the build was shipping a flow whose runtime was already decided. This is the same severity axis `flow-template-unknown-field` applies at this exact position.
|
|
142
|
+
|
|
143
|
+
Silent, deliberately: `{TODAY() - 45}` and every other whole-token date form; `{$User.Id}`; `{current_user_id}` / `{today}` / `{30_days_ago}` and the rest of the filter placeholders; and every bare or dotted identifier (`{recordId}`, `{record.id}`, `{currentTask.id}`), which addresses the run's variable map — declared flow variables, node outputs, and the trigger record's own fields — and is not decidable from authored metadata.
|
|
144
|
+
|
|
145
|
+
Finding delta on this repo's example apps: **0**. Expect a new `error` only where a flow filter calls a function the evaluator would refuse at run time.
|
|
146
|
+
- a87163c: New advisory rule `field-no-consumers` (`validateFieldConsumers`): a field declared on an object that nothing in the stack reads or displays is reported as a `warning` by `os validate`, `os build` and `os lint`.
|
|
147
|
+
|
|
148
|
+
Until now such a field was schema-valid and passed every platform check — the declaration was inert and nothing in the toolchain said so. The rule is object-aware (the same field name on two objects gets two verdicts, resolved against the object whose declaration encloses each reference), and it distinguishes consumers from carriers: a view column, form section, page binding, flow node, dataset dimension, widget filter, formula, validation, hook or action is a consumer; a translation label, a seed value, an import-mapping column, a field-level permission grant or a flow that only writes the field is a carrier and never counts. The finding carries the verdict (`carrier-only` with the carrier paths a removal must clean, or `inert`), the roots scanned, and — when the name is also declared elsewhere — the other objects, so a per-object verdict is never mistaken for a name-level one.
|
|
149
|
+
|
|
150
|
+
Exempt, each derived from the spec rather than listed by hand: the registry-injected system columns an author re-declared, the record's title field (ADR-0079 `nameField` ladder), and `master_detail` fields (ADR-0035 — cascade delete, `controlled_by_parent` sharing and roll-ups read the relationship by declaration). A stack that declares no consumer root at all (objects only, or objects plus carriers) is not judged: its consumers live in another package. Test fixtures are never scanned.
|
|
151
|
+
|
|
152
|
+
Public surface: `validateFieldConsumers`, `FIELD_NO_CONSUMERS`, `FIELD_CONSUMER_ROOTS`, `FIELD_CARRIER_ROOTS`, and the `FieldConsumerFinding` / `FieldConsumerVerdict` / `FieldConsumerSeverity` types.
|
|
153
|
+
- 0cde37d: `objectstack lint` now judges hooks authored as inline `handler` functions with the same write-set rules it already applied to explicit `body` hooks.
|
|
154
|
+
|
|
155
|
+
The `hook-body-write-unknown-field`, `hook-body-write-unprovisioned-anchor`, `hook-body-source-unparseable`, `hook-api-update-readonly-field` and `hook-api-update-readonly-when-field` rules open on `body.language === 'js'`. A hook written as `handler: async (ctx) => { … }` carries no `body`, so on the stack `objectstack lint` handed the rule registry the whole family returned before reading anything — while the reference app authors every one of its hooks that way. `objectstack build` never had the gap: it lowers each inline handler to a metadata body before it parses and judges the lowered stack.
|
|
156
|
+
|
|
157
|
+
`objectstack lint` now hands the registry's parsed-tier rules that same lowered view (the `lowerCallables` pass the build runs), so a handler-authored hook writing a `readonly` field through `ctx.api` is refused by the pre-flight exactly as the build would refuse it. What this does and does not change:
|
|
158
|
+
|
|
159
|
+
- A config whose inline handler writes a `readonly: true` field via `ctx.api.object(...).update()` / `.updateById()` / `.insert()` — and does not declare `runAs: 'system'` — now fails `objectstack lint` with `hook-api-update-readonly-field` (exit 1). It already failed `objectstack build` with the same finding, so nothing that built green fails lint red.
|
|
160
|
+
- The warning-severity members of the family (`hook-body-write-unknown-field`, `hook-api-update-readonly-when-field`, …) now report on inline handlers too; they never fail a run without `--strict`.
|
|
161
|
+
- Nothing about what `objectstack build` accepts changes, and `objectstack validate` — which parses without lowering — is unchanged and still does not see handler-authored hooks; both are recorded in the rules' headers.
|
|
162
|
+
- The lint input is never mutated: rules that read the live function value (`hook-body/not-lowerable` and its siblings) keep seeing it, and a handler the extractor refuses has no body on any command, so no rule guesses about a body that was not produced.
|
|
163
|
+
|
|
164
|
+
Measured on this repository's own four example apps (`examples/app-crm`, `app-showcase`, `app-todo`, `app-multi-package`), before and after: **121 findings before, 121 after — row for row identical, and zero at `error` on both sides.** No config that passes today starts failing. Two of the six hooks in that corpus are `handler`-authored and were invisible to this family before; their bodies write nothing the family objects to, which is why the delta is zero rather than the family being unreached. The reach itself is pinned separately, with a body-authored control beside every leg.
|
|
165
|
+
|
|
166
|
+
`@objectstack/lint` carries only the header ledger recording which *intakes* reach each hook rule — the call sites of `runAuthoringRules`, which are more numerous than the three commands (the scaffold validator is a fourth, and it has always reached this family). Its behaviour is unchanged.
|
|
167
|
+
- 9f890d3: `filter-preset-comparand` gains a FIELD-TYPED arm (#16106, maintainer-ruled 1′): on a declared `date` / `datetime` field, a dashboard date-range preset name (`last_30_days`, `this_quarter`, …) is now refused in EVERY comparand position — bare (implicit equality), `$eq` / `$ne`, `$in` / `$nin`, and their view-rule (`equals` / `not_equals` / `in` / `not_in`) and triple (`=` / `!=` / `in` / `nin`) spellings — with the same located message and prescription the ordering positions already carry (`{ $gte: '{30_days_ago}' }` for `last_30_days`, and so on). The field type is read from the stack's own object graph: a dashboard widget or report through its `dataset` to that dataset's `object`, a view through `data.object`, a flow CRUD node through `config.objectName`, a page component through `dataSource` / `properties`, an object's own list views and `relatedListFilter`, a summary field's child object. A position the graph cannot bind, a registry-injected column, a `time` field, or a select / text column stays unjudged — equality against a picklist value that collides with a preset name is a working filter. The field-agnostic schema door in `@objectstack/spec` keeps its ordering-only boundary unchanged; this closes the authoring-time gap where `objectstack lint` and the runtime publish gate accepted a filter the engine then refused with `INVALID_FILTER` / 400 on first render.
|
|
168
|
+
- 720bf47: `flow-update-readonly-field` and `hook-api-update-readonly-field` now report a non-system **create** of a static-`readonly` field — a new **error**-severity finding that fails `os lint` / `os validate` / `os build` on a shape they used to accept.
|
|
169
|
+
|
|
170
|
+
Both rules scanned only the update verb (`update_record`; `ctx.api…update()` / `.updateById()`) and justified the omission with the same sentence: INSERT is engine-exempt from the author-declared `readonly` strip, so a create that seeds a `readonly` column is not a no-op. The maintainer ruling of 2026-09-03 (option C, #14147) made that false — `engine.insert` now runs the same `isSystem`-gated `stripReadonlyFields` the update path runs — so a flow `create_record` without `runAs: 'system'`, or a hook body's `ctx.api.object('…').insert()` under a non-system trigger, that writes a `readonly` field became a **silent no-op**: the row lands without the column (which falls back to its `defaultValue`), the step reports `success`, and only a run-time warning names the dropped field (measured end to end in `@objectstack/service-automation`'s `create-record-readonly-drop.test.ts`). Nothing reported it at build time. This closes that scan gap (#15394).
|
|
171
|
+
|
|
172
|
+
**What now fails that passed before.** Exactly one new shape per rule, at `error`:
|
|
173
|
+
|
|
174
|
+
- a flow `create_record` node whose literal `fields` map writes a field the target object declares `readonly: true`, on a flow that does not declare `runAs: 'system'`;
|
|
175
|
+
- an L2 hook body's literal `ctx.api.object('<name>').insert({ … })` writing such a field, on a hook that does not declare `runAs: 'system'`.
|
|
176
|
+
|
|
177
|
+
The rule ids and severities are the update ones — one id per shape, not per verb — and each finding's message names the verb it was judged on and what actually happens to a create. Everything the rules already skipped is still skipped: a templated object name, a non-literal payload, an object outside the stack or declaring no fields, an unknown field (the unknown-field rules' question), and any `runAs: 'system'` flow or hook, because seeding a `readonly` column at create time is a system act and that write lands.
|
|
178
|
+
|
|
179
|
+
**Deliberately not reported.**
|
|
180
|
+
|
|
181
|
+
- No `readonlyWhen` (conditional) finding on a create, on either surface: a conditional lock is evaluated against the record being written over, which a create does not have, and the engine runs no conditional strip on INSERT ("INSERT stays exempt"). A warning there would state something false about a write that lands.
|
|
182
|
+
- The hook rule judges `.insert()` only, not `.create()`. The host `ObjectRepository` aliases `create()` to `insert()`, but L2 bodies run in QuickJS and the VM-side `ctx.api.object()` installs no `create` leaf — a body calling `.create()` throws `TypeError: not a function` on its first run, a loud failure rather than the silent drop this rule reports. The silence is recorded as a reasoned method exclusion (`READONLY_HOOK_METHOD_EXCLUSIONS`) and pinned.
|
|
183
|
+
- No create finding on a **platform object** — one declaring `managedBy`, or in the reserved `sys_` namespace. The engine's create-side strip does not judge those at all (`staticReadonlyInsertSubject`: their own ADR-0086 write guard governs them), so a finding there would describe a strip that never runs. The update verb keeps judging them, exactly as the engine's update path does.
|
|
184
|
+
- `validate-readonly-action-writes` is unchanged: an action body runs system-elevated by design, so its create genuinely lands.
|
|
185
|
+
|
|
186
|
+
**Migration.** If your build reds on the new finding, the fix is one of: declare `runAs: 'system'` on the flow or hook when seeding the `readonly` column is the intent (the intended channel — `readonly` governs the end-user/API surface, not trusted system writers); remove the key from the `create_record` `fields` / `insert()` payload when it is not; or stamp it in a `beforeInsert` hook on the target object (`ctx.input.<field> = …`), which is a server value the strip does not touch. Measured over this repository's shipped examples (`app-crm`, `app-showcase`, `app-todo`): zero in-repo flows or hooks go red — the two `create_record` nodes that target an object carrying a `readonly` field write none of its `readonly` fields, and the one flow that creates unauthenticated already declares `runAs: 'system'`; no shipped hook body inserts through `ctx.api`.
|
|
187
|
+
- b4b37e5: The object publish door now refuses an object whose `searchableFields` entry, or whose built-in list view's `columns` (and every other field-naming position on that list view), names a field the object does not have.
|
|
188
|
+
|
|
189
|
+
`#15254` closed this one key over: it crossed the reference-integrity suite onto the object write door for the object's own field-name **lists** (`highlightFields`, `publicSharing.redactFields`). The two members that read the *other* field surfaces an object carries — its ADR-0061 search set and its built-in `listViews` — still declared `runtimeTypes: ['flow', 'view']`, so on the only door a Studio, REST `/meta` or MCP author has they never judged the snapshot that arrived. An object could publish clean with `searchableFields: ['gone_field']` or a list-view column resolving to nothing, and both fail the same silent way downstream: the engine filters a stale search entry out without a word (`resolveSearchFields`), so `$search` scans a narrower set than declared — or, once every entry is stale, the auto-default set the author never chose — and a dangling column renders one field short.
|
|
190
|
+
|
|
191
|
+
- **`validateSearchableFields` and `validateListViewFieldRefs` gain `object`** in their suite-member `runtimeTypes`. No new rule and no new finding class: the rule ids (`searchable-field-unknown`, `searchable-field-unsearchable`, `list-view-field-unknown`, `list-view-field-dotted`) and their severities are unchanged — they now reach the door where the author actually is.
|
|
192
|
+
- **The crossing carries the #9313 precondition.** Both members resolve only against `stack.objects`, the one collection every per-write snapshot carries, so neither opens a missing-collection false-positive channel; their `views[]` rungs simply find no `stack.views` on an object snapshot.
|
|
193
|
+
- **Measured before crossing**, at the door's own snapshot shape and differential, over every shipped object definition in the monorepo: 116 objects (platform-objects 48, showcase 24, plugins 19, services 12, crm 6, metadata-core 5, todo 1, qa 1), 105 built-in list views on 40 objects, 666 list-view field-naming positions and 5 `searchableFields` entries judged — **0 findings for both members, precision 1.0**, against synthetic probes that are refused.
|
|
194
|
+
- **`validateSortableFields`, the third sibling, is deliberately not crossed** — it measured equally clean, but that crossing is its own adjudication.
|
|
195
|
+
|
|
196
|
+
## Migration
|
|
197
|
+
|
|
198
|
+
**A publish that used to succeed can now be refused (HTTP 422, `INVALID_METADATA`).** The receipt names the rule id and the offending path, name-keyed on the wire — for example `objects.proj_task.searchableFields[1]` or `objects.proj_task.listViews.all.columns[1]` — plus the string that was written and the fields the object actually has.
|
|
199
|
+
|
|
200
|
+
To fix a refusal, do one of:
|
|
201
|
+
|
|
202
|
+
- rewrite the entry to the field's current API name (after a Studio label edit the derived name is the one to use — `field_10` becomes `health_score`); or
|
|
203
|
+
- drop the entry from the declaration; or, for `searchable-field-unsearchable`, target a text-like stored column instead of a virtual or non-scannable one.
|
|
204
|
+
|
|
205
|
+
`os validate` / `os build` / `os lint` already reported these findings at the same severity, so a code-authored stack can be repaired before it reaches a publish. Objects that name a platform-injected system column are unaffected — both members resolve those per object and stay silent where the platform really provisions them.
|
|
206
|
+
- e0af1a8: feat(spec)!: `<ListView objectName>` / `<ListView viewType>` are retired from the react-tier component contract — `data={{ provider: 'object', object }}` / `type` are the only spellings (#14791)
|
|
207
|
+
|
|
208
|
+
<!-- adr-0087: registered ui-react-list-view-binding-aliases-retired -->
|
|
209
|
+
|
|
210
|
+
**BREAKING** — an accept-set narrowing on a published contract. The `REACT_BLOCKS`
|
|
211
|
+
ListView entry no longer publishes the `objectName` and `viewType` overlay props that
|
|
212
|
+
#11284 had deprecated in favour of ListViewSchema's own `data` / `type`: the generated
|
|
213
|
+
contract (`skills/objectstack-ui/references/react-blocks.md`) drops both rows, and
|
|
214
|
+
`@objectstack/lint`'s `validate-react-page-props` now REFUSES either spelling on a
|
|
215
|
+
`kind:'react'` page with a new `react-prop-retired` error that carries the fix, where it
|
|
216
|
+
used to warn and accept. Shipped as `minor` under the repo's launch-window convention for
|
|
217
|
+
breaking changes; the hand-migration prescription is registered under protocol major 18
|
|
218
|
+
(`ui-react-list-view-binding-aliases-retired`). Maintainer ruling on #14791 (2026-09-07,
|
|
219
|
+
director seat summon #17, decision batch #1, option B — retire now, no deprecation window,
|
|
220
|
+
「同意」).
|
|
221
|
+
|
|
222
|
+
## FROM → TO
|
|
223
|
+
|
|
224
|
+
| you wrote | write instead |
|
|
225
|
+
|:--|:--|
|
|
226
|
+
| `<ListView objectName="account" … />` | `<ListView data={{ provider: 'object', object: 'account' }} … />` |
|
|
227
|
+
| `<ListView viewType="kanban" … />` | `<ListView type="kanban" … />` |
|
|
228
|
+
| `<ListView … />` with no binding at all | add `data={{ provider: 'object', object: '…' }}` — it is the required binding on a react page |
|
|
229
|
+
|
|
230
|
+
One-line fix: on every `<ListView>` in react page source replace `objectName="X"` with
|
|
231
|
+
`data={{ provider: 'object', object: 'X' }}` and `viewType="K"` with `type="K"`, then re-run
|
|
232
|
+
`objectstack validate` — a leftover alias is reported as `react-prop-retired` with this
|
|
233
|
+
same prescription, and a list with no data source as `react-prop-missing-required`.
|
|
234
|
+
|
|
235
|
+
## Why now, and why no window
|
|
236
|
+
|
|
237
|
+
The contract deprecated both aliases (#11284) while objectui's ListView still read only
|
|
238
|
+
`objectName`, so a page written the canonical way validated green and rendered an empty
|
|
239
|
+
list. That consumer half has landed and ships in the console this repo pins
|
|
240
|
+
(`normalizeListViewSchema` at `a472b071` folds `data.provider === 'object'` onto the key
|
|
241
|
+
the renderer reads and takes the author's `type` for the view kind), so both spellings
|
|
242
|
+
render today — and the maintainer's standing rule for a spelling with zero external
|
|
243
|
+
authors is to retire it at once rather than keep two vocabularies alive.
|
|
244
|
+
|
|
245
|
+
## What else moved
|
|
246
|
+
|
|
247
|
+
- `REACT_RETIRED_OVERLAY_PROPS` is a new export of `@objectstack/spec/ui`: the tombstone
|
|
248
|
+
ledger (prop → replacement + one-line fix) the lint quotes, the react-tier twin of a
|
|
249
|
+
metadata schema's `retiredKey()`.
|
|
250
|
+
- `data` is restated on the ListView overlay as its **required** binding (ledgered in
|
|
251
|
+
`REACT_OVERLAY_SHADOWS`), so the generated contract marks it ✓ and a `<ListView>` with no
|
|
252
|
+
data source is refused — the check the required `objectName` used to carry.
|
|
253
|
+
- `REACT_RECORD_BLOCK_ALTERNATIVES['record:related_list']` prescribes the canonical spelling.
|
|
254
|
+
- The showcase pages (`crm-workbench`, `renewals-pipeline`, `task-desk`), the published
|
|
255
|
+
`objectstack-ui` skill and the react-pages / validating-metadata guides write the
|
|
256
|
+
canonical spelling; `@objectstack/lint` exports `REACT_PROP_RETIRED`.
|
|
257
|
+
- 9408b7f: A flow condition that is neither CEL text nor an expression is now refused at build time, instead of being read as an empty condition and answering a silent `false`.
|
|
258
|
+
|
|
259
|
+
`evaluateCondition` derives its source as `typeof expression === 'string' ? expression : (expression?.source ?? '')`. For a value that is neither — a number, a boolean, an array — the read yields `undefined`, the `??` supplies `''`, and the empty-source arm returns **`false`**: the "an unauthored branch must not open" rule, applied to a value that was very much authored. Measured: a `decision` node carrying `config: { condition: 42 }` **registered clean** and executed `success: true` with nothing said at any layer; `{ source: 1 }` did not even get that far and threw a bare `TypeError: exprStr.trim is not a function` out of the validator. `config.condition` is also the key a **start node's trigger gate** is read from, so the same value could gate a whole flow shut forever with no signal to the author.
|
|
260
|
+
|
|
261
|
+
- The new `structuralConditionRefusal` / `STRUCTURAL_CONDITION_SHAPE_REFUSAL` in `@objectstack/spec/automation` are the single shared notion of why, read by both validators so build time and author time cannot disagree about the shape. `registerFlow` throws, naming the node or edge and attributing the finding; `objectstack validate` reports the same refusal as a located `error`.
|
|
262
|
+
|
|
263
|
+
**This is deliberately NOT the `predicate`-slot rule, and the difference is measured.** A ledger `predicate` slot (`decision.conditions[].expression`, a screen field's `visibleWhen`) is declared `z.string()`, so `PREDICATE_SLOT_STRING_REFUSAL` refuses every non-string including an envelope. Neither structural slot is declared that way: `FlowEdgeSchema.condition` is `ExpressionInputSchema`, whose string arm **transforms into** `{ dialect: 'cel', source }` — so after `FlowSchema.parse` every authored edge condition *is* an envelope — and `FlowNodeSchema.config` is an open `z.record` that passes an envelope written at `config.condition` through verbatim, where `evaluateCondition` evaluates it correctly. Both shapes stay accepted here; an envelope with no `dialect`, and an `ast`-carrying one (`ExpressionSchema`'s own `source`-or-`ast` rule), stay accepted too.
|
|
264
|
+
|
|
265
|
+
**Strings are untouched, deliberately.** A whitespace-only condition still means "not authored" and still answers `false` on both sides — consistent behaviour, ruled correct, not a defect. What a non-empty string *says* is still `validateExpression('predicate', …)`'s verdict, brace trap and all. Only the shape moved.
|
|
266
|
+
|
|
267
|
+
An app that authored a number, a boolean, an array or a source-less object in a node or edge `condition` now fails to register with a message naming the site; the fix is to write the condition as bare CEL text (`record.rating >= 4`) or as an expression envelope.
|
|
268
|
+
- 615fac3: A publish now refuses an object whose `highlightFields` names a field that does not exist on it — the same gate that refuses a code-authored stack.
|
|
269
|
+
|
|
270
|
+
`list-view-field-unknown` inspects `view.columns`, and Studio's app builder mints no `view` items at all, so the reference-integrity family had nothing to inspect on the only artifacts the click path authors. What it authors is the **object**, and an object-level field-name list was covered by nothing that could refuse: measured on `origin/main`, `runtimeAuthoringRulesFor('object')` dispatched seven rules with no reference-integrity rule among them, while the object-level existence check that did exist (`semantic-role-field-unknown`) is `warning`, advisory-tier and CLI-only. So `os validate` exited 0 on a dangling reference and the runtime publish door — the only door a Studio, REST `/meta` or MCP author has — said nothing at all.
|
|
271
|
+
|
|
272
|
+
The reproduction is the natural click order, not a contrived one: click-create a field (Studio mints it as `field_10`), add it to `highlightFields`, then give it a label — the API name auto-derives to `health_score` and `highlightFields` keeps `field_10`. Anyone who names a field after placing it produces this.
|
|
273
|
+
|
|
274
|
+
- **New rule `object-field-ref-unknown` (`error`)**, in `@objectstack/lint`, over the object-level field-name **lists** that no rule owned: `highlightFields` (ADR-0085) and `publicSharing.redactFields`. It resolves through the same `object-graph` seam as the rest of the family, so the three shared skips hold — an object outside the stack, an object with no readable field map (ADR-0015 `external`), and a registry-injected system column resolved **per object** (`highlightFields: ['owner_id']` is a live pointer on an owned object and a real miss under `ownership: 'none'`).
|
|
275
|
+
- **It runs on the runtime publish door.** The reference-integrity suite entry's `runtimeTypes` gains `object`, and the suite's per-member declaration keeps the crossing narrow: this is the only member that judges an object snapshot; every other member keeps `['flow', 'view']` or the frozen `['flow']` default.
|
|
276
|
+
- **`validateSemanticRoles` keeps the provenance question** at the same position (`semantic-role-field-unprovisioned`, still `warning`) and no longer restates existence — one finding per path, at one tier.
|
|
277
|
+
- **`probes.checked` gained an `objects` counter.** Its absence was the tell: a receipt reading `{seeds: 0, views: 0, widgets: 0}` was accurate while the objects the package published were probed by nothing.
|
|
278
|
+
|
|
279
|
+
## Migration
|
|
280
|
+
|
|
281
|
+
**A publish that used to succeed can now be refused (HTTP 422, `INVALID_METADATA`).** The receipt names the rule id `object-field-ref-unknown` and the offending path, name-keyed on the wire — for example `objects.proj_task.highlightFields[1]` — plus the string that was written and the fields the object actually has.
|
|
282
|
+
|
|
283
|
+
To fix a dangling reference, do one of:
|
|
284
|
+
|
|
285
|
+
- rewrite the entry to the field's current API name (after a Studio label edit the derived name is the one to use — `field_10` becomes `health_score`); or
|
|
286
|
+
- drop the entry from the list.
|
|
287
|
+
|
|
288
|
+
`os validate` / `os build` / `os lint` report the same finding at `error`, so a stack can be repaired before it reaches a publish. If an object legitimately points at a platform-injected system column, no change is needed — the rule resolves those per object and stays silent where the platform really provisions them.
|
|
289
|
+
- 6b7d709: `objectstack validate` now lowers hooks authored as inline `handler` functions to a metadata body before it parses, so the hook write-set rules judge them there exactly as `objectstack build` and `objectstack lint` already do.
|
|
290
|
+
|
|
291
|
+
The `hook-body-write-unknown-field`, `hook-body-write-unprovisioned-anchor`, `hook-body-source-unparseable`, `hook-api-update-readonly-field` and `hook-api-update-readonly-when-field` rules open on `body.language === 'js'`. A hook written as `handler: async (ctx) => { … }` carries no `body`, and `objectstack validate` parsed the normalized stack without lowering — so on that command the whole family returned before reading anything, and a stack `objectstack build` refuses with `hook-api-update-readonly-field` (exit 1) passed `objectstack validate` with exit 0 and no finding. The same statement authored as an explicit `body: { language: 'js', source }` was refused by `objectstack validate` all along, so the silence was the command's intake, not the rule.
|
|
292
|
+
|
|
293
|
+
`objectstack validate` now runs the same `lowerCallables` pass `objectstack build` runs before its parse — after its two pre-parse undeclared-key lints, which keep reading the un-lowered stack, and before the schema parse, which reads the lowered view — and hands the rule registry the parsed result as before. This moves what `objectstack validate` accepts in **both** directions, and both are parity with `objectstack build`:
|
|
294
|
+
|
|
295
|
+
- **Narrowing (hooks).** A config whose inline handler writes a `readonly: true` field via `ctx.api.object(...).update()` / `.updateById()` / `.insert()` — and does not declare `runAs: 'system'` — now fails `objectstack validate` with `hook-api-update-readonly-field` (exit 1). It already failed `objectstack build` and (since #16095) `objectstack lint` with the same finding, so nothing that builds green starts failing `objectstack validate`.
|
|
296
|
+
- **Widening (actions, and a nameless `functions` array entry).** A plain-object config carrying an inline action `target` callable — `actions: [{ name, label, target: async (ctx) => { … } }]`, or the same on `objects[*].actions[*]` — was **refused** by `objectstack validate` before this change: `ActionSchema.target` is a string, and nothing lowered the function before the parse, so the run exited 1 with `invalid_type` at `actions.0.target` (measured through the real CLI: `valid=false errors=2 invalid_type@objects.0.actions.0.target | invalid_type@actions.0.target`). The same pass now lowers it to a ref string plus `body` on this command too, so `objectstack validate` **accepts** it (exit 0, `valid: true`) — exactly as `objectstack build` accepted it all along (exit 0 on both sides). Likewise a nameless `functions` **array** entry, `functions: [{ handler: async (ctx) => { … } }]`, which the same pass names `anon_fn`: the array form requires `name`, so `objectstack validate` refused it at the parse (measured: `valid=false errors=1 invalid_union@functions`, exit 1) and now accepts it (exit 0, `valid: true`), as `objectstack build` did (exit 0 on both sides). The `functions` map forms and `hooks[*].handler` parse either way and are not affected. These are accepted-set relaxations on a published command; they are declared here rather than inferred from the build's behaviour, and pinned beside the hook legs.
|
|
297
|
+
- The warning-severity members of the family now report on inline handlers under `objectstack validate` too; they fail a run only with `--strict`, as every other advisory does.
|
|
298
|
+
- The `--json` payload gains no key and the text face prints no new step: the lowering is a view for the parse and the rule registry. A handler the extractor cannot lower (a forbidden token, a module-scope identifier) has no body on any command and is reported by `objectstack lint`'s `hook-body/*` rules and `objectstack build`'s warn-and-bundle line, never guessed at here.
|
|
299
|
+
- Nothing about what `objectstack build` accepts changes; on both axes above `objectstack validate` now agrees with it.
|
|
300
|
+
|
|
301
|
+
Measured on this repository's ten `objectstack.config.ts` corpus files at `6ba0db4e0` with `objectstack validate --json`, before and after: **exit code, error text and rule-id list identical on 10 of 10 — zero findings change, zero verdicts change.** Six reach the rule registry (the four example apps and the `plugin-auth` / `plugin-security` / `service-i18n` configs); two (`driver-memory`, `plugin-hono-server`) are plugin manifests, not stacks, and are refused at the schema parse — after the lowering point — with the same top-level `unrecognized_keys` on both sides; two (`app-showcase`, the `blank` template) fail at load in the measuring environment, before the lowering point, on both sides. None of the repository's handler-authored hooks writes through `ctx.api`, and none of the ten carries an inline action `target` callable, which is why the delta is zero on both axes rather than either being unreached — a corpus with neither shape cannot see either limb, so both are pinned on their own fixtures; the reach itself is pinned by the card's own fixture, with the body-authored control beside it and a handler-authored hook the family has nothing to say about still passing.
|
|
302
|
+
|
|
303
|
+
`@objectstack/lint` carries only the header ledger recording which intakes reach each hook rule; `objectstack validate` moves from "not reached" to "reached". Its behaviour is unchanged.
|
|
304
|
+
- cd55558: `widget-measures-missing` — the empty-measure selection is reported on every widget family, not just charts
|
|
305
|
+
|
|
306
|
+
`chart-measures-missing` (#15462) reported the authoring placeholder only for the chart
|
|
307
|
+
family, but the return that produces it is type-independent. At the `@object-ui` revision
|
|
308
|
+
this repo pins (`.objectui-sha` = `a472b0716`), `packages/plugin-dashboard/src/DatasetWidget.tsx:683`
|
|
309
|
+
reads `if (values.length === 0)` and returns *"Pick measures (values) for this dataset
|
|
310
|
+
widget."* ABOVE `isMetric` (`:423`, over `METRIC_TYPES` at `:343`), `isTable` (`:424`) and
|
|
311
|
+
the chart branch alike. So a `metric`, `kpi`, `gauge`, `solid-gauge`, `bullet`, `table` or
|
|
312
|
+
`pivot` widget that selects no measures renders the same placeholder — the KPI number or
|
|
313
|
+
the table the author declared is not drawn at all — and nothing reported it:
|
|
314
|
+
`table-count-only` requires `values.length > 0` before it looks, and the rules that iterate
|
|
315
|
+
`dimensions[]`/`values[]` are silent on an empty array by construction.
|
|
316
|
+
|
|
317
|
+
- **New id `widget-measures-missing`** — a NON-chart declared widget type selects no
|
|
318
|
+
measures. Warning tier, suppressible per widget with
|
|
319
|
+
`suppressWarnings: ['widget-measures-missing']`, exactly as the chart-family id is. The
|
|
320
|
+
message states the consequence its family actually has (the single KPI number is not
|
|
321
|
+
drawn / no table is rendered) and the hint names the dataset's declared measures.
|
|
322
|
+
- **`chart-measures-missing` is unchanged** — same id, same chart-family population, same
|
|
323
|
+
message and same suppression. The condition split rather than widened because "chart"
|
|
324
|
+
stops naming it once the population is every family, while the old id is reachable from
|
|
325
|
+
the package barrel (a public-surface contract) and may already be written into a board's
|
|
326
|
+
`suppressWarnings`.
|
|
327
|
+
- `chart-dimensions-missing` stays chart-family only: a dimensionless `metric` or `table`
|
|
328
|
+
is what those families are for.
|
|
329
|
+
|
|
330
|
+
The two never double-report one widget, in the pin's own order: the measures check runs
|
|
331
|
+
before the dimensions one, and `table-count-only` already skips an empty selection.
|
|
332
|
+
|
|
333
|
+
### Patch Changes
|
|
334
|
+
|
|
335
|
+
- 347b777: `chart-config-missing` no longer fires on a widget whose binding the renderer derives
|
|
336
|
+
|
|
337
|
+
The rule warned on every chart-family widget that declared no `chartConfig`, on the
|
|
338
|
+
stated grounds that "the renderer cannot determine which measure to plot, so the series
|
|
339
|
+
renders empty". Measured against the `@object-ui` revision this repo pins
|
|
340
|
+
(`.objectui-sha`), that consequence is false: `DatasetWidget` derives the x-axis key and
|
|
341
|
+
one series per measure from the widget's own `dimensions` / `values` via
|
|
342
|
+
`buildChartSeries`, and refuses an authored `ChartAxis.field` / `ChartSeries.name`
|
|
343
|
+
outright — `chartConfig` carries presentation only. The renderer pins this by name:
|
|
344
|
+
"ignores an authored axis `field` and keeps the derived axis binding", "ignores an
|
|
345
|
+
authored series and keeps one derived series per measure", "emits none of the
|
|
346
|
+
presentation keys when no chartConfig is declared".
|
|
347
|
+
|
|
348
|
+
The false finding was landing on this platform's own shipped metadata — the
|
|
349
|
+
`system_overview` dashboard's pie and bar tiles, on the Setup board every customer opens
|
|
350
|
+
first — which is the ADR-0072 D1 cost the rule family exists to avoid.
|
|
351
|
+
|
|
352
|
+
The rule id is unchanged and keeps one true arm: a `combo` widget with no `chartConfig`,
|
|
353
|
+
whose per-series mark is authored as `chartConfig.series[].type` and has no other
|
|
354
|
+
channel, so every measure draws with the same default mark and the chart is not a
|
|
355
|
+
combination at all. Its message now names that consequence instead of the binding.
|
|
356
|
+
An existing `suppressWarnings: ['chart-config-missing']` entry stays valid.
|
|
357
|
+
- a51eb86: `chart-measure-unknown` no longer blocks a build over a chart `series[].name` (or a page chart's `yAxis[].field`) that names nothing — those positions are presentation, and the message now says so.
|
|
358
|
+
|
|
359
|
+
The rule fired at `error` on every measure position of the three chart surfaces it covers, with one consequence sentence: *"result rows are keyed by MEASURE NAME … so this series comes back empty"*. Read at the `@object-ui` revision this repo pins (`.objectui-sha`), that is true only where the position feeds the dataset query, and the three surfaces do not agree:
|
|
360
|
+
|
|
361
|
+
- **Report charts** run the chart's own query out of the two axis strings (`useDatasetRows(dataset, [xAxis], [yAxis], …)` — *"the embedded chart queries only `chart.xAxis` × `chart.yAxis`"*), so `chart.xAxis`/`chart.yAxis` are the binding. `chart.series[]` is *"the author's per-chart override for ONE measure's display name"*, lowered through `mergeAuthoredSeries`, where *"an authored entry naming a measure that is NOT in the dataset selection is **ignored** — membership belongs to the dataset"*.
|
|
362
|
+
- **List-view charts** have no presentation position at all: `ListChartConfigSchema` is a strict object of `chartType`/`dataset`/`dimensions`/`values`, and `values[]` is handed to the chart as the dataset measures.
|
|
363
|
+
- **Dataset-bound page chart components** query `{ dimensions, measures: values }` and then replace the authored series wholesale with one derived entry per selected measure, so `properties.series[].name` reaches the renderer not at all and `properties.yAxis[].field` re-points nothing.
|
|
364
|
+
|
|
365
|
+
**Behaviour change users see:** the three presentation positions — report `chart.series[].name`, page-component `properties.series[].name` and `properties.yAxis[].field` — drop from `error` to `warning`. A build or a metadata publish that used to be refused because of one of them now succeeds, with the finding on the advisory channel. The finding is KEPT, not deleted: the metadata really is wrong — the author wrote a key and believes it is in force. Every query position (report `chart.yAxis`, and `values[]` on all three surfaces) keeps `error` and its existing message verbatim.
|
|
366
|
+
|
|
367
|
+
Two smaller corrections ride along, both from the same read:
|
|
368
|
+
|
|
369
|
+
- The page surface's `yAxis[].field` refs are no longer concatenated into the `series[]` limb before the measure walk, so an axis position no longer takes the series message. Reading both shapes on that surface stays deliberate; giving them one sentence was not.
|
|
370
|
+
- `chart-axis-not-selected` (a declared measure outside the selection) took the same one-size consequence, *"the query does not return it, so the series plots nothing"*. It keeps that wording at a query position and states the real one at a presentation position, where no series is derived for the name in the first place.
|
|
371
|
+
|
|
372
|
+
Note that none of these three surfaces declares `suppressWarnings` — it is a dashboard-widget key — so the new advisories cannot be individually silenced; the hint says so instead of pointing at a key that does not exist.
|
|
373
|
+
- 693fbcb: `dashboard-action-route-unresolved` now resolves the `apps/NAME` head of a dashboard header action's `url` target against `stack.apps`, and reports every unresolved `<collection>/<name>` segment in the path rather than stopping at the first one it recognizes.
|
|
374
|
+
|
|
375
|
+
Before this, `URL_COLLECTION_TO_STACK_KEY` had no `apps` entry, so an `actionUrl` like `/apps/no_such_app_nope/crm_lead` was never checked at all — a dashboard button pointing at an app that does not exist passed lint clean. Worse, once a bad app name was combined with a second bad segment later in the same path (e.g. `/apps/no_such_app_nope/dashboard/no_such_dashboard_nope`), the old loop returned at the FIRST recognized segment and reported only that one — so a bad app name plus a bad dashboard name reported only the dashboard, never the app.
|
|
376
|
+
|
|
377
|
+
**Behavior change on paths that used to pass clean:** the loop no longer stops scanning a path the moment it recognizes one collection segment, resolved or not. A path like `/dashboards/exec/views/bad_view` — where `exec` is a real dashboard but `bad_view` names no view — used to report nothing (the loop returned as soon as `dashboards/exec` resolved, never reaching `views/bad_view`); it now reports one warning on the `views/bad_view` segment. Any stack with a dashboard `url` action whose path recognizes a valid collection segment followed later by an unresolved one will see a NEW warning here that did not fire before. This is intentional — it is the same false-affordance category the rule already exists to catch — but it is a real, visible change to what a clean `lint` run reports on such stacks, not a pure addition.
|
|
378
|
+
- 86c75f4: `firstUndeclaredReference` now documents the side of its contract it was silent about: it can false-NEGATIVE, and a `null` is "nothing was reported", not "every reference is rooted".
|
|
379
|
+
|
|
380
|
+
The existing sentence — "Acts ONLY on cel-js's `Unknown variable: X` fault, so it cannot false-positive on arithmetic/comparison overloads" — is true, and stays. What it never said is what that narrowing costs. cel-js's checker returns exactly ONE error, so when the first one is of another class every undeclared reference behind it in the same source goes unjudged and the helper answers `null` — the same value that means the source is clean. A contract that declares only which error it cannot make reads as making neither.
|
|
381
|
+
|
|
382
|
+
No behaviour changes. This is the contract text, and it ships: the amended block is JSDoc on a published export, so it is emitted into `@objectstack/formula`'s `dist/index.d.ts` and `dist/index.d.mts` (measured — the declaration file grew 53.45 KB to 55.99 KB) and is what a consumer reads on hover.
|
|
383
|
+
|
|
384
|
+
What the amendment adds, all of it measured rather than reasoned:
|
|
385
|
+
|
|
386
|
+
- **The masking is positional, not name-keyed.** The masked name is not the one that triggered the first error, so excluding the trigger's own name does not reach it. `data == 'x' && status == 'q'` answers `null`; the same two names in the other order answer `"status"`.
|
|
387
|
+
- **`celEngine.compile()` is not a gate against it.** `compile` type-checks in the permissive environment, where every unlisted name is `dyn`. The strict environment here declares `SCOPE_ROOTS` as `map`, so a root — or an object field sharing one of those names (`data`, `config`, `result`, `item`, `event`, `input`, `user`, …) — used as the operand of an operator with no `map` overload faults HERE and nowhere else. A caller that only reaches the helper on a clean compile is therefore not protected by its own gate.
|
|
388
|
+
- **The CEL type-name class is the same shape.** `type == 'grid'` is already pinned as a blind spot in `@objectstack/lint`'s `visibility-bare-identifier` suite, but pinned per NAME; the masking it causes is source-wide.
|
|
389
|
+
- **What closing it would take, and why that is not this change.** Widening the regex onto the overload message is the false positive the narrowing buys off (`type(record.x) == string` is legitimate CEL). Reporting past the first error needs a re-check loop over a neutralised source, or a checker entry returning more than one error — cel-js 8.0.0 has neither; its `TypeCheckResult` carries a single `error`. Both change what every consuming rule reports, so the oracle's shape is a design decision.
|
|
390
|
+
|
|
391
|
+
`@objectstack/lint` carries a second comment-only correction, to `flow-variable-scope`'s account of the same oracle. Its "known, deliberate blind spot" note bounded the under-report to a flow variable named after a `SCOPE_ROOTS` member; measured, the bound does not hold — such a name in an operand position terminates the discovery loop on iteration 0 and every shadow in that source is lost, whatever it is named. That block sits on an internal function, so unlike the `formula` half it reaches no published declaration file; the entry is here because the package is touched and published.
|
|
392
|
+
- b371960: `flow-decision-unconditional-branch` now reports the decision that gates on nothing — the shape the rule used to skip.
|
|
393
|
+
|
|
394
|
+
A `decision` whose out-edges carry no `condition` and no `isDefault`, and whose node declares no `config.conditions[]`, selects no branch at all: the automation engine's own decision executor reports no branch when `conditions[]` is empty, so traversal considers every out-edge and each successor runs on every pass. The gateway is decoration. The rule could not see that shape, because it was framed as "an unconditional edge undercuts a guarded one" and read zero guarded edges as nothing to undercut — so the strictly worse gateway was the one case that stayed silent, and it is the harder one to notice in review, because the node still says `type: 'decision'`.
|
|
395
|
+
|
|
396
|
+
Same rule id, same `warning` tier, with its own message: it names the out-edges that run unconditionally and offers the three fixes (a `condition` per branch plus `isDefault: true` on the fallback, a `config.conditions[]` whose `label` matches an out-edge, or dropping `type: 'decision'` for the node the gateway already behaves as). The mixed shape — one guarded out-edge beside an unconditional one — keeps its existing wording and its single finding.
|
|
397
|
+
|
|
398
|
+
Decisions that do declare their routing stay silent, including the two that are easiest to catch by mistake: an ordinary gateway with guarded edges, and a decision that routes by `config.conditions[]` labels alone with bare out-edges. A decision declaring a label no out-edge claims remains the gating `flow-branch-label-unmatched` on its own, with no second finding piled on the same node.
|
|
399
|
+
- d91dff4: `validateStackExpressions` no longer throws on a non-record entry of a flow's `nodes` list.
|
|
400
|
+
|
|
401
|
+
An empty item in a YAML `nodes:` list deserialises to `null`, so this is an authorable shape — the same one #15552, #15636 and #15742 closed for stack collections and for `objects[].fields`. Here it crashed the linter instead of producing a finding: `flow.nodes: [null, ...]` threw `Cannot read properties of null (reading 'type')`, which presents to an author as a broken tool rather than as a problem with their metadata.
|
|
402
|
+
|
|
403
|
+
Both of the file's inline casts now read through `recordsOf`, the one home of this coercion, instead of asserting that `Array.isArray` proves anything about a list's MEMBERS:
|
|
404
|
+
|
|
405
|
+
- The flow walk reads `flow.nodes` through `recordsOf`, and — the half that actually removes the crash — hands that coerced array to `collectFlowGraphs` rather than the raw flow. `collectFlowGraphs` declares its input as already-parsed `FlowNodeParsed[]` and is transparent about members, so passing raw authored metadata was calling it out of contract; coercing only the local variable relocated the throw into `@objectstack/spec` instead of ending it. The producer's contract is unchanged, deliberately: widening it to tolerate malformed members is the wrong direction.
|
|
406
|
+
- The per-graph walk reads `graph.nodes` through `recordsOf` in place of an `as unknown as` double cast. A nested region's node list is only `Array.isArray`-checked before it becomes a graph, so that list carries the producer's word about its members and not a check.
|
|
407
|
+
|
|
408
|
+
A non-record member is dropped whole and in silence, exactly as the file's sibling field readers already did; a flow standing beside the junk entry is still judged, and a `nodes` list holding a plain string still reports exactly what it reported before.
|
|
409
|
+
- dff0bdd: `flow-template-unknown-field` and `flow-template-lookup-traversal` now reach a `{record.<field>}` template that sits outside a node filter — the `warning` half both rules already declared, and never emitted.
|
|
410
|
+
|
|
411
|
+
A `{record.<field>}` token in a filter has always been reported as an `error`: an unresolved token there erases the condition and the CRUD node refuses to run. A token anywhere else — a message body, an http request payload, a created row's field values — is the quiet failure the rules were written for: it renders as an empty string on every run, and nothing reports it at either end — no build-time finding, no run-time error — so a hand-off payload naming a renamed field ships an empty value and the run is recorded as a success. That half was silent.
|
|
412
|
+
|
|
413
|
+
The cause was one key, in the shared flow walk rather than in either rule. A rule that scans a node's config recursively has to read a view of it with the nested regions removed, or it reports every finding inside a `loop` / `try_catch` / `parallel` a second time against the container. That view was built by removing every key that holds a region on *any* node type — and `body` is `loop`'s region slot **and** the canonical request-payload key on an `http` node. So `config.body` was deleted from every node's view before any rule read it, and the whole of an http payload was invisible. The view now removes only the slots the node's own type declares, which is exactly the set the walk descended into: nothing is double-reported, and nothing that was never a region is dropped.
|
|
414
|
+
|
|
415
|
+
Expect new `warning` findings on flows that publish clean today. Each one names a token that renders empty at run time; `warning` does not change `os validate`'s exit code, so a build that passed still passes.
|
|
416
|
+
- 7a01847: Fix: a name-keyed `pages:` map no longer passes every source-page lint vacuously.
|
|
417
|
+
|
|
418
|
+
`pages` has two authoring carriers — a list, or a map keyed by page name that
|
|
419
|
+
`normalizeStackInput` folds into a list before the schema sees it. Four rules
|
|
420
|
+
(`validate-jsx-pages`, `validate-page-source-styling`,
|
|
421
|
+
`validate-react-page-props`, `validate-react-pages`) read the collection through
|
|
422
|
+
a private coercion that answered a map with an empty list, and they run on the
|
|
423
|
+
raw `os lint` path where nothing has normalized it yet. On a map-shaped stack
|
|
424
|
+
all four therefore returned no findings by never walking a single page: an
|
|
425
|
+
empty source, a syntax error, an unparseable component and a Tailwind
|
|
426
|
+
`className` were all reported as clean. They now read `collectionEntries`,
|
|
427
|
+
which handles both carriers, and a finding on the map carrier is located by the
|
|
428
|
+
author's own key (`pages.home.source`) rather than a synthetic index.
|
|
429
|
+
|
|
430
|
+
The same change removes the last sixteen private copies of the collection
|
|
431
|
+
coercion in this package. Twelve rules — the `function` form, which had already
|
|
432
|
+
grown the non-record filter locally in two different spellings — now read
|
|
433
|
+
`recordsOf` from `object-graph.ts`. Two behaviour changes fall out, both on
|
|
434
|
+
input that was already malformed: an array-typed member of `agents:` /
|
|
435
|
+
`skills:` / `tools:` used to survive the looser local filter and draw one
|
|
436
|
+
reference-integrity finding at a position nobody authored, and is now dropped;
|
|
437
|
+
a member of a name-keyed `validations:` map whose value is not a record is now
|
|
438
|
+
carried as `{ name }` rather than discarded, which reaches no check that reads
|
|
439
|
+
it. No rule id, message or severity changes, and every finding path on the list
|
|
440
|
+
carrier is unchanged.
|
|
441
|
+
- f36eef5: Flow-node-list readers no longer throw on a non-record member — `lintFlowPatterns`, `collectFlowVariableNames` and the three record-change template-path readers now coerce through `recordsOf`.
|
|
442
|
+
|
|
443
|
+
`lintFlowPatterns` crashed on an ordinary flow. A YAML `nodes:` list item left empty deserialises to `null`, and the rule read `nodes.find(n => n.type === 'start')` off a list it had only `Array.isArray`-checked, so an author's own metadata turned `objectstack validate` into an uncaught `TypeError` out of a function contractually typed `(stack) => Finding[]`:
|
|
444
|
+
|
|
445
|
+
```
|
|
446
|
+
TypeError: Cannot read properties of null (reading 'type')
|
|
447
|
+
at lint-flow-patterns.ts:1430
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
`Array.isArray` proves the LIST, never its MEMBERS — the same sentence removed from `validate-expressions.ts` one file over. All seven readers now go through `recordsOf` (`object-graph.ts`), which stays the single home for this coercion; no new copy of the predicate is declared.
|
|
451
|
+
|
|
452
|
+
- **`lintFlowPatterns`** — `flow.nodes` is coerced once, and that coerced array is what is handed on to `collectFlowGraphs`. That second half is the load-bearing one: `collectFlowGraphs` is transparent about members (it forwards the caller's array and re-exposes the same objects), so coercing only for the local read would have moved the crash into `packages/spec` rather than removing it. Its two `graph.nodes` readers are coerced as well, because a nested region's node list reaches them with only an `Array.isArray` behind it.
|
|
453
|
+
- **`collectFlowVariableNames`** — the `graph.nodes` walk had no member guard while the `flow.variables` walk seven lines above it did. Reachable today only at a region nest of exactly `MAX_REGION_DEPTH`; it now cannot throw at any depth.
|
|
454
|
+
- **The three record-change template-path readers** (`boundObjectOf`, `declaredExpandOf` and the per-flow start lookup in `validateFlowTemplatePaths`) were **not** throwing. They survived on an optional chain in the `.find` predicate — one character's difference from the reader that did throw, maintained by nothing and looking redundant next to the `Array.isArray` above it. They are coerced for the same reason and the optional chain goes with it. This half is a hardening, not a bug fix.
|
|
455
|
+
|
|
456
|
+
A malformed member is dropped, in silence, exactly as `recordsOf` drops one everywhere else; the valid nodes standing beside it are still judged and the findings a flow draws are unchanged.
|
|
457
|
+
- 36a6082: `flow-double-brace-interpolation` and `flow-bare-dollar-reference` now read an `http` node's request payload. Both rules were blind to the whole of `config.body` on every node type — the one key where an uninterpolated token has an outbound consequence.
|
|
458
|
+
|
|
459
|
+
The recursive template scan in `lint-flow-patterns.ts` read a region-stripped view of each node's config, and it built that view from the FLAT UNION of every config key that holds a region on *any* node type (`body`, `try`, `catch`, `branches`) rather than from the slots the node in hand actually owns. `body` is `loop`'s region slot **and** the canonical request-payload key on an `http` node, so `config.body` was deleted from every node's view before the scan ever read it.
|
|
460
|
+
|
|
461
|
+
That made the two rules silent exactly where they matter most: `http-nodes.ts` interpolates the raw config wholesale, so a double-brace `{{ record.title }}` or a bare `$source.id` written in a payload is never interpolated and ships to the endpoint as literal text. Measured before this change, an `http` node whose `body` carried either token shape — at the top level or nested inside a `try_catch` region — produced zero findings from either rule.
|
|
462
|
+
|
|
463
|
+
- **The call site passes its own slots.** `stripRegions(node.config, ownRegionKeys(node.type))`. The remedy was already written in `stripRegions`' own docblock ("Pass the OWNING node's slots, not the flat union") and the sibling call site in `flow-walk.ts` already followed it; this one did not.
|
|
464
|
+
- **The trapping default is gone.** `stripRegions`' `regionKeys` parameter is now REQUIRED. The flat union survived as a default only to bound an earlier change, and the cost of leaving it was this defect: the shorter call compiled and quietly asked a different question. A caller that has not decided which set it means now fails to compile instead.
|
|
465
|
+
- **The double-count direction is unchanged and pinned.** A token inside a `loop` body is still reported exactly ONCE, against the node that carries it and not also against the container — the reason the strip exists, and the direction that breaks if a repair over-corrects to stripping nothing.
|
|
466
|
+
|
|
467
|
+
Both rules keep their existing severity. New findings appear only where a `{{ }}` or bare `$ref.field` sits in a previously-hidden key; measured across `examples/app-showcase`, `app-crm` and `app-todo` (34 flows, `http` payloads inside a `parallel` branch and a `try_catch` try among them), the count is unchanged at zero — those payloads use correct single-brace tokens.
|
|
468
|
+
- 7ad2ca0: `validateFormLayout` now resolves the bound object for a view container's default `form` (and its `formViews.*` entries that declare no binding of their own) when that container names its object only on the `list` block (`list.data.object`, `list.object` or `list.objectName`) and nowhere on the container itself.
|
|
469
|
+
|
|
470
|
+
Before this fix, `containerObject` had no way to see a list-only binding, so `objName` stayed `undefined` for every site under such a container — and `form-field-unknown` / `form-section-group-unknown` never fired there, however wrong the section content was. This is the same fallback rung `validate-translatable-sections.ts` already carries for its own sites; it is now shared by both. `absolute-colspan-discouraged` is unaffected by this change — it was never gated on the object binding (it needs only a field's `colSpan`), so it already fired on a list-bound container's form sections before this fix.
|
|
471
|
+
|
|
472
|
+
Consequence: a view whose object binding lives only on `list` and whose default `form` (or an unbound `formViews.*` entry) references a nonexistent field or an undeclared `section.group` now gets a `warning` finding it did not get before. A stack with no such dangling reference sees no new output.
|
|
473
|
+
- 7dafaae: No authoring rule throws on a non-record entry of any stack collection.
|
|
474
|
+
|
|
475
|
+
A collection is authored either as a list or as a name-keyed map, so every rule that reads one coerces `unknown` into an array of records first. That coercion had been hand-copied into 39 modules, and 23 of the copies spelled the array branch as an unchecked cast — every member was asserted to be a record. A YAML list item left empty deserialises to `null`, so a single stray `-` under `flows:`, `pages:`, `dashboards:`, `datasets:`, `apps:`, `permissions:`, `capabilities:`, `data:`, `hooks:`, `views:`, `actions:`, `translations:` (or a per-object `fields:` / `actions:` / `views:`) reached a property read on `null` and threw a stack trace out of `os lint` / `os validate` instead of reporting a finding. The rules are pure `(stack) => Finding[]` running on the raw path, so nothing upstream had judged the entry's shape.
|
|
476
|
+
|
|
477
|
+
Twenty-two of those readers now read through the shared, guarded `recordsOf`, which drops a non-record member of the array shape whole and keeps the author's key on the map shape. Nothing else about what the rules judge changes: a valid entry standing beside a junk one is still read, and still draws exactly the findings it drew before.
|
|
478
|
+
|
|
479
|
+
The remaining copies are pinned by a new source-text test in the package, so the predicate cannot be pasted back in: it asserts that `recordsOf` is the only collection coercion, that every module still holding a private one is named in a dated ledger that is exact in both directions, and that no coercion outside a dated single-file allowance casts its array branch unchecked.
|
|
480
|
+
- 52b59d6: fix(lint): every `stack.objects` reader skips a non-record entry, so no authoring rule throws on the publish door
|
|
481
|
+
|
|
482
|
+
A `null` member of `stack.objects` — what an empty YAML list item
|
|
483
|
+
deserialises to, and what a partial editor write leaves behind — crashed
|
|
484
|
+
13 of the 42 `AUTHORING_RULES` with
|
|
485
|
+
`TypeError: Cannot read properties of null (reading 'name')`. The
|
|
486
|
+
authoring rules are pure `(stack) => Finding[]` (ADR-0019) and run on the
|
|
487
|
+
RAW `lint` path as well as the parsed one, so nothing upstream had judged
|
|
488
|
+
the entry's shape. At the runtime publish gate they are called inside the
|
|
489
|
+
gate rather than behind a try/catch of their own, so the throw was an
|
|
490
|
+
exception on a WRITE path, not a skipped finding; on the CLI, `os lint` /
|
|
491
|
+
`os validate` / `os compile` died on the first one instead of reporting
|
|
492
|
+
the stack.
|
|
493
|
+
|
|
494
|
+
The repair before this one guarded ONE seam — the object-graph index every
|
|
495
|
+
field-path rule opens with. The crash stood at fourteen more readers of
|
|
496
|
+
the same collection, each a hand-copied `asArray` whose array branch was
|
|
497
|
+
an unchecked `v as AnyRec[]`. Copies are why: the defensive spelling was
|
|
498
|
+
already present in about a dozen siblings and absent in the rest, so
|
|
499
|
+
fixing one left the others answering the old way.
|
|
500
|
+
|
|
501
|
+
So the copies are gone. `recordsOf` — the guarded reader, exported from
|
|
502
|
+
`object-graph.ts` and package-private — is now the one coercion from a
|
|
503
|
+
collection authored as an array OR as a name-keyed map into the records it
|
|
504
|
+
holds, and fifteen files call it:
|
|
505
|
+
|
|
506
|
+
- `validate-expressions.ts`, `validate-list-view-mode.ts`,
|
|
507
|
+
`validate-widget-bindings.ts`, `filter-walk.ts`,
|
|
508
|
+
`validate-object-references.ts`, `validate-record-title.ts`,
|
|
509
|
+
`validate-form-layout.ts`, `lint-autonumber-formats.ts`,
|
|
510
|
+
`lint-view-refs.ts`, `validate-org-axis-red-lines.ts`,
|
|
511
|
+
`validate-sharing-rule-enforceability.ts` — the eleven sites that threw.
|
|
512
|
+
- `validate-searchable-fields.ts`'s `indexObjectSearchTargets` and
|
|
513
|
+
`validate-page-field-bindings.ts`'s `indexObjectFields` — two shared
|
|
514
|
+
indexers inside the reference-integrity suite, each in front of two
|
|
515
|
+
rules and both hidden behind whichever suite member threw first.
|
|
516
|
+
- `object-field-groups.ts`'s `indexObjectFieldGroups`, which the
|
|
517
|
+
re-measure surfaced only once the eleven above stopped throwing.
|
|
518
|
+
- `validate-security-posture.ts`, the one that never threw: an `[]`
|
|
519
|
+
member passed its `typeof v === 'object'` read and drew a second
|
|
520
|
+
`security-owd-unset` at `object "(object 0)"` — an `error` about an
|
|
521
|
+
entry no author wrote.
|
|
522
|
+
|
|
523
|
+
The verdict is a SKIP, not a finding, matching the seam it extends: a junk
|
|
524
|
+
`objects` member is a SHAPE defect and belongs to the schema, every rule
|
|
525
|
+
already re-answers the question in its own per-object guard, and reporting
|
|
526
|
+
it at the reader would emit one finding per member for one bad entry. On
|
|
527
|
+
the name-keyed map shape a member whose VALUE is unreadable keeps its key
|
|
528
|
+
(`{ name }`) — the author named it, only its body is illegible.
|
|
529
|
+
|
|
530
|
+
No rule tier, id, message or accept-set changes. A valid object standing
|
|
531
|
+
beside a junk one is judged exactly as it is judged alone; only a path
|
|
532
|
+
index moves, and only for the rules that index `objects` raw, where
|
|
533
|
+
`objects[1]` is the honest position.
|
|
534
|
+
- 434ca2d: `validateStackExpressions` no longer throws on a non-record entry in an object's `fields:` list.
|
|
535
|
+
|
|
536
|
+
An empty item in a YAML `fields:` list deserialises to `null`, and `buildFieldIndex` cast each member of the list inline (`fields.map(f => (f as AnyRec).name)`) before the `.filter` two calls later could drop it. `Array.isArray` proves the LIST, never its MEMBERS, so linting such a stack failed with `TypeError: Cannot read properties of null (reading 'name')` out of the whole rule instead of reporting anything about the file.
|
|
537
|
+
|
|
538
|
+
The list is now read through `recordsOf` — the one place that coercion is decided — which drops a non-record member of the array shape whole and in **silence**: it carries no author-written name, so there is nothing to report about it. That matches what the two sibling field readers in the same module (`buildFieldTypeIndex`, `fieldEntries`) already did with the same member, so the three readers now agree. The readable siblings of the junk member are still indexed, so unknown-field findings on that object continue to be reported.
|
|
539
|
+
|
|
540
|
+
The map shape (`fields: { amount: { … } }`) is unchanged: there the author's key is the field name, which is what this index needs.
|
|
541
|
+
- 25a3d91: Stop reporting a declarative `operation: 'update'` action as "a button wired to nothing"
|
|
542
|
+
|
|
543
|
+
The boot action-governance inventory (ADR-0110 D5) built its `unboundDeclarations`
|
|
544
|
+
finding from a `type`-only test. The declarative single-record field write
|
|
545
|
+
(`operation: 'update'` + `patch`, #14092) is exactly the shape that test mistakes
|
|
546
|
+
for a dead button: `ActionSchema` refuses `target` and `body` beside it and keeps
|
|
547
|
+
`type` at its default `script`, because the platform action route is where the
|
|
548
|
+
write is performed. Every such action was named at every boot and every
|
|
549
|
+
`metadata:reloaded` — with a prescription ("add a `body`, or register a handler
|
|
550
|
+
under the declared `target`") that parse itself refuses.
|
|
551
|
+
|
|
552
|
+
Both readers now read `operation` before `type`, the precedence the runtime doors
|
|
553
|
+
already use: the engine inventory, and the authoring-time AI tool-reference rule,
|
|
554
|
+
which had diverged from the runtime's listing door and reported a resolvable
|
|
555
|
+
`action_<name>` reference as fictional.
|
|
556
|
+
- ce21963: `nav-object-ungranted`'s hint no longer tells you to gate the nav entry with `requiredPermissions`/`visible` — that never cleared the finding, because the rule never reads either key. Gating restricts who can see the entry; it doesn't grant the object read, so a holder who clears the gate could still hit permission-denied, and the warning kept firing anyway. The hint (and the module doc-block) now name the two remedies that actually clear it: grant read on the object in a permission set (`allowRead: true` or `viewAllRecords`), or drop the nav entry. No behavior change — the rule fires and stays silent on exactly the same inputs as before; only the wording of the hint moved.
|
|
557
|
+
- ba426b0: A junk entry in `stack.objects` no longer crashes the reference-integrity rules, and a probe rule that throws is reported instead of read as "nothing wrong".
|
|
558
|
+
|
|
559
|
+
`indexObjectGraph` is the first statement of every rule that resolves a field path, and it read each `stack.objects` member without checking it was a record — so a `null` entry (an empty YAML list item, a partial editor write) threw `TypeError: Cannot read properties of null (reading 'name')` before any rule's own per-object guard could run. Because these rules also run inside the runtime publish gate, that was an exception on a write path rather than a missed finding. The seam now drops non-record entries — silently, matching every sibling collection reader in the package — and the valid objects beside them are judged exactly as before.
|
|
560
|
+
|
|
561
|
+
On the publish receipt, `runBuildProbes`' object plane wrapped its rule call in a catch that produced an empty finding list, so a crashed rule was indistinguishable from a clean object while `checked.objects` had already counted it. A rule that throws now surfaces as a `runtime`-layer `object_field_ref_rule_failed` error carrying the thrown message, so an unverified object never reads as a verified one. Probes still never fail the publish they verify.
|
|
562
|
+
- eda26ce: `security-owd-alias` no longer tells authors that `sharingModel: 'public'` is a retired ADR-0090 D4 alias. It never was one — no shipped schema ever accepted it — and the map that said so is now split along the two histories it was conflating.
|
|
563
|
+
|
|
564
|
+
`OWD_ALIAS_FIX` carried four keys under one comment, `[ADR-0090 D4] Legacy alias → canonical fix-it mapping`, while D4 names three: "The legacy aliases `read`, `read_write`, `full` are **removed from the zod enum**". Those three have a retirement behind them — the `owd-legacy-read-aliases` ADR-0087 stored-row conversion for the two `read*` spellings, the `13.owd-full-alias-removed` semantic entry for `full`. `public` has neither, and correctly so: a conversion rewrites a spelling some shipped schema once took, and this one never was taken, so its stored population is zero by construction. The missing conversion was the mislabel's shadow, not a gap.
|
|
565
|
+
|
|
566
|
+
- **Two maps, one union.** `OWD_RETIRED_ALIAS_FIX` holds the three D4 aliases; `OWD_WRONG_LAYER_FIX` holds `public`; `OWD_ALIAS_FIX` stays as their union, so every key still earns the same rule id, the same path and the same fix-it. No accept set moves and no value starts or stops being reported.
|
|
567
|
+
- **The `public` fix-it is KEPT.** It catches a real authoring mistake: three neighbouring keys on the same `ObjectSchema` take `'public'` legally — `access.default` (`z.enum(['public', 'private'])`, ADR-0066) and `publicSharing.allowedAudiences` (`z.enum(['public', 'link_only', 'signed_in', 'email'])`) — and off-schema so does the sharing runtime's own internal vocabulary, `effectiveSharingModel(): 'private' | 'read' | 'public'`. `sharingModel` is the one neighbour that refuses it, and it fails CLOSED to `private` with no notice on the read path, so this fix-it is the author's only signal.
|
|
568
|
+
- **The message says which group it is in.** One shared clause, used by both the `sharingModel` and the `externalSharingModel` branch. A retired alias still reads `is a retired alias (ADR-0090 D4)`; `public` now reads that it is not an OWD value and never was, and names the neighbouring keys that do take it. The fix-it text, severity, rule id and path are byte-identical either way.
|
|
569
|
+
|
|
570
|
+
Why the wording mattered enough to change: a diagnostic that credits `public` to D4 sends its reader looking for the conversion and the semantic entry that would exist if the acceptance had happened, and finding them absent reads as a data-fidelity defect in the conversion registry. It is not one.
|
|
571
|
+
- ed6579b: `objectstack build` now refuses to lower a hook/action body that calls `.create(`, and the shared write-pattern ledger stops advertising the verb. Three layers used to disagree about `ctx.api.object('x').create({ … })`, and the loudest one was wrong.
|
|
572
|
+
|
|
573
|
+
- The spec contract `IScopedObjectRepository` (`packages/spec/src/contracts/scoped-context.ts`) declares `insert` and names `create` as measured-and-deliberately-excluded.
|
|
574
|
+
- The QuickJS sandbox installs exactly `insert / update / delete / updateMany / deleteMany / upsert` as the `ctx.api.object()` write leaves — no `create`. An L2 body calling `.create()` therefore threw `TypeError: not a function` on its **first run**, and under a hook's default `onError: 'abort'` that throw aborted the triggering write, with a message naming no member.
|
|
575
|
+
- The extractor ledger nonetheless advertised `.create({…})` as legal `api-crud-literal` syntax and mapped it in `API_WRITE_METHODS`, so `hook-body-write-unknown-field` graded the payload as a live write and stayed silent when the field existed — a clean bill of health for a call that cannot run. Build time said nothing at all.
|
|
576
|
+
|
|
577
|
+
What changes:
|
|
578
|
+
|
|
579
|
+
- **`@objectstack/cli`** — `.create(` joins `FORBIDDEN_PATTERNS` in the hook/action body extractor, beside `.sudo(` and for the same reason (a member real on the in-process `ScopedContext` / `ObjectRepository` and absent from the VM). The refusal names `.insert({ ... })` as the spelling the sandbox actually has. Behaviour is the `forbidden-token` fallback every other entry has: the callable is still registered and still shipped through the back-compat `.mjs` bundle, so a handler keeps running in-process where the host `create()` alias exists — `objectstack build` merely declines to *also* emit it as a body that cannot run. Under `--strict-body` it is a hard failure, correctly. The rule is receiver-loose like `.sudo(` (`const repo = ctx.api.object('x'); repo.create(…)` is refused too) with one carve-out: `Object.create()` is a real sandbox global and is **not** affected.
|
|
580
|
+
- **`@objectstack/lint`** — `create` is withdrawn from `HOOK_BODY_WRITE_PATTERNS`' advertised `api-crud-literal` syntax and from `API_WRITE_METHODS`, on the hook and action surfaces alike. `hook-body-write-unknown-field` / `action-body-write-unknown-field` no longer grade a `.create()` payload; `hook-api-update-readonly-field` keeps its existing `create` exclusion, whose *reason* is updated — it is no longer "the call throws, so a silently-dropped finding would be false" but "the shape can no longer reach this rule at all".
|
|
581
|
+
|
|
582
|
+
**Migration.** If a hook or action body calls `ctx.api.object('x').create({ … })`, spell it `ctx.api.object('x').insert({ … })` — the same host method, the one the sandbox installs and the only insert verb the contract declares. The host-side `ObjectRepository.create()` alias is untouched and stays reachable from in-process handlers and actions.
|
|
583
|
+
- 89758ac: `chart-axis-not-selected` resolves a report chart against its own `chart.yAxis`, not `report.values` (#15734)
|
|
584
|
+
|
|
585
|
+
**Behaviour change — one false finding removed on the report surface.** A report chart whose `chart.yAxis` names a declared measure that `report.values` does not select no longer raises a `chart-axis-not-selected` warning. Nothing else about the rule moves, and no other surface moves at all.
|
|
586
|
+
|
|
587
|
+
The warning stated a query consequence the renderer refutes. Read at the `@object-ui` revision this repo pins (`.objectui-sha`), `plugin-report/src/DatasetReportRenderer.tsx` does not query `report.values` for the chart at all — it runs the chart's own, narrower query out of the two axis strings:
|
|
588
|
+
|
|
589
|
+
```
|
|
590
|
+
const state = useDatasetRows(
|
|
591
|
+
dataset,
|
|
592
|
+
plan.kind === 'series' && xAxis ? [xAxis] : [],
|
|
593
|
+
wantsQuery && yAxis ? [yAxis] : [],
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
and says so in that file's own words at the `scopeOrder` docblock: *"the embedded chart queries only `chart.xAxis` × `chart.yAxis`"*. So the measure the warning said "the query does not return" is exactly the one the query asks for, and the chart plots it. `report.values` is the selection of the TABLE beneath the chart.
|
|
597
|
+
|
|
598
|
+
Both limbs follow from that one measurement:
|
|
599
|
+
|
|
600
|
+
- **No not-selected check at the report `chart.yAxis`.** That position IS the chart's query, so it cannot fail to select itself. `chart-measure-unknown` there is untouched: an UNDECLARED measure is still no column at all, and still an `error`.
|
|
601
|
+
- **`chart.series[].name` resolves against the singleton `{ chart.yAxis }`.** The entry is a display-name override paired with a DERIVED series, and the chart derives exactly one (`buildChartSeries(…, [xAxis], [yAxis], …)`). An entry naming `chart.yAxis` now lands however the table is selected, and one naming any other declared measure is still reported — including a measure `report.values` does select, which it could not reach before.
|
|
602
|
+
|
|
603
|
+
The list-view and page-component surfaces are unchanged, and carry firing controls that say so: on both, `values` IS the measure set the query asks for (`ObjectView` hands it to the chart; `ObjectChart` queries `{ dimensions: schema.dimensions, measures: schema.values }`), so the existing resolution is the right one there.
|
|
604
|
+
|
|
605
|
+
The per-position tier and consequence wording is untouched — only the SET the report surface resolves against moves.
|
|
606
|
+
- d83d079: A report's `dataset`, `rows`, `columns` and `values` are checked whether or not the report draws a chart (#16105)
|
|
607
|
+
|
|
608
|
+
**Behaviour change — new findings on reports that used to publish clean.** `validateChartBindings` reached a report through one closure that opened `if (!isRec(chart)) return`, and that closure was the only place a report's `dataset` was ever passed to the resolver. Two gaps followed, and both are closed:
|
|
609
|
+
|
|
610
|
+
- **A report authored without a `chart` was not checked at all.** Bind it to a dataset that does not exist and `os lint` exited 0 and named nothing. It now reports `chart-dataset-unknown` at `error`, the same as a charted report always did.
|
|
611
|
+
- **`rows` and `columns` were resolved on no report, charted or not.** On one and the same report object the measure selection (`values`) was resolved against the dataset and the dimension selection beside it was not. Both now report `chart-dimension-unknown` at `error` for a name the bound dataset does not declare as a dimension, at `reports[i].rows[j]` / `reports[i].columns[j]`. A chartless report's `values` is resolved for the first time too, under the existing `chart-measure-unknown`.
|
|
612
|
+
|
|
613
|
+
`ReportSchema` is what makes these bindings rather than free text: it requires `dataset` + `values` on every non-`joined` report, and declares `rows` (the down axis) and `columns` (the across axis a `matrix` pivots on, ADR-0021 D2) as dimension names taken from that dataset. The chart is optional decoration on top of a binding the report already has. So a report bound to a missing dataset, or grouping on a dimension its dataset does not declare, now fails authoring instead of rendering blank or mis-grouped in production.
|
|
614
|
+
|
|
615
|
+
No new rule id, no severity moved, and the charted path is unchanged — `chart-axis-not-selected` stays a `warning` and still resolves against the chart's own `chart.yAxis`. Two smaller corrections come with the restructure, both on messages an author reads:
|
|
616
|
+
|
|
617
|
+
- The dataset finding on a report now points at `reports[i].dataset`, the key the author wrote. It used to say `reports[i].chart.dataset`, a position a report does not have.
|
|
618
|
+
- Its sentence ends "there is no data to render" rather than "the chart has no data to render", which is not true of a report that draws no chart.
|
|
619
|
+
|
|
620
|
+
Blocks of a `joined` report carry the same keys and take the same checks. An unresolvable dataset is still exactly one finding per report or block.
|
|
621
|
+
- c5d6803: Published `.js.map` files no longer embed the complete original source text (`sourcesContent`) — comments included. `sourcemap: true` was esbuild shorthand, and esbuild's own default for `sourcesContent` is `true`; nobody had decided to publish every package's full source (including `@internal`/test-only comments) to npm inside its source maps, it fell out of a default nobody had looked at. Measured before this change: 55 of 57 publishable packages shipped embedded source text, and maps were roughly half of `@objectstack/spec`'s published bytes.
|
|
622
|
+
|
|
623
|
+
`sourcesContent: false` is now set at one shared place (`scripts/tsup-drop-sources-content.mjs`, wired into every `tsup.config.ts` via tsup's `esbuildOptions` hook — most packages build through the repo-root config directly and pick this up with no config change of their own). `mappings` are untouched, so stack-trace positions still resolve correctly to the original file/line/column; only the embedded source text is gone.
|
|
624
|
+
|
|
625
|
+
`@objectstack/cli` (built with `tsc`, not `tsup`) never embedded source text to begin with — its maps' `sources` entries point at `src/**` paths that are not part of the published tarball either way. That is not a defect unique to `cli`: every `tsup`-built package's `sources` entries are `../src/**`-relative paths that are equally outside `files: ["dist", …]`, and were merely masked by the embedded content that just stopped shipping. Shipping `src/**` in `files[]` to make `sources` resolve was rejected — it would put most of the removed bytes straight back. So `cli`'s maps are left exactly as `tsc` emits them: this is now the fleet-consistent shape (accurate `mappings`, non-resolving-but-honest `sources` labels, no embedded text), not an outlier.
|
|
626
|
+
|
|
627
|
+
A new gate, `pnpm check:sourcemap-no-sources-content`, sweeps every built, non-private package's `dist/**/*.map` and fails if any of them carries a non-empty `sourcesContent` array — so a future `tsup.config.ts` that skips the shared hook, or a toolchain upgrade that changes esbuild's default back, is caught rather than silently re-publishing source text.
|
|
628
|
+
- b398ad2: **BREAKING (behaviour):** a static `readonly` field is now stripped from a **non-system caller's INSERT payload inside `engine.insert`**, exactly as it already was on `engine.update`. A non-system create that used to write a read-only column now has that column dropped, reported through `onFieldsDropped` / `droppedFields`, logged at `warn`, and refused outright under `strictReadonlyWrites`. Seeding a read-only column at create time is a **system** act — use `context.isSystem`, a flow's `runAs: 'system'`, a system hook or a seed.
|
|
629
|
+
|
|
630
|
+
Until now the create-side strip lived only at the DataProtocol ingress (`stripReadonlyForInsert` in `@objectstack/metadata-protocol`), so `readonly` meant one thing on insert and another on update: every external REST/GraphQL/MCP create was stripped, while a caller reaching `engine.insert` directly — the automation engine's `create_record` among them — wrote the column with no refusal, no `WARN` and no dropped-field event.
|
|
631
|
+
|
|
632
|
+
- `stripReadonlyForInsert` and its five call sites in `@objectstack/metadata-protocol` are **deleted**, not kept as a second implementation; every create face — `createData`, `cloneData`, `createManyData`, `insertManyData`, and `batchData`'s `create` rows and both arms of `upsert` that create — now hands the caller's payload to the engine whole, and every face whose response carries `droppedFields` (`createData`, `createManyData`, `insertManyData`, every `batchData` row that created) reports the engine's own verdict there, so `droppedFields` says the same thing at each of those seams. `cloneData` forwards whole but reports nothing on the wire: its response contract (`CloneDataResponseSchema`, declared as produced) has no `droppedFields` member, so a clone that carried or overrode a read-only column is stripped and logged at `warn` but not reported in the 201 body — adding that key is a spec change, not part of this one.
|
|
633
|
+
- `create_record` (`@objectstack/service-automation`) starts receiving readonly drops on the `onFieldsDropped` channel it has been wired for since #3407 — a flow without `runAs: 'system'` that seeds a read-only column now reports a node warning and `output.droppedFields` instead of a clean success. That package's own code changes only in prose; the traffic is new, the surface is not.
|
|
634
|
+
- Unchanged, deliberately: `isSystem` is still the exemption; `preserveAudit` is still an UPDATE-path exemption and a create that asks for it is told so out loud; runtime-owned types (`autonumber`) keep their own pass and their own wider whitelist; platform objects (`managedBy`, the `sys_` namespace) are still left to their own field-write guards; `readonlyWhen` still has no create-side strip. A stripped key's `defaultValue` is re-derived, so a forged `approval_status` becomes `draft` rather than NULL.
|
|
635
|
+
- `@objectstack/service-settings` is `patch`: prose only — the `upsertRow` docblock, which ships in the package's `.d.ts`, no longer states the superseded INSERT exemption; it names the platform-object carve-out that actually keeps a `sys_setting` insert outside the strip.
|
|
636
|
+
- `@objectstack/lint` and `@objectstack/spec` are `patch`: both change prose only. All three lint rules — `validate-readonly-action-writes`, `validate-readonly-flow-writes`, `validate-readonly-hook-writes` — drop the superseded "INSERT is exempt" premise from their docblocks and from the justification of their green control cases; the two non-elevated rules now name their `insert`/`create` silence as a scan gap rather than an exemption (the action rule additionally records its now-reasoned refusal as a module-local constant that its `index` does not re-export, so no public surface widens). The spec change is prose only: one docblock sentence that named the deleted function, the `strictReadonlyWrites` contract docblock (which now states what strict refuses on insert), and the `readonly` liveness-ledger verdict, whose evidence pointer named the deleted ingress strip.
|
|
637
|
+
|
|
638
|
+
<!-- adr-0087: not-required (no-migration-prescription) The BREAKING here is a WRITE-PATH BEHAVIOUR change, not a retirement of an authorable or published surface, so there is nothing for the ledger to carry to `objectstack migrate meta`, `spec-changes.json` or the upgrade guide: no spec property, metadata key, accepted value or exported symbol disappears, and this body prescribes no FROM/TO migration. The remedy for an affected caller is to declare the write trusted (`context.isSystem` / `runAs: 'system'`), which is application code, not a metadata migration. The obvious retirement candidate is a non-question in the same direction: `stripReadonlyForInsert` was a bare module-private `function` in `packages/metadata-protocol/src/protocol.ts`, absent from that package's `index.ts` (the only path its `exports` map offers), so no consumer could name it. -->
|
|
639
|
+
- 60ff091: `visibility-bare-identifier` now reports an identifier written bare beside a `has()` guard in the same visibility predicate.
|
|
640
|
+
|
|
641
|
+
`has(status) && status == "qualified"` published clean while `status == "qualified"` — the same defect, without the guard — gated at `error`. The guarded spelling is the one the totality discipline pushes authors toward, so an author who correctly adds `has()` and forgets the `record.` prefix on both halves landed in the silent row. That predicate never evaluates for any record, and an unevaluable `visibleWhen` on a form surface fails OPEN: the field renders and carries its `required: true` into the console's submit check.
|
|
642
|
+
|
|
643
|
+
The cause was not the exclusion a `has()` argument earns — that is correct and stays. `firstUndeclaredReference` reads the first error the CEL checker reports and acts only on `Unknown variable: X`; a bare `has(x)` fails that check with `has() invalid argument` instead, and a first error of a different class masked every undeclared reference behind it in the same predicate, whatever it was called. Each `has(…)` call is now masked out of the source before the checker sees it, using the canonical AST's own spans, so the argument occurrence is excluded and every other occurrence is judged exactly as it would be with no guard written beside it.
|
|
644
|
+
|
|
645
|
+
Expect new `error` findings on predicates that used to publish clean: a guarded-but-unprefixed `visibleWhen` on a view, page component or form section is now refused at build, validate and lint alike. That is the fail-open shape the rule exists to catch. A `has()` argument that is the only bare occurrence — `has(status)` on its own — stays silent, as it did before.
|
|
646
|
+
- Updated dependencies [fe0d9a4]
|
|
647
|
+
- Updated dependencies [ecd2158]
|
|
648
|
+
- Updated dependencies [f2b5e46]
|
|
649
|
+
- Updated dependencies [ed7243d]
|
|
650
|
+
- Updated dependencies [6ba0db4]
|
|
651
|
+
- Updated dependencies [625b0c3]
|
|
652
|
+
- Updated dependencies [233222e]
|
|
653
|
+
- Updated dependencies [07f40e5]
|
|
654
|
+
- Updated dependencies [ceb4877]
|
|
655
|
+
- Updated dependencies [e9fcd6b]
|
|
656
|
+
- Updated dependencies [90e7e6d]
|
|
657
|
+
- Updated dependencies [2bdabe6]
|
|
658
|
+
- Updated dependencies [ca326b5]
|
|
659
|
+
- Updated dependencies [8f404a5]
|
|
660
|
+
- Updated dependencies [68437d4]
|
|
661
|
+
- Updated dependencies [abb140c]
|
|
662
|
+
- Updated dependencies [8333a6c]
|
|
663
|
+
- Updated dependencies [3e3ecb0]
|
|
664
|
+
- Updated dependencies [3030369]
|
|
665
|
+
- Updated dependencies [d5d8d50]
|
|
666
|
+
- Updated dependencies [e08892d]
|
|
667
|
+
- Updated dependencies [ae05f2e]
|
|
668
|
+
- Updated dependencies [b548e43]
|
|
669
|
+
- Updated dependencies [c463d03]
|
|
670
|
+
- Updated dependencies [64bd6a3]
|
|
671
|
+
- Updated dependencies [13c48c2]
|
|
672
|
+
- Updated dependencies [132742f]
|
|
673
|
+
- Updated dependencies [85a2459]
|
|
674
|
+
- Updated dependencies [50dc214]
|
|
675
|
+
- Updated dependencies [e89fa92]
|
|
676
|
+
- Updated dependencies [e9fcd6b]
|
|
677
|
+
- Updated dependencies [8976ea1]
|
|
678
|
+
- Updated dependencies [56fe8c2]
|
|
679
|
+
- Updated dependencies [acabd24]
|
|
680
|
+
- Updated dependencies [ab50c8f]
|
|
681
|
+
- Updated dependencies [6491463]
|
|
682
|
+
- Updated dependencies [89cf4d6]
|
|
683
|
+
- Updated dependencies [21c5dcb]
|
|
684
|
+
- Updated dependencies [6d4d5d3]
|
|
685
|
+
- Updated dependencies [ed5d557]
|
|
686
|
+
- Updated dependencies [bca21f7]
|
|
687
|
+
- Updated dependencies [e9fcd6b]
|
|
688
|
+
- Updated dependencies [1a7a7c9]
|
|
689
|
+
- Updated dependencies [e9fcd6b]
|
|
690
|
+
- Updated dependencies [ef3a138]
|
|
691
|
+
- Updated dependencies [68d5dfd]
|
|
692
|
+
- Updated dependencies [3e21cf0]
|
|
693
|
+
- Updated dependencies [4cfc93b]
|
|
694
|
+
- Updated dependencies [098cbb7]
|
|
695
|
+
- Updated dependencies [efd6b43]
|
|
696
|
+
- Updated dependencies [859ded3]
|
|
697
|
+
- Updated dependencies [fa125f3]
|
|
698
|
+
- Updated dependencies [74628d9]
|
|
699
|
+
- Updated dependencies [a646120]
|
|
700
|
+
- Updated dependencies [6f1ce7d]
|
|
701
|
+
- Updated dependencies [7778115]
|
|
702
|
+
- Updated dependencies [86c75f4]
|
|
703
|
+
- Updated dependencies [2c753fe]
|
|
704
|
+
- Updated dependencies [52804cd]
|
|
705
|
+
- Updated dependencies [3f89967]
|
|
706
|
+
- Updated dependencies [53cf263]
|
|
707
|
+
- Updated dependencies [21aabbc]
|
|
708
|
+
- Updated dependencies [9c270bb]
|
|
709
|
+
- Updated dependencies [76c8c5a]
|
|
710
|
+
- Updated dependencies [a84e1ce]
|
|
711
|
+
- Updated dependencies [bf1054a]
|
|
712
|
+
- Updated dependencies [d8d2776]
|
|
713
|
+
- Updated dependencies [222dc0f]
|
|
714
|
+
- Updated dependencies [e9fcd6b]
|
|
715
|
+
- Updated dependencies [32c917d]
|
|
716
|
+
- Updated dependencies [f9a3c32]
|
|
717
|
+
- Updated dependencies [f502898]
|
|
718
|
+
- Updated dependencies [af7edfe]
|
|
719
|
+
- Updated dependencies [b60f48b]
|
|
720
|
+
- Updated dependencies [c78c918]
|
|
721
|
+
- Updated dependencies [cf9bda4]
|
|
722
|
+
- Updated dependencies [784cb92]
|
|
723
|
+
- Updated dependencies [7629f4d]
|
|
724
|
+
- Updated dependencies [51df9fd]
|
|
725
|
+
- Updated dependencies [a7da4de]
|
|
726
|
+
- Updated dependencies [de0bcdd]
|
|
727
|
+
- Updated dependencies [70f7d6d]
|
|
728
|
+
- Updated dependencies [c677cda]
|
|
729
|
+
- Updated dependencies [554a160]
|
|
730
|
+
- Updated dependencies [f7da71e]
|
|
731
|
+
- Updated dependencies [7f745c3]
|
|
732
|
+
- Updated dependencies [5eb24f8]
|
|
733
|
+
- Updated dependencies [2a3decc]
|
|
734
|
+
- Updated dependencies [cc00df2]
|
|
735
|
+
- Updated dependencies [f4e6adf]
|
|
736
|
+
- Updated dependencies [ee4a59b]
|
|
737
|
+
- Updated dependencies [4db3c61]
|
|
738
|
+
- Updated dependencies [5ca314a]
|
|
739
|
+
- Updated dependencies [e0af1a8]
|
|
740
|
+
- Updated dependencies [414c1fc]
|
|
741
|
+
- Updated dependencies [22c0279]
|
|
742
|
+
- Updated dependencies [0db2947]
|
|
743
|
+
- Updated dependencies [92b5d7f]
|
|
744
|
+
- Updated dependencies [613bfbd]
|
|
745
|
+
- Updated dependencies [abae16a]
|
|
746
|
+
- Updated dependencies [094b8fd]
|
|
747
|
+
- Updated dependencies [c7aca0d]
|
|
748
|
+
- Updated dependencies [c1d8f98]
|
|
749
|
+
- Updated dependencies [8e0b297]
|
|
750
|
+
- Updated dependencies [5f7fa1d]
|
|
751
|
+
- Updated dependencies [87f0ccc]
|
|
752
|
+
- Updated dependencies [aedbaef]
|
|
753
|
+
- Updated dependencies [c5d6803]
|
|
754
|
+
- Updated dependencies [10d05bb]
|
|
755
|
+
- Updated dependencies [69602e5]
|
|
756
|
+
- Updated dependencies [c3ce76c]
|
|
757
|
+
- Updated dependencies [7936b29]
|
|
758
|
+
- Updated dependencies [46803fa]
|
|
759
|
+
- Updated dependencies [c2a336c]
|
|
760
|
+
- Updated dependencies [9f890d3]
|
|
761
|
+
- Updated dependencies [0bb2318]
|
|
762
|
+
- Updated dependencies [f7db8f4]
|
|
763
|
+
- Updated dependencies [1ecee3e]
|
|
764
|
+
- Updated dependencies [9408b7f]
|
|
765
|
+
- Updated dependencies [e9fcd6b]
|
|
766
|
+
- Updated dependencies [9bcd9be]
|
|
767
|
+
- Updated dependencies [b398ad2]
|
|
768
|
+
- Updated dependencies [99261a7]
|
|
769
|
+
- Updated dependencies [81b426f]
|
|
770
|
+
- Updated dependencies [001af1c]
|
|
771
|
+
- Updated dependencies [fb77aa5]
|
|
772
|
+
- Updated dependencies [581d8f8]
|
|
773
|
+
- Updated dependencies [f81afe3]
|
|
774
|
+
- Updated dependencies [40a44b9]
|
|
775
|
+
- Updated dependencies [7a7fb03]
|
|
776
|
+
- Updated dependencies [8fd246d]
|
|
777
|
+
- @objectstack/spec@17.4.0
|
|
778
|
+
- @objectstack/formula@17.4.0
|
|
779
|
+
- @objectstack/sdui-parser@17.4.0
|
|
780
|
+
|
|
781
|
+
## 17.3.0
|
|
782
|
+
|
|
783
|
+
### Minor Changes
|
|
784
|
+
|
|
785
|
+
- 345fc33: Resolve an ADR-0021 dataset's own references — base object, `include[]`,
|
|
786
|
+
`dimensions[].field` / `measures[].field`, and filter KEYS — at
|
|
787
|
+
`validate`/`build` (#14105)
|
|
788
|
+
|
|
789
|
+
A dataset could name a **base object that does not exist**, join a
|
|
790
|
+
**relationship that does not exist**, and bind every dimension and measure to
|
|
791
|
+
**fields that do not exist**, and `objectstack validate` exited **0** with
|
|
792
|
+
`✓ Validation passed`. `objectstack build` also exited 0 and wrote the dangling
|
|
793
|
+
dataset into `dist/objectstack.json`.
|
|
794
|
+
|
|
795
|
+
The sting was that the author-time rule pass **already walked those exact
|
|
796
|
+
nodes**. Measured on published 17.2.0, each mutation applied on its own and
|
|
797
|
+
confirmed on disk before running:
|
|
798
|
+
|
|
799
|
+
| mutation | before | after |
|
|
800
|
+
|:----------------------------------------------------|:-------|:------|
|
|
801
|
+
| dimension `field` → a base field that does not exist | passed | `dataset-field-unknown` |
|
|
802
|
+
| dimension `field` → a joined field that does not exist | passed | `dataset-field-unknown` |
|
|
803
|
+
| measure `field` → a field that does not exist | passed | `dataset-field-unknown` |
|
|
804
|
+
| measure filter KEY → a field that does not exist | passed | `dataset-filter-field-unknown` |
|
|
805
|
+
| `include[]` → a relationship that does not exist | passed | `dataset-include-unknown` |
|
|
806
|
+
| `object` → an object that does not exist | passed | `object-reference-unknown` |
|
|
807
|
+
|
|
808
|
+
The two controls in that measurement — a duplicate measure name
|
|
809
|
+
(`DatasetSchema.superRefine`) and a bad date macro in a **measure filter**
|
|
810
|
+
(`filter-token-unknown`) — both failed the build, so datasets were
|
|
811
|
+
demonstrably in the validation path the whole time. `filter-token-unknown`
|
|
812
|
+
already stood at `datasets[1].measures[1].filter.last_update_at.$lt` and
|
|
813
|
+
reasoned about the **value**; nothing standing in that same position resolved
|
|
814
|
+
the **key**, or the sibling `field` one level up.
|
|
815
|
+
|
|
816
|
+
This matters more for a dataset than for most metadata because a dataset is the
|
|
817
|
+
semantic layer: dashboards and reports bind its dimensions and measures by name
|
|
818
|
+
(ADR-0021), and the consumer end of that binding is already guarded
|
|
819
|
+
(`widget-dataset-unknown` / `widget-dimension-unknown` / `widget-measure-unknown`,
|
|
820
|
+
#7529/#8902). So the surviving hole was the quiet one — every binding resolves,
|
|
821
|
+
the board renders, and the charts are empty or subtly wrong because the dataset
|
|
822
|
+
underneath addresses columns that do not exist.
|
|
823
|
+
|
|
824
|
+
**Five verdicts, all `error`.** Four are new rule ids on a new suite member,
|
|
825
|
+
`validateDatasetReferences`:
|
|
826
|
+
|
|
827
|
+
- `dataset-include-unknown` — an `include[]` entry that resolves to nothing, or
|
|
828
|
+
to a field that is not a relationship, so no join can be derived from it.
|
|
829
|
+
- `dataset-field-unknown` — a dimension or measure `field` path that resolves to
|
|
830
|
+
no column, on the base object or on any joined object along the path.
|
|
831
|
+
- `dataset-field-not-included` — the second real check: a dotted path that
|
|
832
|
+
RESOLVES, but whose relationship prefix was never declared in `include`.
|
|
833
|
+
ADR-0021 D-C joins only declared paths, so the column is out of the query's
|
|
834
|
+
reach however real it is.
|
|
835
|
+
- `dataset-filter-field-unknown` — a filter key on `Dataset.filter` or
|
|
836
|
+
`measures[].filter`, in any of the three authored filter shapes.
|
|
837
|
+
|
|
838
|
+
The fifth, the base object itself, lands on `validateObjectReferences` as a new
|
|
839
|
+
`datasets[].object` reference site rather than as a sixth id here. That rule's
|
|
840
|
+
charter IS object-name references that are plain `z.string()`, and putting it
|
|
841
|
+
there buys the curated cross-package severity ladder: the platform's own
|
|
842
|
+
`system.datasets.ts` declares five datasets over `sys_*` objects, three of which
|
|
843
|
+
live in packages a stack compiling plugin-auth alone cannot see. All five
|
|
844
|
+
resolve through `PLATFORM_PROVIDED_OBJECT_NAMES`; a local "not in this stack ⇒
|
|
845
|
+
error" check would have reported every one of them. When the base object does
|
|
846
|
+
not resolve, `validateDatasetReferences` skips the dataset entirely, so one typo
|
|
847
|
+
yields one finding rather than one per dimension, measure and filter key.
|
|
848
|
+
|
|
849
|
+
**Skips, so a finding is never a guess** (ADR-0072 D1): an object this stack
|
|
850
|
+
does not define, an object with no readable field map (ADR-0015 `external` and
|
|
851
|
+
introspected schemas), a registry-injected system column, and any hop *through*
|
|
852
|
+
one — an injected `owner_id` is a lookup at the registry whose target is
|
|
853
|
+
invisible here, so `owner_id.name` is unanswerable rather than a miss. The
|
|
854
|
+
shipped `showcase_task_metrics` dimension `{ field: 'created_at' }` is that skip's
|
|
855
|
+
live case, and every shipped dataset in the repo is silent under the new rule.
|
|
856
|
+
|
|
857
|
+
**Two reusable seams ship with it**, newly exported, because the same two
|
|
858
|
+
questions are asked at a dashboard widget's filter keys and `sortBy` and at a
|
|
859
|
+
list view's field positions, and three independent copies of a hop-walker drift:
|
|
860
|
+
|
|
861
|
+
- `object-graph.ts` — `indexObjectGraph` / `resolveFieldPath` / `isUnjudgeable`,
|
|
862
|
+
answering "what does this `relationship[.relationship].field` path resolve to?"
|
|
863
|
+
as a discriminated **verdict** union rather than a boolean, so a caller can
|
|
864
|
+
tell "this hop is not a relationship" from "this leaf does not exist" and write
|
|
865
|
+
the right prescription. Plus `nearestName` / `suggestName` / `listNames`.
|
|
866
|
+
- `walkFilterFieldKeys` (`filter-walk.ts`) — the FIELD-KEY half of a filter
|
|
867
|
+
subtree, beside the subtree-finding half that module already owns. It handles
|
|
868
|
+
all three authored shapes (Mongo condition object, `{ field, operator, value }`
|
|
869
|
+
rules, `[field, op, value]` triples), because a reader that handles only one
|
|
870
|
+
shape is the exact bug #3574 was filed against, and it composes a nested
|
|
871
|
+
condition object into one relationship path so `{ account: { region: … } }`
|
|
872
|
+
reports `account.region` rather than a bare `region` resolved against the
|
|
873
|
+
wrong object.
|
|
874
|
+
|
|
875
|
+
Both hold mechanism only — no rule ids, no severities, no findings.
|
|
876
|
+
- 09ae32e: Judge `visibleWhen` / `readonlyWhen` / `requiredWhen` examples in the docs corpus
|
|
877
|
+
as CEL — where the enclosing structure says which layer they are about
|
|
878
|
+
|
|
879
|
+
`{/* os:check */}` blocks are type-checked by `tsc --noEmit`, and every CEL
|
|
880
|
+
string is the same type as every other CEL string, so
|
|
881
|
+
`visibleWhen: "record.status != 'closed' && user.hasRole('admin')"` type-checked
|
|
882
|
+
perfectly. `hasRole` is a CEL function that exists nowhere — it is in no stdlib
|
|
883
|
+
registry and on no contract — so the predicate faults at runtime, and a
|
|
884
|
+
field-level `visibleWhen` fault is fail-**open**: `resolveFieldRuleState`
|
|
885
|
+
evaluates visibility with `fallback: true`, so the element the author wrote the
|
|
886
|
+
predicate to hide is shown to everyone who copies the page. That is not a
|
|
887
|
+
hypothetical shape — a shipped doc taught it (#11034 fixed the instance).
|
|
888
|
+
|
|
889
|
+
`check:doc-formula-expressions` gains this as a third scan surface rather than a
|
|
890
|
+
second gate, because two gates with opinions about one contract is the thing
|
|
891
|
+
Prime Directive #12 exists to prevent. The verdict is imported whole: syntax, the
|
|
892
|
+
unknown-function catch and the bare-reference rule come from
|
|
893
|
+
`@objectstack/formula`'s `validateExpression`, and the closed-root rule comes
|
|
894
|
+
from `fieldRuleRootIssue` — the same two the metadata walk applies to the same
|
|
895
|
+
slot, in the same order, in the same words.
|
|
896
|
+
|
|
897
|
+
**The layer is decided first, and a layer that cannot be decided is skipped and
|
|
898
|
+
printed.** `visibleWhen` is one key spelling several unrelated contracts, and the
|
|
899
|
+
binding root really does differ: an object field binds `record` + `previous`
|
|
900
|
+
(+ `parent`), a per-option predicate binds `record` plus the host predicate scope
|
|
901
|
+
including `current_user`, a page component binds the user roots and `app`, and a
|
|
902
|
+
flow-screen field **flattens its own field names to top level**. A gate keyed on
|
|
903
|
+
the key alone would have gone red on
|
|
904
|
+
`content/docs/automation/flows.mdx`'s correct `visibleWhen:
|
|
905
|
+
'createOpportunity == true'` and on `content/docs/ui/pages.mdx`'s correct
|
|
906
|
+
`'sales_manager' in current_user.positions` — and a gate whose reds are wrong is
|
|
907
|
+
worse than no gate, because it teaches people to add ignores.
|
|
908
|
+
|
|
909
|
+
So admission is structural and schema-backed, never keyed on the key: a
|
|
910
|
+
`Field.*({ … })` factory call, or a raw field definition carrying `type:` inside
|
|
911
|
+
an object-literal `fields:` **map**. The map-versus-array test is the load-bearing
|
|
912
|
+
half and it is read off the schemas — `ObjectSchema.fields` is
|
|
913
|
+
`z.record(name, FieldSchema)` while `FormFieldSchema` and `ScreenFieldConfigSchema`
|
|
914
|
+
are both `z.array(…)`, so a `fields:` map is the object-field layer and nothing
|
|
915
|
+
else, and a `fields:` array is exactly the case that cannot be told apart.
|
|
916
|
+
|
|
917
|
+
**The skip list is printed and counted on every run, including green ones.** A
|
|
918
|
+
gate that skips in silence is the same false-green one level up, so the summary
|
|
919
|
+
names every skipped site and why. Measured on the corpus as it stands: 23
|
|
920
|
+
text-level `*When:` occurrences, of which 13 are admitted and judged, 7 are
|
|
921
|
+
listed as skipped, and 3 are the ADR quoting `field.zod.ts`'s schema
|
|
922
|
+
(`visibleWhen: ExpressionInputSchema.optional()`) rather than authoring a
|
|
923
|
+
predicate. Three of those seven were invisible to an AST-only walk — a bare
|
|
924
|
+
`visibleWhen: "…"` line at statement position is a labelled statement, not a
|
|
925
|
+
property — so a text-level tripwire reconciles the two counts and any site the
|
|
926
|
+
parser never surfaced is listed rather than dropped.
|
|
927
|
+
|
|
928
|
+
`@objectstack/lint` newly exports `fieldRuleRootIssue` and
|
|
929
|
+
`FIELD_RULE_BOUND_ROOTS`. The field-rule root decision was a closure inside
|
|
930
|
+
`validateStackExpressions` — correct while it had one caller, and exactly how a
|
|
931
|
+
second caller comes to own a dialect of a rule instead of the rule. Behaviour is
|
|
932
|
+
unchanged: the metadata walk now calls the extracted function and its 2271 tests
|
|
933
|
+
pass untouched.
|
|
934
|
+
- 12e306a: The canonical-expression-envelope detector for raw-literal `Page` exports gets a shared home in `@objectstack/lint` (#11480). New public API beside `walkPageComponents`: `auditPageExpressionEnvelopes(page, label)` runs the three parse doors (`PageSchema` / `PageComponentSchema` / `ComponentPropsMap`) over one authored page and reports bare-expression findings plus every door's precondition failures; `renderBareExpressionFindings(findings)` renders the actionable red; types `BareExpressionFinding`, `PageEnvelopeAudit`, `EnvelopeAuditDoor`. The detector previously lived package-local to `@objectstack/platform-objects`' gate, which could not reach raw-literal pages shipped by other packages. `@objectstack/cloud-connection`'s two shipped pages are now covered by the same gate, and `MarketplaceInstalledPage` is declared `: Page` (type-level only; no runtime change) so export-shape page discovery sees it.
|
|
935
|
+
- 661275d: security lint: report a `controlled_by_parent` object whose master is decided by FIELD DECLARATION ORDER (#14747)
|
|
936
|
+
|
|
937
|
+
`SecurityPlugin.resolveCbpRelation` resolves the master a `controlled_by_parent`
|
|
938
|
+
object derives record-level access from through three tiers — a required
|
|
939
|
+
`master_detail`, then any `master_detail`, then a required `lookup` — and picks
|
|
940
|
+
inside a tier with `Array.prototype.find`. So when two or more candidates sit in
|
|
941
|
+
the tier that wins, the master is whichever one the field map happens to list
|
|
942
|
+
first. Measured on a real kernel: an object declaring two required lookups
|
|
943
|
+
resolved its security master to the first-declared one, and swapping the two
|
|
944
|
+
field declarations — nothing else — repointed every row's record-level access
|
|
945
|
+
to the other object. Nothing reported it: not `os validate`, not `os lint`, not
|
|
946
|
+
a boot warning.
|
|
947
|
+
|
|
948
|
+
New error id **`security-controlled-by-parent-ambiguous-relation`**, the mirror
|
|
949
|
+
image of `security-controlled-by-parent-no-relation` (#7503): that one reports
|
|
950
|
+
ZERO candidates, this one reports two or more. The message names every
|
|
951
|
+
candidate — field, type and master — in declaration order, says which tier was
|
|
952
|
+
tested, and says which candidate wins today and therefore which object access
|
|
953
|
+
derives from right now.
|
|
954
|
+
|
|
955
|
+
Only the **winning** tier is judged, and that is not a shortcut: the runtime's
|
|
956
|
+
`??` chain stops at the first tier that resolves, so a tie in a lower tier is
|
|
957
|
+
masked by a higher tier's single winner and is not a decision the platform ever
|
|
958
|
+
makes. An object with one required `master_detail` and two required lookups is
|
|
959
|
+
silent, and stays silent.
|
|
960
|
+
|
|
961
|
+
`error` rather than advisory, for the inverse of the usual reason. The other
|
|
962
|
+
error rules in this linter mirror a hard runtime refusal; this one has none to
|
|
963
|
+
mirror precisely BECAUSE the runtime does not refuse — it silently picks — so
|
|
964
|
+
author time is the only place the ambiguity can ever surface. What it does meet
|
|
965
|
+
is the admissibility bar the #7503 rule states: a self-contained property of the
|
|
966
|
+
object document, no per-permission-set nuance to adjudicate, and no legitimate
|
|
967
|
+
reading, since two tied candidates is not an author saying which master they
|
|
968
|
+
meant.
|
|
969
|
+
|
|
970
|
+
This **narrows the accept set of a gating rule** — error findings fail
|
|
971
|
+
`os validate` / `os compile`. Measured over the shipped corpus: the three
|
|
972
|
+
`controlled_by_parent` objects in the example apps (`showcase_invoice_line`,
|
|
973
|
+
`showcase_expense_line`, `crm_opportunity_line_item`) plus the 27
|
|
974
|
+
`ObjectSchema.create` sites the `check:doc-security-posture` gate reads across
|
|
975
|
+
226 marked prose blocks — **0 findings before and 0 after**. Each of the three
|
|
976
|
+
declares exactly one required `master_detail`, so tier 1 wins with a single
|
|
977
|
+
candidate. `showcase_invoice_line` is the interesting one: it also carries a
|
|
978
|
+
required `lookup`, and the rule is silent because that tie-free lower tier is
|
|
979
|
+
never reached.
|
|
980
|
+
|
|
981
|
+
No runtime behaviour changes. `resolveCbpRelation` in this package now reads its
|
|
982
|
+
tiers from one shared table so the two rules cannot disagree about which tier
|
|
983
|
+
wins, and its answer is unchanged by construction: `find` over a tier is the
|
|
984
|
+
first element `filter` over that tier keeps. The mirror's one deliberate
|
|
985
|
+
divergence from the runtime is kept — `reference` is the only spelling accepted
|
|
986
|
+
here (#5017), so a field carrying the rejected `reference_to` alias is not a
|
|
987
|
+
candidate and cannot create a tie.
|
|
988
|
+
- 9b30cc1: `lintLivenessProperties` no longer crashes on the `live-elsewhere` verdict — and never tells an author to remove a key a sibling repo enforces
|
|
989
|
+
|
|
990
|
+
`describe()` in `lint-liveness-properties.ts` knew three verdicts
|
|
991
|
+
(`experimental`, `planned`, `dead`) and threw, loudly and by design, on any
|
|
992
|
+
other. #13483 then shipped the ledger's fifth status — `live-elsewhere`: dead
|
|
993
|
+
HERE by measurement, genuinely enforced in a sibling repo — and migrated
|
|
994
|
+
`manifest.runtime` onto it (its enforcer is the cloud marketplace publish
|
|
995
|
+
gate). Nothing taught `describe()` about it, so the day any `live-elsewhere`
|
|
996
|
+
row opts into `authorWarn: true`, `os lint` would raise that
|
|
997
|
+
shipped-ledger-integrity error instead of the advisory warning the author
|
|
998
|
+
should get. No shipped row carries `authorWarn` today, so this was a fuse
|
|
999
|
+
rather than a fire.
|
|
1000
|
+
|
|
1001
|
+
`describe()` now has a fourth branch. `live-elsewhere` gets its own rule id —
|
|
1002
|
+
`liveness-live-elsewhere-property`, exported as `LIVENESS_LIVE_ELSEWHERE_PROPERTY`
|
|
1003
|
+
beside `LIVENESS_DEAD_PROPERTY` / `LIVENESS_EXPERIMENTAL_PROPERTY` /
|
|
1004
|
+
`LIVENESS_PLANNED_PROPERTY` and advisory-only like them — plus its own message
|
|
1005
|
+
(`is enforced in a sibling repo, not here`) and its own default hint, which keeps
|
|
1006
|
+
the property and points at the ledger row's `evidence` for the enforcer. It
|
|
1007
|
+
deliberately does **not** reuse the `dead` branch: that is the #11384 lesson,
|
|
1008
|
+
which is that verdicts imply OPPOSITE author actions, and "Remove it" is the
|
|
1009
|
+
single most damaging sentence available about a key whose enforcement is real
|
|
1010
|
+
and remote — deleting it tears out a live gate's input. The sentinel throw
|
|
1011
|
+
stays for genuinely unknown statuses, with its enumeration of the known ones
|
|
1012
|
+
updated.
|
|
1013
|
+
|
|
1014
|
+
The suite gains a coverage pin derived from the shipped ledgers rather than from
|
|
1015
|
+
a hand-written list: every distinct `status` those ledgers actually carry must be
|
|
1016
|
+
answered by `describe()` with a rule id of its own, or (for `live`, which reaches
|
|
1017
|
+
`describe()` only through a ledger-authoring mistake) must still fail loud. A
|
|
1018
|
+
sixth status now fails that pin by name instead of waiting for an author to trip
|
|
1019
|
+
the sentinel.
|
|
1020
|
+
- 8d3f093: fix(lint): refuse a list view's dotted field reference at author time where the runtime door refuses it (#14282)
|
|
1021
|
+
|
|
1022
|
+
An accept-set narrowing on `validateListViewFieldRefs`, the #14107 rule — shipped
|
|
1023
|
+
as `minor`, matching the level that landing and the two family landings before it
|
|
1024
|
+
(#14105, #14148) were given.
|
|
1025
|
+
|
|
1026
|
+
#14107 judges only the HEAD segment of a list-view field reference, so a dotted
|
|
1027
|
+
path whose head resolves to a real relationship field (`columns: [{ field:
|
|
1028
|
+
'owner.name' }]`) passed `os validate` and `os build` clean while every query
|
|
1029
|
+
door a list view reaches refuses it by name. That half was recorded in the rule's
|
|
1030
|
+
docblock and pinned in tests rather than closed, because its failure mode is the
|
|
1031
|
+
opposite of the silent-blank class #14107 gates: a loud `400 INVALID_FIELD` on
|
|
1032
|
+
the first fetch. This is the ruled resolution of that half, as a second finding
|
|
1033
|
+
class with its own id, `list-view-field-dotted`, so one class can be suppressed
|
|
1034
|
+
or filtered without silencing the other (the convention
|
|
1035
|
+
`validate-sortable-fields` and `validate-searchable-fields` already follow).
|
|
1036
|
+
|
|
1037
|
+
The class is scoped by the DOOR, not by the position table, because some
|
|
1038
|
+
list-view positions are read client-side out of the fetched row and walk a dotted
|
|
1039
|
+
path perfectly well:
|
|
1040
|
+
|
|
1041
|
+
- **Projection** — `columns[]`, in both authored spellings. Clients build the
|
|
1042
|
+
`$select` projection from them, and both doors refuse a dotted entry
|
|
1043
|
+
unconditionally (`assertProjectionHasNoDottedPaths` on the engine boundary,
|
|
1044
|
+
`assertProjectionFieldsExist` at the REST ingress).
|
|
1045
|
+
- **Filter** — the view's `filter`, its `tabs[].filter`, its
|
|
1046
|
+
`userFilters.tabs[].filter`, and the two positions declaring which names an end
|
|
1047
|
+
user may filter on (`filterableFields`, `userFilters.fields`). Here the rule
|
|
1048
|
+
asks the same `classifyDottedFilterHead` the runtime doors ask, so the #8371
|
|
1049
|
+
carve-outs the doors serve — structured/JSON heads, array-valued heads, heads
|
|
1050
|
+
whose type is unreadable — are NOT refused at author time.
|
|
1051
|
+
|
|
1052
|
+
Deliberately excluded, each measured rather than assumed:
|
|
1053
|
+
`gantt.quickFilters[].field` and `gantt.tooltipFields[]`, which the renderer
|
|
1054
|
+
resolves IN MEMORY over already-fetched rows through walkers that split on `.`
|
|
1055
|
+
(the spec describes the former as "Record field / dot-path", and the measurement
|
|
1056
|
+
agreed); and every renderer binding that reaches no query door, which stays
|
|
1057
|
+
unjudged rather than acquiring a verdict nobody measured.
|
|
1058
|
+
|
|
1059
|
+
Existing behaviour is untouched: a dotted path whose head resolves to nothing
|
|
1060
|
+
still reports `list-view-field-unknown`, `sort[]` keeps its owner, and the
|
|
1061
|
+
shipped example corpus was measured at zero findings both before and after.
|
|
1062
|
+
- aca23ab: feat(lint): resolve a list view's field references at validate/build (#14107)
|
|
1063
|
+
|
|
1064
|
+
Accept-set narrowing, `minor` under the family precedent (#14105, #14148).
|
|
1065
|
+
|
|
1066
|
+
A list view names fields in more than twenty places and **none of them was
|
|
1067
|
+
resolved against the bound object** — not by `os validate`, and not by `os
|
|
1068
|
+
build`, which is the publish gate. Measured on `@objectstack/cli` 17.2.0 from a
|
|
1069
|
+
real app, each mutation applied on its own and confirmed on disk: a
|
|
1070
|
+
`columns[].field`, a `filter[].field`, a `grouping.fields[].field`, a
|
|
1071
|
+
`kanban.groupByField` and a `gantt.startDateField` naming a field that does not
|
|
1072
|
+
exist all left `os validate` at `valid: true, warnings: []` and `os build` at
|
|
1073
|
+
exit 0, `✓ Build complete`.
|
|
1074
|
+
|
|
1075
|
+
Each one fails silently at render, in the way ADR-0078 and the
|
|
1076
|
+
`view/layout-without-binding` rule already treat as worth gating: a bad column
|
|
1077
|
+
renders blanks, a bad filter key is sent to the engine and matches nothing (an
|
|
1078
|
+
empty list indistinguishable from a true zero), a bad gantt start date leaves a
|
|
1079
|
+
blank chart, a bad kanban group-by collapses every card into the uncolumned
|
|
1080
|
+
bucket. The platform already shipped the *harder* half of this check —
|
|
1081
|
+
`view/layout-without-binding` warns when a binding block is **absent**; a block
|
|
1082
|
+
that is present but points at a field that does not exist reaches the identical
|
|
1083
|
+
end state and got nothing.
|
|
1084
|
+
|
|
1085
|
+
The new rule `list-view-field-unknown` (`validateListViewFieldRefs`, a member of
|
|
1086
|
+
the reference-integrity suite, so it runs on `validate` / `lint` / `compile` and
|
|
1087
|
+
on `view` per-write publish snapshots) resolves every field-naming position on a
|
|
1088
|
+
list view against the object graph:
|
|
1089
|
+
|
|
1090
|
+
- `columns[]` (bare-string and `{ field }` forms, plus `summary.field` and
|
|
1091
|
+
`prefix.field`), `filter[]` keys, `tabs[].filter[]` keys, `grouping.fields[]`,
|
|
1092
|
+
`rowColor.field`, `userFilters.fields[]`, `userFilters.tabs[].filter[]` keys,
|
|
1093
|
+
`filterableFields[]`, `hiddenFields[]`, `fieldOrder[]`;
|
|
1094
|
+
- every field binding inside the `kanban`, `calendar`, `gantt`, `timeline`,
|
|
1095
|
+
`gallery`, `map` and `tree` blocks.
|
|
1096
|
+
|
|
1097
|
+
`sort[]` and `searchableFields[]` are deliberately untouched — they already have
|
|
1098
|
+
owners (`sort-field-unknown` #9257, `searchable-field-unknown` #6674/#4830),
|
|
1099
|
+
each with a runtime-admissibility verdict on top of existence.
|
|
1100
|
+
|
|
1101
|
+
Two severity tiers, the `validateFlowTemplatePaths` precedent: `error` where the
|
|
1102
|
+
miss changes the data the view returns or collapses the layout it configures
|
|
1103
|
+
(every position in the card's measured table), `warning` where the renderer
|
|
1104
|
+
drops one decoration and renders the rest (optional colour/title/tooltip/cover
|
|
1105
|
+
bindings, a stale `hiddenFields` or `fieldOrder` entry).
|
|
1106
|
+
|
|
1107
|
+
Resolution goes through the shared `object-graph.ts` seam (#14105/#14148) — no
|
|
1108
|
+
second field-resolution implementation — and judges the **head segment** of a
|
|
1109
|
+
dotted reference rather than walking relationship hops: a list view compiles no
|
|
1110
|
+
joins, and all three query axes it reaches refuse a dotted path by name
|
|
1111
|
+
(`assertProjectionFieldsExist` #7532 / `assertProjectionHasNoDottedPaths` #7589,
|
|
1112
|
+
the #8371 dotted filter door, `assertSortFieldsExist` #6994). This is strictly
|
|
1113
|
+
wider than "skip dotted paths": `ownr.name` is now reported, where a skip would
|
|
1114
|
+
have passed it.
|
|
1115
|
+
|
|
1116
|
+
**Migration.** A list view refused by the new rule names a field the bound
|
|
1117
|
+
object does not have: correct the spelling (the finding carries a "did you mean"
|
|
1118
|
+
and the object's field list) or drop the entry. The three standard skips apply —
|
|
1119
|
+
an object this stack does not define, an object with no readable field map
|
|
1120
|
+
(ADR-0015 `external`), and registry-injected system columns — plus a fourth on
|
|
1121
|
+
this surface: a list view whose `data.provider` is not `object`.
|
|
1122
|
+
- a39b02a: Resolve an app navigation entry's `viewName` against its object's list views at `validate` and `build`
|
|
1123
|
+
|
|
1124
|
+
`AppNavigationItemSchema.viewName` is documented as *"Default list view to open"*, so an
|
|
1125
|
+
unresolvable name never failed — it **fell back**. A nav entry keeping its authored label and
|
|
1126
|
+
icon would open a different view, and nothing said so: `os validate --json` reported
|
|
1127
|
+
`valid: true` and `os build` was green. The decay mode was worse than the typo mode — renaming
|
|
1128
|
+
a list view silently degraded every nav entry pointing at it, with every gate green and the
|
|
1129
|
+
diff reading correctly in review.
|
|
1130
|
+
|
|
1131
|
+
`lintViewRefs` now walks `app.navigation` (and the `areas[]` container) recursively and reports
|
|
1132
|
+
`view-ref-nav-view-missing` as an **error** when a `viewName` resolves to no list view on the
|
|
1133
|
+
object it names. This extends #2554's existing rule to the second, more travelled door into the
|
|
1134
|
+
same `listViews` namespace rather than adding a new rule class.
|
|
1135
|
+
|
|
1136
|
+
Resolution mirrors the runtime matcher (objectui's `resolveViewId`) in all three directions —
|
|
1137
|
+
exact id, short name retried as `<object>.<name>`, and qualified name with the prefix stripped —
|
|
1138
|
+
so a name that works at runtime is never reported. The accept set narrows only where the stack
|
|
1139
|
+
itself declares the object's list views: an entry is skipped when the `viewName` is interpolated,
|
|
1140
|
+
when `recordId` is set (the schema documents `viewName` as ignored there), when the item carries
|
|
1141
|
+
`requiresObject`, or when this stack contributed no list view for that object.
|
|
1142
|
+
- 225e769: Author-time rejection for unknown `PageComponentSchema.type` strings inside the spec's own namespaces — the type-vocabulary half of the "Component Placeholder" gap.
|
|
1143
|
+
|
|
1144
|
+
`PageComponentSchema.type` is `z.union([PageComponentType, z.string()])`, and the open string arm is deliberate: custom and registered components (`object-grid`, `mcp:connect-agent`, `custom.widget`, kebab SDUI blocks) keep parsing exactly as before — nothing about the parse changed. What is new is that the spec now answers for its own namespaces (`page:` `record:` `nav:` `global:` `user:` `ai:` `app:` `element:`, derived from the enum): a type inside them that the vocabulary does not declare is refused at author time by the new gating rule `component-type-unknown` (`os validate` / `os build` / `os lint`), with the closest declared spellings suggested. Previously `global:serch` validated clean and the published page drew a literal "Component Placeholder" scaffold in front of the end user.
|
|
1145
|
+
|
|
1146
|
+
- `@objectstack/spec` exports the vocabulary claim from `@objectstack/spec/ui`: `RESERVED_COMPONENT_TYPE_NAMESPACES` (derived), `KNOWN_COMPONENT_TYPES` / `KNOWN_COMPONENT_TYPE_CANDIDATES`, `STRING_ARM_REGISTERED_TYPES` (the evidenced ledger of registered-but-row-less types, currently `record:line_items`), and the `hasReservedComponentNamespace` / `isKnownComponentType` predicates.
|
|
1147
|
+
- `@objectstack/lint` ships `validateComponentTypes` (rule id `component-type-unknown`, severity `error`) on all three CLI commands; the runtime publish door is deliberately deferred pending a measured false-refusal budget over stored tenant page rows.
|
|
1148
|
+
|
|
1149
|
+
If a page authored a type in a reserved namespace that nothing declares, the fix is the rule's own hint: rename to the suggested declared type, or move a genuinely custom component to its own namespace (e.g. `my-plugin:widget`) so it cannot be mistaken for platform vocabulary.
|
|
1150
|
+
- e38da2b: feat(lint): resolve an interface page's whitelisted visualizations at validate/build (#14073)
|
|
1151
|
+
|
|
1152
|
+
Accept-set narrowing, `minor` under the family precedent (#14107, #14105, #14148).
|
|
1153
|
+
|
|
1154
|
+
An interface `list` page whitelists renderers with
|
|
1155
|
+
`interfaceConfig.appearance.allowedVisualizations`, and `InterfacePageConfigSchema`
|
|
1156
|
+
is a closed shape with **no per-visualization binding key at all** — no
|
|
1157
|
+
`calendar:`, no `kanban:`, no `map:`. So #13817's parse-time refinement, which
|
|
1158
|
+
demands a `calendar:` block on a list VIEW that whitelists `calendar`, was
|
|
1159
|
+
correctly not extended to this door: a requirement the page surface cannot
|
|
1160
|
+
satisfy would be unauthorable.
|
|
1161
|
+
|
|
1162
|
+
That left the page door with no check of any kind. Measured on objectui
|
|
1163
|
+
`f0f774b0` (after objectui#7029 removed the invented `due_date` default), the
|
|
1164
|
+
renderer derives each binding from the source object's fields
|
|
1165
|
+
(`InterfaceListPage.tsx`, `view.<viz> ?? deriveFromObject(objectDef)`), and when
|
|
1166
|
+
nothing derives there are exactly two outcomes, neither of which reaches the
|
|
1167
|
+
author:
|
|
1168
|
+
|
|
1169
|
+
- the entry LEADS the whitelist — it becomes the page's forced view type, is
|
|
1170
|
+
force-pushed into the switcher's resolvable set, and every visitor lands on
|
|
1171
|
+
the renderer's "Calendar configuration required" refusal screen;
|
|
1172
|
+
- the entry is anywhere else — it is filtered out of the switcher **silently**,
|
|
1173
|
+
while the switcher chrome still appears (it is shown on whitelist length).
|
|
1174
|
+
|
|
1175
|
+
The new rule `page/visualization-without-binding`
|
|
1176
|
+
(`validatePageVisualizationBindings`, a member of the reference-integrity suite,
|
|
1177
|
+
so it runs on `validate` / `lint` / `compile`) asks the renderer's own question
|
|
1178
|
+
at authoring time. For every `list` page, each whitelisted visualization must be
|
|
1179
|
+
either derivable from the source object's declared fields — using the SAME
|
|
1180
|
+
predicates the renderer applies, the field TYPE first and then the NAME regex
|
|
1181
|
+
fallback (kanban: select-like type or status-like name; calendar and timeline: a
|
|
1182
|
+
date-typed non-hidden non-system field, else a date-like name; gallery:
|
|
1183
|
+
image-typed, no name leg; gantt: two distinct date fields; map: location-typed or
|
|
1184
|
+
a geo-like name) — or bound by the block of the list view the page references
|
|
1185
|
+
through `sourceView` (a `calendar:` block also binds a `timeline`, which is what
|
|
1186
|
+
`resolveTimelineDateBinding` accepts). `grid` always passes.
|
|
1187
|
+
|
|
1188
|
+
Severity tracks what the visitor sees: **`error`** when the unbound entry is
|
|
1189
|
+
`allowedVisualizations[0]`, **`warning`** otherwise. Every message names
|
|
1190
|
+
`sourceView` as the remedy, because on this door it is the one schema-legal
|
|
1191
|
+
channel for a per-visualization binding — exactly how the shipped showcase map
|
|
1192
|
+
page binds its `locationField`.
|
|
1193
|
+
|
|
1194
|
+
Deliberately NOT stricter than the renderer: mirroring both predicates rather
|
|
1195
|
+
than the type half alone means a page whose only date is a text field called
|
|
1196
|
+
`due_date` still passes, because it still renders. The mirrored table is exported
|
|
1197
|
+
(`OBJECTUI_DERIVATION_PREDICATES`) and pinned verbatim by a fixture test, and the
|
|
1198
|
+
seven shipped `showcase_task` interface pages are pinned as a live regression
|
|
1199
|
+
corpus — the two halves catch drift on either side of a mirror no build edge
|
|
1200
|
+
connects. `chart` and `tree` get no verdict: the renderer derives no binding for
|
|
1201
|
+
them on this seam, so the rule says nothing rather than guessing.
|
|
1202
|
+
|
|
1203
|
+
**Migration.** A page reported by the new rule whitelists a visualization that
|
|
1204
|
+
renders nothing: point the page at a list view that declares the block
|
|
1205
|
+
(`interfaceConfig.sourceView`), give the source object a field the derivation can
|
|
1206
|
+
find, or drop the entry from `appearance.allowedVisualizations`. Four skips keep
|
|
1207
|
+
it quiet where it cannot know: a page that is not `type: 'list'`, an object this
|
|
1208
|
+
stack does not define, an object with no readable field map (ADR-0015
|
|
1209
|
+
`external`), and a `sourceView` naming a view this stack does not declare (the
|
|
1210
|
+
runtime hydrates stored view bodies over the network).
|
|
1211
|
+
|
|
1212
|
+
No `packages/spec` change — the page surface stays closed, which is what ADR-0047
|
|
1213
|
+
§7 open question 3 asks for.
|
|
1214
|
+
- 5383fa6: React-tier vocabulary converges on the metadata-tier spelling, deprecate-first (#11284, maintainer ruling 2026-08-23). `<ListView>`'s canonical bindings are now the spec ListView schema's own props: `data={{ provider: 'object', object: '…' }}` for the object binding (objectui#2890 A6) and `type` for the visualization kind. `objectName` and `viewType` remain published and accepted as deprecated aliases for the whole deprecation window — nothing is removed in this release — with the deprecation visible at authoring time: `[DEPRECATED → …]` markers in the generated react-blocks contract, and a new `react-prop-deprecated` lint warning (never an error) on every use of a deprecated spelling. The lint accepted either spelling as satisfying `<ListView>`'s required binding and resolved field-name props (`columns`, `searchableFields`, filter positions, …) against the object bound by whichever spelling is present, canonical winning when both are — true at this entry's own commit `5383fa6`, and already false in the release that published it. `22b0081` (#15025) landed before the 17.3.0 version cut and tightened the lint back to what actually renders, so as released here every clause is the other way round: a `<ListView>` bound only by `data={{ provider: 'object', object }}` does NOT satisfy the required binding and `os validate` refuses it, field-name props resolve against `objectName` only, and `objectName` is the spelling that decides when a page carries both, because it is the one the renderer queries. `objectName` / `viewType` are therefore the spellings to write. `<ObjectForm>` / `<ObjectChart>` `objectName` are unchanged: the form's spec counterpart is explicitly not 1:1 (objectui#2890 Scope B), and the chart has no metadata-tier object binding to converge on (charts bind through a dashboard `dataset` there — see chart.zod.ts guidance). Removal of the deprecated aliases was described here as a later card after the deprecation window; that is no longer the plan. The maintainer ruling of 2026-09-07 on #14791 — the card that measured the missing consumer half — retires `objectName` / `viewType` with no deprecation window at all, leaving `data` / `type` as the only spellings. objectui has since landed the renderer fold the canonical spelling needed (present at the pinned `.objectui-sha`, `packages/core/src/utils/normalize-list-view.ts`), and the spec-side retirement is authored as PR #16777, still draft and awaiting a maintainer merge — so within this release the deprecated spellings remain the working ones. (Corrected after publication, #15026.)
|
|
1215
|
+
- 46b53a2: Add `validateReadonlyActionWrites` — an author-time warning on an action body writing a `readonlyWhen` field through `ctx.api`.
|
|
1216
|
+
|
|
1217
|
+
The action surface is the third write surface in the readonly family, after `flow-update-readonly-field` and `hook-api-update-readonly-field`, and it is the one where the family's answer differs. An action body's `ctx.api` is `createContext({ ...callerEnvelope, isSystem: true })` — elevated by design, so RLS/FLS-bypassing trusted execution is the documented posture — and the engine's **static** readonly strip runs only for non-system callers. Measured against a real engine over a memory driver:
|
|
1218
|
+
|
|
1219
|
+
| channel | static `readonly` | `readonlyWhen`, predicate TRUE |
|
|
1220
|
+
| --- | --- | --- |
|
|
1221
|
+
| action body `ctx.api` | lands | **stripped** |
|
|
1222
|
+
| hook body `ctx.api`, non-system trigger | stripped | stripped |
|
|
1223
|
+
| `ctx.api.sudo()` | lands | **stripped** |
|
|
1224
|
+
|
|
1225
|
+
So exactly one shape is a silent no-op on this surface, and that is what the new rule reports:
|
|
1226
|
+
|
|
1227
|
+
- `action-api-update-readonly-when-field` — **warning**. A literal `ctx.api.object('…').update()` / `.updateById()` in an action body writing a field the named object declares `readonlyWhen`. The conditional strip takes no `isSystem` exemption, so elevation is not a workaround and the hint does not offer one: confirm the call only targets records whose predicate is FALSE, or derive the field in a `beforeUpdate` hook on the target object (a hook-written value is not caller-supplied and does land).
|
|
1228
|
+
|
|
1229
|
+
A static-`readonly` counterpart is deliberately **not** shipped: an elevated action write lands on such a field, so the finding would state a falsehood and, at the hook rule's `error` grade, would gate a build over working code.
|
|
1230
|
+
|
|
1231
|
+
Wired through `REFERENCE_INTEGRITY_RULES`, so it runs on `os validate`, `os lint` and `os compile` at once. It reuses the existing machinery rather than adding any: `buildReadonlyIndex` from the flow rule for the field metadata, and `collectActionBodies` from the action rule for the body walk (both registration sites, with the merged-action de-duplication that walk owns).
|
|
1232
|
+
|
|
1233
|
+
`ctx.record` is excluded from the match set, and that exclusion is the rule's load-bearing decision: an action's `ctx.record` is a dead snapshot the runtime never writes back, so no readonly strip is ever consulted on it and a readonly verdict there would be false on every occurrence. `action-record-write-discarded` already owns that shape and states its real reason. Also skipped, each for a stated reason: `insert` / `create` (INSERT is exempt from both strips), `ctx.input` writes (an action's `ctx.input` is its params bag), dynamic object names, non-literal payloads, objects this stack does not declare, fields the object does not declare, and `id` in an `update` payload (the row address, not a field write).
|
|
1234
|
+
- 36d2878: Add `validateReadonlyHookWrites` — an author-time gate on a hook body writing a `readonly` field through `ctx.api`.
|
|
1235
|
+
|
|
1236
|
+
A hook's `ctx.api` is a `ScopedContext` over the **triggering** operation's execution context, so `ctx.api.object('x').update({ someReadonlyField })` reaches the engine as an ordinary non-system caller and the update path strips the key. The call returns success, the step looks clean, and the column is simply always null — a failure only an end-to-end read-back detects. This completes the hook side of the flow-side gate that shipped as `flow-update-readonly-field`.
|
|
1237
|
+
|
|
1238
|
+
Two new rule ids, wired through `REFERENCE_INTEGRITY_RULES` so they run on `os validate`, `os lint` and `os compile`:
|
|
1239
|
+
|
|
1240
|
+
- `hook-api-update-readonly-field` — **error**. A literal `ctx.api.object('…').update()` / `.updateById()` writing a field the named object declares `readonly: true`.
|
|
1241
|
+
- `hook-api-update-readonly-when-field` — **warning**. The same write against a `readonlyWhen` field, which strips per record state.
|
|
1242
|
+
|
|
1243
|
+
The rule keys on the write **channel**, not on the field, so the correct and widely used pairing is untouched: a `beforeInsert`/`beforeUpdate` body stamping `ctx.input.<field> = …` writes a server value that survives the strip and is **never** flagged. Also skipped, each for a stated reason: `ctx.api.sudo()` chains (elevated — the intended channel), `insert`/`create` (INSERT is engine-exempt), dynamic object names, non-literal payloads, objects this stack does not declare, fields the object does not declare, and `id` in an `update` payload (the row address, not a field write).
|
|
1244
|
+
- 39404f3: feat(spec,lint): a layout section can reference a declared field group instead of copying its members (#13855)
|
|
1245
|
+
|
|
1246
|
+
Additive accept widening. Maintainer ruling 2026-08-31 (option B on #13855):
|
|
1247
|
+
「直接处理b」.
|
|
1248
|
+
|
|
1249
|
+
ADR-0085 makes `fieldGroups` + `Field.group` the canonical grouping, assembled in
|
|
1250
|
+
one place — `deriveFieldGroupLayout` (ADR-0085 §5). The two layout escape hatches
|
|
1251
|
+
were whole-takeover shapes with no way back to it: a custom record page's
|
|
1252
|
+
`record:details` `properties.sections` and a view-level `form.sections` each
|
|
1253
|
+
enumerated their members by hand, so an author who reached for either had to
|
|
1254
|
+
hand-copy the same membership fact a second and third time. Nothing linked the
|
|
1255
|
+
copies to the declaration, so every field added to the object afterwards made
|
|
1256
|
+
them quietly staler — measured on a real app as three disagreeing groupings of
|
|
1257
|
+
one object, with the detail page missing two fields the form showed.
|
|
1258
|
+
|
|
1259
|
+
**A section may now name the group instead.** On both surfaces:
|
|
1260
|
+
|
|
1261
|
+
```ts
|
|
1262
|
+
sections: [
|
|
1263
|
+
{ group: 'contact_info' }, // members + presentation derived
|
|
1264
|
+
{ label: 'Notes', fields: ['note'] }, // enumerated, unchanged
|
|
1265
|
+
]
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
Members (every visible field whose `Field.group` points at the key, in
|
|
1269
|
+
field-declaration order) and the group's own presentation (label, icon,
|
|
1270
|
+
description, `collapse`, `visibleWhen`, and the drop when a group has no visible
|
|
1271
|
+
members) all come from `deriveFieldGroupLayout`. Nothing is re-implemented in
|
|
1272
|
+
section land.
|
|
1273
|
+
|
|
1274
|
+
**The mixing rule**, declared once for both surfaces and pinned:
|
|
1275
|
+
|
|
1276
|
+
- `group` and `fields` are mutually exclusive; a section declaring neither is
|
|
1277
|
+
refused (before this change it was unrepresentable, because `fields` was
|
|
1278
|
+
required).
|
|
1279
|
+
- A group-referencing section carries no key the group already declares —
|
|
1280
|
+
`name`, `label`, `icon`/`description`, the collapse pair, `visibleWhen` (and
|
|
1281
|
+
its deprecated `visibleOn` spelling) are refused beside `group`, each with the
|
|
1282
|
+
pointer to the `fieldGroups` entry that owns it. Not a precedence rule: the
|
|
1283
|
+
absence of one. The surface keys the group says nothing about — `columns`,
|
|
1284
|
+
`pane`, `hideEmpty`, `showBorder`, `headerColor` — ride alongside as usual.
|
|
1285
|
+
- Across sections, both kinds coexist in declared array order; a
|
|
1286
|
+
group-referencing section occupies one slot and expands in place.
|
|
1287
|
+
- ⛔ Not on a wizard step: a group carries `visibleWhen` and `collapse`, and a
|
|
1288
|
+
wizard step has no slot for either (the #13704 refusals, reached through the
|
|
1289
|
+
object's declaration instead of the step's own keys).
|
|
1290
|
+
|
|
1291
|
+
**Existence is checked by reference diagnostics, not at parse.** The key names
|
|
1292
|
+
something on a different schema, so the spec door takes any well-formed
|
|
1293
|
+
snake_case key — the `UserFilterFieldSchema.field` precedent. `@objectstack/lint`
|
|
1294
|
+
reports a dangling one as `page-section-group-unknown` (`record:details`) or
|
|
1295
|
+
`form-section-group-unknown` (form views, both the canonical `sections` and the
|
|
1296
|
+
legacy `groups` bucket), advisory like every other dangling-reference finding in
|
|
1297
|
+
that family, with the object's declared groups listed in the hint.
|
|
1298
|
+
|
|
1299
|
+
The key grammar is now single-sourced as `FIELD_GROUP_KEY_PATTERN` beside the
|
|
1300
|
+
derivation, so the declaring surface (`ObjectFieldGroupSchema.key`) and the two
|
|
1301
|
+
referencing surfaces cannot drift into accepting different keys.
|
|
1302
|
+
|
|
1303
|
+
**Type-surface note for consumers.** `fields` becomes optional on both section
|
|
1304
|
+
shapes (that is what makes `group` the other way to declare the same fact), so
|
|
1305
|
+
`z.infer` now types it `… | undefined`. A consumer that reads `section.fields`
|
|
1306
|
+
unconditionally must handle the reference form; every in-repo reader already
|
|
1307
|
+
guards it. No authored metadata changes shape, and nothing that parsed before
|
|
1308
|
+
stops parsing — `fields: []` included.
|
|
1309
|
+
|
|
1310
|
+
The renderer half (objectui) is tracked separately; until it lands, a
|
|
1311
|
+
group-referencing section is declared and diagnosed but not yet rendered.
|
|
1312
|
+
- 06ee8bf: Warn when a bare identifier in a flow node/edge condition is shadowed by a declared flow variable
|
|
1313
|
+
|
|
1314
|
+
A flow `condition` is evaluated in a flattened scope, so a bare `status` normally
|
|
1315
|
+
resolves to the trigger record's field and is the correct, canon-taught spelling.
|
|
1316
|
+
`objectstack validate` deliberately never judged a bare identifier there, and it
|
|
1317
|
+
still does not — with one exception it now names.
|
|
1318
|
+
|
|
1319
|
+
When the same name is BOTH a declared flow variable and a field on the bound
|
|
1320
|
+
object, the two collide silently: a run seeds its declared variables first and
|
|
1321
|
+
flattens the record's fields only where nothing is bound yet, so the variable
|
|
1322
|
+
wins, the field is unreachable under its own name, and nothing anywhere reports
|
|
1323
|
+
it. The author reads `status` and gets the variable. On this surface that is the
|
|
1324
|
+
least visible failure there is — a flow condition that never fires produces no
|
|
1325
|
+
record, no error and no log line.
|
|
1326
|
+
|
|
1327
|
+
`validateStackExpressions` now emits a `warning` (never an error) on exactly that
|
|
1328
|
+
case, naming the mechanism and both repairs: `record.status` for the field, or
|
|
1329
|
+
rename the variable. A bare name that is only a field, or only a variable, stays
|
|
1330
|
+
silent as before.
|
|
1331
|
+
|
|
1332
|
+
The variable set is collected across every ADR-0031 region of the flow, since a
|
|
1333
|
+
run holds one variable map: flow-level declarations, loop/map iterator and index
|
|
1334
|
+
variables, the try/catch error variable, node output variables, assignment
|
|
1335
|
+
targets in all three shapes the executor accepts (including a legacy assignment
|
|
1336
|
+
node with no `assignments` wrapper, whose top-level config keys are the variable
|
|
1337
|
+
names), and node ids, which are bare CEL roots at runtime.
|
|
1338
|
+
- d23dc08: feat(spec,lint,metadata-protocol): a `page` member on the `view` type enum — mount an already-published page on an object view (#13216)
|
|
1339
|
+
|
|
1340
|
+
A custom page created and published at runtime through the metadata API had no
|
|
1341
|
+
in-protocol way to reach an end user (#13100's evidence map). App navigation is
|
|
1342
|
+
closed to runtime content (`app.allowOrgOverride: false`), and the `view` `type`
|
|
1343
|
+
enum — on one of the five types the platform deliberately leaves open
|
|
1344
|
+
(`allowOrgOverride: true`, `allowRuntimeCreate: true`) — was closed over
|
|
1345
|
+
declarative row renderers, so a published page could not be mounted as an
|
|
1346
|
+
object's list view or tab.
|
|
1347
|
+
|
|
1348
|
+
Maintainer ruling 2026-08-29 (live director session, verbatim 「同意」), 方向 1:
|
|
1349
|
+
|
|
1350
|
+
> `view` 的 `type` 枚举新增 `page` 成员——对象的列表视图/标签页可挂载一个已发布页面。走平台**有意开着**的门(`view` 本就 `allowOrgOverride=true` + 运行时可创建),零新增授权面;设计要点:`page` 型 view 需声明 `pageName` 绑定,校验目标页面存在,渲染委托既有页面渲染器
|
|
1351
|
+
|
|
1352
|
+
**Zero new authorization surface, as the ruling's basis requires.** Nothing in
|
|
1353
|
+
this change touches a metadata type's `allowOrgOverride` / `allowRuntimeCreate`
|
|
1354
|
+
flags, adds a write door, or adds a read door. A `page` view is a `view` written
|
|
1355
|
+
through the door `view` already opens, and it holds a NAME — the page itself is
|
|
1356
|
+
still fetched through the page read path it already had, and still renders
|
|
1357
|
+
through the existing page renderer, so the page's own audience gate
|
|
1358
|
+
(`page.assignedProfiles`) rides along unchanged. Delegation is what preserves
|
|
1359
|
+
that: a second renderer is what would have introduced a second gate.
|
|
1360
|
+
|
|
1361
|
+
**The binding, refused in both directions at parse.** `ListViewSchema` gains
|
|
1362
|
+
`pageName`, declared with `SnakeCaseIdentifierSchema` — the same grammar
|
|
1363
|
+
`PageSchema.name` carries, so the accepted set is exactly the set of strings that
|
|
1364
|
+
could name a page. `checkListViewPageMount` then refuses:
|
|
1365
|
+
|
|
1366
|
+
- `type: 'page'` with no `pageName` — unlike every other view type there is no
|
|
1367
|
+
degraded rendering to fall back to, so the view would be blank;
|
|
1368
|
+
- `pageName` on any other view type — the accepted-and-ignored shape;
|
|
1369
|
+
- a non-empty `columns` beside a page mount — `columns` is the one required key
|
|
1370
|
+
on a list view, and the only truthful value for a page mount is `[]`.
|
|
1371
|
+
|
|
1372
|
+
The check is attached at all three list-view doors (`ListViewSchema`,
|
|
1373
|
+
`ObjectListViewSchema`, and the flattened runtime overlay behind
|
|
1374
|
+
`PUT /api/v1/meta/view`), with a pinned test that fails if any attachment is
|
|
1375
|
+
dropped.
|
|
1376
|
+
|
|
1377
|
+
**Existence of the target page** is answered where the collection is visible:
|
|
1378
|
+
`defineStack`'s `validateCrossReferences` refuses at build time (same
|
|
1379
|
+
`pageNames.size > 0` policy the two other page references in that function
|
|
1380
|
+
already use), and the new `@objectstack/lint` rule `view-page-unresolved`
|
|
1381
|
+
(`validateViewPageRefs`) resolves it on `os validate` / `os lint` / `os compile`
|
|
1382
|
+
**and** at the runtime publish gate. Advisory, not gating, for its nav twin's
|
|
1383
|
+
reason: with no curated cross-package page registry, "unresolved here" cannot be
|
|
1384
|
+
told apart from "provided by a package this stack cannot see".
|
|
1385
|
+
|
|
1386
|
+
Reaching the runtime publish gate needed the per-write snapshot to carry the
|
|
1387
|
+
`pages` collection (`RuntimeStackContext.pages`, threaded through
|
|
1388
|
+
`evaluateRuntimeAuthoringGate` and read off the live registry in
|
|
1389
|
+
`saveMetaItem`'s gate call). That is the one-key widening `RuntimeStackContext`
|
|
1390
|
+
documents, made when a rule that reads the collection crossed the wall — never
|
|
1391
|
+
in advance — and the false-positive channel it closes is measured both ways in
|
|
1392
|
+
`runtime-gate.view-page-refs.test.ts`. `pages` joined `NAME_KEYED_STACK_KEYS` in
|
|
1393
|
+
the same edit, because a collection that is both context-filled and
|
|
1394
|
+
write-targeted must have its finding paths name-keyed (#10064).
|
|
1395
|
+
|
|
1396
|
+
**Downstream note (not an accept-set narrowing).** No previously valid metadata
|
|
1397
|
+
becomes invalid: `pageName` is a new key and `page` a new enum member, so every
|
|
1398
|
+
refusal above can only fire on a document that could not be written before.
|
|
1399
|
+
What does change for a downstream schema author is composition: `ListViewSchema`
|
|
1400
|
+
now carries a refinement, and zod 4 refuses `.omit()` / key-overwriting
|
|
1401
|
+
`.extend()` on a refined object. The unrefined shape stays module-private
|
|
1402
|
+
(publishing it would mint a duplicate protocol def and a second full set of
|
|
1403
|
+
ratcheted authorable-surface keys), so a consumer that derived from
|
|
1404
|
+
`ListViewSchema` by omission should compose with `.safeExtend()` or narrow after
|
|
1405
|
+
parsing. `FormViewSchema` has had this property since its own refinement landed,
|
|
1406
|
+
so this is the established shape for view schemas rather than a new one.
|
|
1407
|
+
|
|
1408
|
+
**Deliberately out of scope**, per the same ruling: 方向 2 (registering app
|
|
1409
|
+
navigation at publish time) is deferred to its own design card — it would
|
|
1410
|
+
require reversing the `app.allowOrgOverride: false` authorization decision — and
|
|
1411
|
+
with it the known limitation the ruling accepts on the record, that a page
|
|
1412
|
+
belonging to no object still has no browse-to entry. `page` is also NOT added to
|
|
1413
|
+
`VisualizationTypeSchema`: the switcher offers alternative ways to draw the same
|
|
1414
|
+
rows, and a page draws none.
|
|
1415
|
+
- 038f333: feat(lint,formula): refuse a visibility predicate that calls a function the CEL environment does not register (#13594)
|
|
1416
|
+
|
|
1417
|
+
An accept-set narrowing at `objectstack validate` and at the runtime publish
|
|
1418
|
+
door, ruled by the maintainer on 2026-08-31 (director batch #21) on a censused
|
|
1419
|
+
premise.
|
|
1420
|
+
|
|
1421
|
+
**The hole.** `validate-visibility-predicates` — the gate that judges
|
|
1422
|
+
`visibleWhen` on view form sections/fields and page components — was
|
|
1423
|
+
deliberately parse-only. So a predicate that parses perfectly and calls a
|
|
1424
|
+
function that does not exist passed CLEAN, measured side by side with two
|
|
1425
|
+
controls that fired:
|
|
1426
|
+
|
|
1427
|
+
```text
|
|
1428
|
+
source lint gate (before) validateExpression
|
|
1429
|
+
totallyBogusFn(1,2) CLEAN ok=false
|
|
1430
|
+
record.x.nosuchmethod('a') CLEAN ok=false
|
|
1431
|
+
country === "USA" syntax ok=false <- control
|
|
1432
|
+
status == 'active' bare-identifier ok=true <- control
|
|
1433
|
+
```
|
|
1434
|
+
|
|
1435
|
+
The runtime fault it hides is the worst-shaped one the platform has: on a view
|
|
1436
|
+
or page surface it falls OPEN (the element renders unconditionally, identical
|
|
1437
|
+
to carrying no predicate at all), and on an action surface — evaluated with
|
|
1438
|
+
`throwOnError: true` — it falls CLOSED, so the action disappears for every
|
|
1439
|
+
user *including one who holds the grant*, behind a single deduped
|
|
1440
|
+
`console.warn` (objectui#4421). A plausible-looking function name that does not
|
|
1441
|
+
exist is exactly what a generator invents.
|
|
1442
|
+
|
|
1443
|
+
**What changed.**
|
|
1444
|
+
|
|
1445
|
+
- `@objectstack/formula` publishes `firstUnknownFunctionCall(source)` — the
|
|
1446
|
+
function-EXISTENCE verdict, isolated from everything else cel-js's `check()`
|
|
1447
|
+
has an opinion about. The oracle is the evaluation environment's own
|
|
1448
|
+
registration set, read through the same `buildEnv` seam `celEngine.compile`
|
|
1449
|
+
and `celEngine.evaluate` build with — never the advertised
|
|
1450
|
+
`CEL_STDLIB_FUNCTIONS` catalog, which lists 35 of the 72 registered names and
|
|
1451
|
+
would have refused 37 functions that resolve and evaluate today (`type`,
|
|
1452
|
+
`map`, `filter`, `split`, `getFullYear`, `json`, …).
|
|
1453
|
+
- `@objectstack/lint` gains `visibility-predicate-unknown-function`
|
|
1454
|
+
(**error**), covering both call forms — global (`totallyBogusFn(1,2)`) and
|
|
1455
|
+
receiver/member (`record.x.nosuchmethod('a')`). The message quotes the
|
|
1456
|
+
engine's own `found no matching overload for '…'` verbatim so publish time
|
|
1457
|
+
and run time read as one system, and offers **no** "did you mean" suggestion:
|
|
1458
|
+
nearest-name matching over the function namespace was measured to answer
|
|
1459
|
+
`min` for `can`.
|
|
1460
|
+
|
|
1461
|
+
**Scoped supersession, not a widening.** The module's parse-only ruling stands
|
|
1462
|
+
for everything except function existence. A registered name called with wrong
|
|
1463
|
+
arguments (`upper(1, 2)`), a registered name called in the wrong position
|
|
1464
|
+
(bare `split('a,b')`), the CEL-type blind spot (`type == 'grid'`) and every
|
|
1465
|
+
operator-overload fault (`1 + 'a'`) are all still unreported — `type(record.x)
|
|
1466
|
+
== string` and every other legal `dyn` predicate is untouched. Refusing an
|
|
1467
|
+
unregistered call cannot be a false positive: the validation and runtime
|
|
1468
|
+
environments are the same builder (53 probes, 0 divergence), so the call this
|
|
1469
|
+
refuses is a call that would have faulted.
|
|
1470
|
+
|
|
1471
|
+
**Migration.** A refused predicate names a function that does not exist and
|
|
1472
|
+
never evaluated — replace it with an advertised callable, or precompute the
|
|
1473
|
+
value into a formula field on the object and test that field. The census found
|
|
1474
|
+
**0** host-registered extra CEL functions across the reachable corpus
|
|
1475
|
+
(objectstack `packages/`/`examples/`/`apps/`, objectui, one shipped host app),
|
|
1476
|
+
with a firing positive control, and this repo's `examples/**` and `apps/**`
|
|
1477
|
+
sweep produces **0** new refusals. `cloud`, `objectos` and published
|
|
1478
|
+
third-party apps were NOT MEASURED — if a host in one of those registers extra
|
|
1479
|
+
CEL functions, its predicates are refused; that gap was declared before the
|
|
1480
|
+
ruling and accepted with it.
|
|
1481
|
+
- fa1eca3: Resolve a dashboard widget's OWN `filter` keys and `options.sortBy` at validate/build
|
|
1482
|
+
|
|
1483
|
+
A dashboard widget could filter by a column that does not exist, and order by a name
|
|
1484
|
+
it never selected, and `objectstack validate` exited 0 with "Validation passed";
|
|
1485
|
+
`build` — the publish gate — wrote the dashboard into `dist/objectstack.json`. The
|
|
1486
|
+
widget then rendered **empty**.
|
|
1487
|
+
|
|
1488
|
+
The surrounding surface was already covered, which is what made the two misses so
|
|
1489
|
+
narrow: `widget-dataset-unknown`, `widget-dimension-unknown`, `widget-measure-unknown`,
|
|
1490
|
+
`filter-token-unknown` and `dashboard-filter-field-unknown` all failed both gates on the
|
|
1491
|
+
same dashboard. On the very same node, the filter TOKEN was checked and the filter
|
|
1492
|
+
COLUMN was not — `filter-token-unknown` fires path-precise at
|
|
1493
|
+
`…widgets[4].filter.due_date.$lte`, so the traversal already walked the filter tree and
|
|
1494
|
+
already knew the widget's dataset. Only the key resolution was missing. And
|
|
1495
|
+
`options.sortBy` was declared-≠-enforced in the plainest way available: the spec states
|
|
1496
|
+
the contract in its own prose — *"must be one this widget actually selects"* — and
|
|
1497
|
+
nothing enforced it.
|
|
1498
|
+
|
|
1499
|
+
Why this class of miss is expensive rather than untidy, in the reporter's words: the
|
|
1500
|
+
dashboard it was measured on leads with a "not moving" tile — open work untouched more
|
|
1501
|
+
than 14 days — and *"an empty tile is indistinguishable from a healthy team: a missing
|
|
1502
|
+
number reads as zero, and zero is the answer the manager is hoping for."* The failure is
|
|
1503
|
+
silent in the direction the reader wants to believe.
|
|
1504
|
+
|
|
1505
|
+
Three gating rule ids, all at the site that already emits `widget-dataset-unknown` /
|
|
1506
|
+
`dashboard-filter-field-unknown`, and all failing **`validate` and `build`** (pinned
|
|
1507
|
+
end-to-end, not inferred from the registry entry):
|
|
1508
|
+
|
|
1509
|
+
- `widget-filter-field-unknown` — a key of the widget's own `filter` resolves to no
|
|
1510
|
+
column on the bound dataset's object graph. Reported path-precise at
|
|
1511
|
+
`dashboards[i].widgets[j].filter.<key>`, matching `filter-token-unknown`'s precision in
|
|
1512
|
+
that same subtree.
|
|
1513
|
+
- `widget-filter-field-not-included` — the key resolves, but its relationship prefix is
|
|
1514
|
+
not declared in the dataset's `include`, so ADR-0021 compiles no join and the column is
|
|
1515
|
+
out of the query's reach.
|
|
1516
|
+
- `widget-sortby-unselected` — `options.sortBy` names neither a `dimensions[]` nor a
|
|
1517
|
+
`values[]` entry of the widget. A name the dataset declares but the widget did not
|
|
1518
|
+
select gets its own message, because the fix is a selection rather than a spelling.
|
|
1519
|
+
|
|
1520
|
+
**A dotted path through a declared `include` is RESOLVED, not skipped.** A widget's
|
|
1521
|
+
`filter` is ANDed into the dataset query as `runtimeFilter`, and that compiled query
|
|
1522
|
+
carries only the joins `include` declared — so the same two clauses the dataset rule
|
|
1523
|
+
applies one level down (existence, then joinability) apply here. The runtime is not a
|
|
1524
|
+
backstop for the second: the dataset compiler's `assertDeclared` runs over `dimensions`
|
|
1525
|
+
and `measures` only, never over `runtimeFilter`.
|
|
1526
|
+
|
|
1527
|
+
Built on the seams that shipped with the dataset-level sibling rather than a second
|
|
1528
|
+
implementation: `walkFilterFieldKeys` (all three authored filter shapes) and
|
|
1529
|
+
`indexObjectGraph` / `resolveFieldPath`. Two helpers that were local to that rule —
|
|
1530
|
+
`joinablePrefixes` and `describeFieldPathVerdict` — moved into the shared seam and are
|
|
1531
|
+
now exported, because both are answers this position asks identically and copying either
|
|
1532
|
+
would have been the second implementation the seam exists to prevent.
|
|
1533
|
+
|
|
1534
|
+
Minor rather than patch: this narrows the accept set. Metadata that built yesterday and
|
|
1535
|
+
names a column or an order that does not exist now fails the build — which is the point.
|
|
1536
|
+
The three skips every field-existence rule in this package takes are unchanged, so an
|
|
1537
|
+
object the stack does not define, an ADR-0015 `external` object with no readable field
|
|
1538
|
+
map, and a registry-injected system column are never reported.
|
|
1539
|
+
|
|
1540
|
+
### Patch Changes
|
|
1541
|
+
|
|
1542
|
+
- e5ed943: fix(lint): guard `collectBare`'s recursion so a self-referential page terminates instead of killing the stack (#13235)
|
|
1543
|
+
|
|
1544
|
+
`page-envelope-audit`'s `collectBare` is a lockstep raw/parsed value walker,
|
|
1545
|
+
separate from the shared `walkPageComponents` traversal, and it carried no cycle
|
|
1546
|
+
guard. A page whose component tree contains itself (`A -> B -> A` through
|
|
1547
|
+
`properties.children`) is input the schema **admits** — `properties` is
|
|
1548
|
+
`z.record(z.unknown())` and `properties.children` is `z.array(z.unknown())`, so
|
|
1549
|
+
`PageSchema.safeParse` succeeds — and door 1 then recursed until
|
|
1550
|
+
`RangeError: Maximum call stack size exceeded`. Door 1 runs over the whole page
|
|
1551
|
+
before any other door, so this is the first thing that died on such a page.
|
|
1552
|
+
|
|
1553
|
+
The guard is an **ancestor set on the authored side**: objects are added on
|
|
1554
|
+
entry to the descent and removed on exit, so a node is skipped only when it is
|
|
1555
|
+
its own ancestor. Two consequences are pinned by tests:
|
|
1556
|
+
|
|
1557
|
+
- **Report-neutral on acyclic input, by construction.** No node is ever its own
|
|
1558
|
+
ancestor on an acyclic page, so the guard never fires and findings are
|
|
1559
|
+
unchanged. A visited-set would instead have skipped merely *shared* subtrees —
|
|
1560
|
+
the same component literal referenced from two slots is legal authoring — and
|
|
1561
|
+
silently dropped their findings.
|
|
1562
|
+
- **Nothing distinct is lost on a cyclic page.** Every node of a finite graph is
|
|
1563
|
+
reachable by a simple path, so each authored position is still visited; only
|
|
1564
|
+
the infinite tail of re-reports at ever-longer paths is dropped.
|
|
1565
|
+
|
|
1566
|
+
Cycles through arrays are covered as well as cycles through records.
|
|
1567
|
+
|
|
1568
|
+
Scope: this is door 1 only. `walkPageComponents` carries its own separate
|
|
1569
|
+
unguarded recursion, so `auditPageExpressionEnvelopes` end-to-end still dies at
|
|
1570
|
+
the walk on the same input until that lands (#13217). No published type changes
|
|
1571
|
+
and no accept/reject behaviour changes on any page that parses today.
|
|
1572
|
+
- 834da6f: lint: `dashboard-filter-field-unknown` resolves dotted dashboard-filter fields on the object graph, and answers system columns per object
|
|
1573
|
+
|
|
1574
|
+
A dashboard-level filter (`dateRange`, or a `globalFilters[]` entry) is ANDed into
|
|
1575
|
+
**every** widget's analytics query, so its effective field — after any
|
|
1576
|
+
`filterBindings` re-target — has to resolve on each bound widget's dataset object.
|
|
1577
|
+
The rule that enforces that shipped with two holes, and this closes both by
|
|
1578
|
+
migrating the check onto the shared `resolveFieldPath` / `joinablePrefixes` seam
|
|
1579
|
+
the widget's own `filter` keys already use one position over.
|
|
1580
|
+
|
|
1581
|
+
- **Dotted paths are no longer skipped.** The branch carried
|
|
1582
|
+
`if (field.includes('.')) continue;`, accurate when nothing in the package could
|
|
1583
|
+
walk relationship hops and false since the object-graph seam landed. A filter
|
|
1584
|
+
re-targeted to `account.signed_at` was unjudged whether or not `account` existed,
|
|
1585
|
+
whether or not `signed_at` existed on it, and whether or not `account` was
|
|
1586
|
+
declared in the dataset's `include`. It is now walked hop by hop, and a miss
|
|
1587
|
+
names **which** hop failed.
|
|
1588
|
+
- **System columns are resolved per object, not through the flat union.** The old
|
|
1589
|
+
test was `objectFields.has(field) || SYSTEM_FIELDS.has(field)`, which answers
|
|
1590
|
+
"could this be a system column *anywhere*". On an `ownership: 'none'` object the
|
|
1591
|
+
platform injects no `owner_id`, and on `systemFields: { audit: false }` no
|
|
1592
|
+
`created_at` — both were answered as resolvable and are now reported.
|
|
1593
|
+
|
|
1594
|
+
New error id **`dashboard-filter-field-not-included`**: the effective field
|
|
1595
|
+
resolves, but its relationship prefix is not declared in the bound dataset's
|
|
1596
|
+
`include`, so ADR-0021 compiles no join and the column is out of the broadcast
|
|
1597
|
+
query's reach. It mirrors `widget-filter-field-not-included` one level down, and is
|
|
1598
|
+
its own id because the fix is a different edit (declare the join, versus point the
|
|
1599
|
+
filter at something real).
|
|
1600
|
+
|
|
1601
|
+
This **narrows the accept set of a shipped gating rule**. Both new answers are
|
|
1602
|
+
error-tier, so like the rule's other errors they fail `os validate` / `os build`
|
|
1603
|
+
and the runtime publish gate for `dashboard` writes. Measured over the shipped
|
|
1604
|
+
dashboard corpus — the three example apps plus the platform's own
|
|
1605
|
+
`system_overview` — the change is 0 findings before and 0 after; the
|
|
1606
|
+
`dashboard-filter-field-unprovisioned` warning is unchanged and now travels with
|
|
1607
|
+
the verdict, so it answers a dotted path landing on an ADR-0015 `external` object
|
|
1608
|
+
too.
|
|
1609
|
+
- a392dbf: fix(ai): author the CANONICAL agent id everywhere the platform teaches one — Studio's pin, the MCP prompt example, and the lint's value roster (#14461)
|
|
1610
|
+
|
|
1611
|
+
`skills/objectstack-ai` tells authors that `data_chat` and `metadata_assistant`
|
|
1612
|
+
"are **not** vocabulary — always write `ask` / `build`". The platform then
|
|
1613
|
+
taught the opposite from every live example it ships. Nothing was broken at
|
|
1614
|
+
runtime; what was wrong is what an author copies.
|
|
1615
|
+
|
|
1616
|
+
**Studio's pin.** `studio.app.ts` was the repo's ONLY `app.defaultAgent` usage,
|
|
1617
|
+
and it spelled the alias:
|
|
1618
|
+
|
|
1619
|
+
```
|
|
1620
|
+
- defaultAgent: 'metadata_assistant',
|
|
1621
|
+
+ defaultAgent: 'build',
|
|
1622
|
+
```
|
|
1623
|
+
|
|
1624
|
+
The triage card left this undecidable — if the cloud plugin registered the
|
|
1625
|
+
agent under the legacy id, re-pinning would be a behaviour change in a
|
|
1626
|
+
consumer this repo cannot see. Measured instead of assumed, at `cloud`
|
|
1627
|
+
`main@3856fbf7`: `service-ai-studio/src/agents/metadata-assistant-agent.ts:12,40`
|
|
1628
|
+
ships the record as `name: BUILD_AGENT_NAME` = `'build'`, and `plugin.ts:58`
|
|
1629
|
+
registers `metadata_assistant` as a **one-way, resolution-only** legacy alias.
|
|
1630
|
+
The canonical id *is* `build`; the old pin reached it by detour.
|
|
1631
|
+
|
|
1632
|
+
Nor is the re-pin cosmetic. Alias resolution depends on an in-memory
|
|
1633
|
+
`registerAgentAlias` call having run at plugin init, and cloud carries two
|
|
1634
|
+
defensive docblocks about that registration silently no-op'ing for real under
|
|
1635
|
+
bundle load ordering (`service-ai-studio/src/plugin.ts:44-57`,
|
|
1636
|
+
`service-ai/src/agent-runtime.ts:30-41` — "a missed alias must never hide a
|
|
1637
|
+
real platform agent like `build`"). The canonical id never touches the alias
|
|
1638
|
+
table, so this drops a load-order dependency from the platform's own flagship
|
|
1639
|
+
authoring surface. On the UI side nothing moves: `objectui`'s
|
|
1640
|
+
`AGENT_ALIAS_GROUPS` is bidirectional and canonical-first, and
|
|
1641
|
+
`SURFACE_DEFAULT['studio-build']` was already `'build'`.
|
|
1642
|
+
|
|
1643
|
+
**The MCP prompt example.** `mcp-server-runtime.ts`'s `agent_prompt` argument
|
|
1644
|
+
described itself as `'Name of the agent to load (e.g. "data_chat",
|
|
1645
|
+
"metadata_assistant")'` — two retired aliases, neither canonical id present.
|
|
1646
|
+
That string is served to every MCP client asking what to pass, so the one
|
|
1647
|
+
surface that suggests a spelling to an LLM suggested the two the catalogue
|
|
1648
|
+
forbids. Now `(e.g. "ask", "build")`.
|
|
1649
|
+
|
|
1650
|
+
**The lint's value roster.** `validate-ai-agent-authoring`'s `defaultAgent`
|
|
1651
|
+
**value** limb reused the four-name `PLATFORM_AGENT_NAMES` set, so it
|
|
1652
|
+
deliberately passed `metadata_assistant` — the gate that exists to make
|
|
1653
|
+
authoring mistakes loud waved through the exact spelling the catalogue bans,
|
|
1654
|
+
which is the silent-tolerance shape ADR-0078 exists to close, committed by the
|
|
1655
|
+
gate itself. The two limbs now read different tables, because they ask
|
|
1656
|
+
different questions:
|
|
1657
|
+
|
|
1658
|
+
- **declaration limb** — unchanged, still all four names. Declaring
|
|
1659
|
+
`metadata_assistant` shadows the `build` record through the alias exactly as
|
|
1660
|
+
declaring `build` does.
|
|
1661
|
+
- **value limb** — canonical `ask` / `build` only. A legacy alias gets its own
|
|
1662
|
+
rule id `default-agent-legacy-alias` (exported) and its own wording, because
|
|
1663
|
+
an alias **resolves** (the app gets the agent it meant — a spelling defect)
|
|
1664
|
+
while an unknown name does **not** (the pin is inert). Describing the alias
|
|
1665
|
+
as "no effect" would send an author hunting a bug that is not there.
|
|
1666
|
+
|
|
1667
|
+
Both of the #6041 ruling's operative decisions are kept intact: still
|
|
1668
|
+
`warning` tier, still no Zod enum narrowing. `defaultAgent: 'metadata_assistant'`
|
|
1669
|
+
keeps parsing, building, and resolving — the only change is that authoring it
|
|
1670
|
+
now says so.
|
|
1671
|
+
|
|
1672
|
+
Not breaking: nothing an author can write was removed, and both aliases stay
|
|
1673
|
+
resolvable for old bookmarks and persisted `agent_id`s, which is the only job
|
|
1674
|
+
ADR-0063 §2 ever gave them.
|
|
1675
|
+
- 948dd6b: `defineStack` now refuses a stack that declares an auto-launched flow while `requires` omits `'triggers'` (#14153) — **BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes.
|
|
1676
|
+
|
|
1677
|
+
A `record_change`, `schedule`, `time_relative` or `api` flow fires only when its trigger is mounted, and every one of those triggers ships in `@objectstack/trigger-*` behind ONE capability token, `requires: ['triggers']`. `defineStack` already hard-errors the same declared-capability class for the hierarchy scopes (`unit` / `unit_and_below` / `own_and_reports` need `'hierarchy-security'`), which fail CLOSED when the capability is missing — a user notices the missing rows. The trigger half failed SILENT: the flow registered, `validate` / `typecheck` / `test` / `build` all exited 0, and the automation simply never happened. Measured downstream: an app shipped four correctly-authored flows, zero bound, across five merged rounds, and the only diagnostic was a boot-banner line printed after deploy.
|
|
1678
|
+
|
|
1679
|
+
The refusal lands in the same throw-site family as its sibling (`defineStack trigger capability validation failed (N issue(s)):` with one `✗` line per flow) and reuses the boot audit's own wording — the flow name, the resolved trigger kind, and the exact remedy (`Add requires: ['triggers'] (record_change/schedule/time_relative/api ship in @objectstack/trigger-*)`). An absent `requires` counts as omitting the token: the CLI reads it as `[]` and appends only the always-on slate, which mounts neither `automation` nor `triggers`, so a stack that declares nothing gets no trigger either. Flows whose `status` disables them (`obsolete` / `invalid`) are skipped, exactly as the engine's boot audit skips them. A stack whose flows are all `screen` or hand-launched `autolaunched` owes nothing. The fix for a refused stack is the one line the message names; a flow that was genuinely meant to be launched only by hand declares `type: 'autolaunched'` (or `'screen'`) instead of a trigger it never intended to bind.
|
|
1680
|
+
|
|
1681
|
+
The kind a flow asks for is now one shared derivation, `resolveFlowTriggerKind` (`@objectstack/spec/automation`), the authoring-time mirror of the automation engine's binding chain — same start-node reads, same precedence (a `timeRelative` descriptor outranks its sibling `schedule` cadence). `@objectstack/lint`'s `validate-flow-trigger-readiness` reads it as the auto-triggered predicate behind its draft-status rule, so the two authoring surfaces cannot disagree on which flows auto-launch; its findings are unchanged.
|
|
1682
|
+
|
|
1683
|
+
In-tree corpus: `examples/app-todo` declared two `schedule` flows and a `record_change` flow with no `requires` at all and now declares `requires: ['automation', 'triggers']`.
|
|
1684
|
+
|
|
1685
|
+
<!-- adr-0087: not-required (no-migration-prescription) the refusal message itself names the one-line fix (declare the `triggers` token), and nothing is renamed or removed — no authorable key changes spelling and no export moves, so the ledger has no rewrite to carry. -->
|
|
1686
|
+
- 4301f78: refactor(lint): derive the runtime gate's name-keyed collection set instead of hand-listing it (#13390)
|
|
1687
|
+
|
|
1688
|
+
The set of collections the runtime publish gate carries in a per-write snapshot was
|
|
1689
|
+
written down in five places, and `NAME_KEYED_STACK_KEYS` was the one with no guard of
|
|
1690
|
+
any kind — `CONTEXT_STACK_KEYS` carries a `satisfies` clause, which is validity rather
|
|
1691
|
+
than completeness, and the compiler held nothing else.
|
|
1692
|
+
|
|
1693
|
+
That list carries a real invariant: a collection the CONTEXT fills **and** that some
|
|
1694
|
+
write type maps into must be name-keyed, or a finding's `path` is a positional index
|
|
1695
|
+
into an in-memory snapshot the caller has never seen and cannot enumerate — the defect
|
|
1696
|
+
#10064 fixed for `objects` / `permissions` / `books`. Omitting a member did not fail to
|
|
1697
|
+
build, fail a test, or fail a gate; it produced correct-LOOKING findings with paths the
|
|
1698
|
+
receiver cannot resolve. Adding the `pages` collection had to touch all five spellings
|
|
1699
|
+
and only one of them announced itself.
|
|
1700
|
+
|
|
1701
|
+
`NAME_KEYED_STACK_KEYS` and the `TOP_LEVEL_INDEX` pattern built from it are now derived
|
|
1702
|
+
from the two inputs that already state the answer: `CONTEXT_STACK_KEYS` intersected with
|
|
1703
|
+
the values of `TYPE_TO_STACK_KEY`. The intersection was measured against the list it
|
|
1704
|
+
replaces before anything changed — same four members (`objects`, `permissions`, `books`,
|
|
1705
|
+
`pages`) in the same order, and `datasets` excluded on its own because no write type maps
|
|
1706
|
+
into it, so no member needed a hand-written exception and none is kept.
|
|
1707
|
+
|
|
1708
|
+
Constructive preservation, not a tightening or a loosening: the derived pattern's `source`
|
|
1709
|
+
is byte-identical to the literal it replaces, and the gate returns the same findings for
|
|
1710
|
+
the same inputs. No published entry point changed — `@objectstack/lint` and
|
|
1711
|
+
`@objectstack/lint/runtime` export exactly the names they did before.
|
|
1712
|
+
- e7191ce: fix(build): give each `exports` condition its own `types` target in the 28 dual-build packages (#13112)
|
|
1713
|
+
|
|
1714
|
+
**Published-surface change, zero runtime change.** No emitted byte moves; what
|
|
1715
|
+
moves is which declaration file a resolver READS. Maintainer ruling 2026-08-29
|
|
1716
|
+
(decision batch #3, verbatim 「同意」) chose declaring the files over deleting
|
|
1717
|
+
them.
|
|
1718
|
+
|
|
1719
|
+
## What was wrong
|
|
1720
|
+
|
|
1721
|
+
These 28 packages are `"type": "module"` and dual-built, and each spelled one
|
|
1722
|
+
`types` condition as a **sibling** of `import`/`require`:
|
|
1723
|
+
|
|
1724
|
+
```json
|
|
1725
|
+
"exports": { ".": {
|
|
1726
|
+
"types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs"
|
|
1727
|
+
} }
|
|
1728
|
+
```
|
|
1729
|
+
|
|
1730
|
+
A sibling `types` answers for **both** conditions, so a CommonJS consumer was
|
|
1731
|
+
handed `dist/index.d.ts` — an ES-module declaration, because the package is
|
|
1732
|
+
`"type": "module"` — for an entry point it reaches with `require`. Measured with
|
|
1733
|
+
`tsc --traceResolution` on a `"type": "commonjs"` fixture at `moduleResolution:
|
|
1734
|
+
node16`:
|
|
1735
|
+
|
|
1736
|
+
```
|
|
1737
|
+
error TS1479: The current file is a CommonJS module whose imports will produce
|
|
1738
|
+
'require' calls; however, the referenced file is an ECMAScript module and cannot
|
|
1739
|
+
be imported with 'require'.
|
|
1740
|
+
```
|
|
1741
|
+
|
|
1742
|
+
The JavaScript at `dist/index.cjs` loads perfectly (`check:dual-build-cjs-loads`
|
|
1743
|
+
has asserted that for months). It is the **types** that told the consumer the
|
|
1744
|
+
supported `require` entry point could not be required. The `dist/index.d.cts`
|
|
1745
|
+
twin tsup emits beside it — 36 files, 5,517,701 B on this build — was named by
|
|
1746
|
+
no condition at all and shipped in every tarball unreachable.
|
|
1747
|
+
|
|
1748
|
+
## What changed
|
|
1749
|
+
|
|
1750
|
+
Each condition now names its own declaration, the shape TypeScript documents:
|
|
1751
|
+
|
|
1752
|
+
```json
|
|
1753
|
+
"exports": { ".": {
|
|
1754
|
+
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
1755
|
+
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
|
|
1756
|
+
} }
|
|
1757
|
+
```
|
|
1758
|
+
|
|
1759
|
+
33 entry points across 27 packages, subpaths included. The root `types` field is
|
|
1760
|
+
untouched, so `node10` resolvers are unaffected; the `import` condition resolves
|
|
1761
|
+
exactly what it resolved before, measured as an unchanged control in the same
|
|
1762
|
+
run.
|
|
1763
|
+
|
|
1764
|
+
## `@objectstack/core` is deliberately NOT changed
|
|
1765
|
+
|
|
1766
|
+
Splitting a declaration in two makes TypeScript compare it nominally, and
|
|
1767
|
+
`ObjectKernel` carries a `private plugins` member that reaches every plugin
|
|
1768
|
+
through `PluginContext.getKernel()`. With core split, whole-repo `pnpm build`
|
|
1769
|
+
fails in `@objectstack/verify` with 5 × TS2345 ("Types have separate
|
|
1770
|
+
declarations of a private property 'plugins'"); with core held back and the
|
|
1771
|
+
other 27 split, 71/71 tasks pass. So core keeps the sibling-`types` shape and
|
|
1772
|
+
its two `.d.cts` files (220,854 B) stay unreachable, declared as such in
|
|
1773
|
+
`check:dual-build-cjs-loads`. Splitting it needs a decision about core's public
|
|
1774
|
+
types, not about an exports map.
|
|
1775
|
+
|
|
1776
|
+
## For consumers
|
|
1777
|
+
|
|
1778
|
+
- **ESM consumers: nothing changes.** Same declaration file, byte for byte.
|
|
1779
|
+
- **CJS consumers under `node16`/`nodenext`: TS1479 goes away** and the
|
|
1780
|
+
declarations they get are the ones built for CommonJS.
|
|
1781
|
+
- **`node10` / `moduleResolution: node` consumers: nothing changes** — they never
|
|
1782
|
+
read `exports`.
|
|
1783
|
+
- Nothing is removed: every path that resolved before still resolves.
|
|
1784
|
+
|
|
1785
|
+
Packages that are CJS-first (`require` → `./dist/index.js`, no `"type": "module"`)
|
|
1786
|
+
were already correct and are untouched — their `dist/index.d.ts` really is the
|
|
1787
|
+
CommonJS declaration. Their ESM mirror (an unreachable `.d.mts` under the
|
|
1788
|
+
`import` condition) is a separate, larger population and is filed separately per
|
|
1789
|
+
the ruling, not fixed here.
|
|
1790
|
+
|
|
1791
|
+
`check:dual-build-cjs-loads` grew a fourth invariant (TYPED) that reds on the old
|
|
1792
|
+
shape, so the drift cannot return silently.
|
|
1793
|
+
- 1fb6281: Walk object-nested `list` / `listViews.*` through the view completeness rules.
|
|
1794
|
+
|
|
1795
|
+
`validateFunctionalCompleteness` walked only the top-level `views[]` containers, so
|
|
1796
|
+
a `timeline` / `gantt` / `map` / `tree` view authored on the object itself — the
|
|
1797
|
+
ADR-0017 "Object has-many View" spelling that `objects[].list` and
|
|
1798
|
+
`objects[].listViews.*` carry — never reached `checkViewCompleteness`. Both doors
|
|
1799
|
+
register the same expanded view items and reach the same renderer, so `os validate`
|
|
1800
|
+
and `os build` were silent on exactly the half of the stack the sibling rules
|
|
1801
|
+
(`lint-view-refs`, `validate-list-view-field-refs`) already walk.
|
|
1802
|
+
|
|
1803
|
+
Both authorable object spellings (array-form and name-keyed map) are covered, and a
|
|
1804
|
+
list view's own `data.object` retarget (ADR-0047) resolves the bound object the same
|
|
1805
|
+
way it does on the top-level door — so `view/layout-without-binding` and
|
|
1806
|
+
`view/tree-without-parent-field` now reach the nested door by construction rather
|
|
1807
|
+
than by a second wiring step. Findings report as
|
|
1808
|
+
`object "<name>" › listViews.<key>` / `objects[<i>].listViews.<key>.<block>`.
|
|
1809
|
+
- 7345308: fix(lint): drop the `element:form` entry from `COMPONENT_FIELD_SPECS` (#9249)
|
|
1810
|
+
|
|
1811
|
+
The whole `element:form` element retired at element grain (ADR-0049 — no
|
|
1812
|
+
renderer ever shipped for it; the #9220 shape one element over), so every
|
|
1813
|
+
`ElementFormProps` key is a `retiredKey()` tombstone and no spec-conformant
|
|
1814
|
+
page carries `fields` on it. The field-binding rule's job (resolve a field
|
|
1815
|
+
NAME against the object) is not the question a retired key raises: an authored
|
|
1816
|
+
key is already reported by name with the element-retirement prescription —
|
|
1817
|
+
which names the live replacement, the object-bound `object-form` block —
|
|
1818
|
+
through the #5068 props gate, and the binding entry would only add a second
|
|
1819
|
+
finding about a key that no longer exists — the #5775/#6629 residue class the
|
|
1820
|
+
package's own `component-field-specs-liveness` gate refuses.
|
|
1821
|
+
- 8ed9c54: flows: warn on a `loop` body with a fallible node and no containment, and on a `try_catch` with no `catch` (#14394)
|
|
1822
|
+
|
|
1823
|
+
Two authoring-time rules in the flow anti-pattern family, both `warning`:
|
|
1824
|
+
|
|
1825
|
+
- **`flow-loop-body-uncontained`** — a `loop` whose `body` region runs a node
|
|
1826
|
+
that can end the run (a record read/write, `http`, `notify`,
|
|
1827
|
+
`connector_action`, `script`, `subflow`, `map`, `approval`) with no
|
|
1828
|
+
`try_catch` between the loop and that node. The `loop` executor iterates with
|
|
1829
|
+
a bare `await` and has no `try`/`catch` at all, so the first failing item ends
|
|
1830
|
+
the whole run: later items are never processed, and the work already done is
|
|
1831
|
+
not even reported. The finding names the loop, the node, and the prescribed
|
|
1832
|
+
spelling.
|
|
1833
|
+
- **`flow-try-catch-without-catch`** — the near-miss, and the first target
|
|
1834
|
+
rather than an extra: `catch` is optional in the schema, and omitting it makes
|
|
1835
|
+
the container fail through, so an author who wrapped the node and stopped
|
|
1836
|
+
there gets **zero** containment and previously got no diagnostic either.
|
|
1837
|
+
Measured, the no-`catch` run and the unwrapped control produce identical
|
|
1838
|
+
output; a `retry` policy only delays that.
|
|
1839
|
+
|
|
1840
|
+
Both stay warnings under the family's severity bar: a loop deliberately allowed
|
|
1841
|
+
to stop at the first failure, and a retry-then-fail `try_catch`, are legitimate
|
|
1842
|
+
readings the rule cannot disprove.
|
|
1843
|
+
|
|
1844
|
+
`content/docs/automation/flows.mdx` documents `loop { try_catch { … } }` as the
|
|
1845
|
+
per-iteration containment spelling, with the measured minimal handler — one bare
|
|
1846
|
+
`assignment` node, `edges` and `errorVariable` omitted — and the three `catch`
|
|
1847
|
+
spellings the schema refuses (`catch` omitted gives no containment; `catch: {}`
|
|
1848
|
+
and `catch: { nodes: [] }` are rejected, the region's `nodes` being `.min(1)`).
|
|
1849
|
+
|
|
1850
|
+
No spec, engine or runtime change: the containment capability already exists and
|
|
1851
|
+
was measured working (5 of 5 iterations, items 4-5 processed, run completes).
|
|
1852
|
+
- f887e52: Re-measure four stale `current_user` binding-text sites, including the form SECTION slot
|
|
1853
|
+
|
|
1854
|
+
The claim that `current_user` is unbound on a form-view **section** predicate was true when
|
|
1855
|
+
it was written and is not any more: the console form renderer threads the host shell's
|
|
1856
|
+
predicate scope into `isSectionVisible` (objectui#6110), and the object-view chain now
|
|
1857
|
+
carries an authored `section.visibleWhen` through to an evaluator via the `section-divider`
|
|
1858
|
+
pseudo-field (objectui#6111). Text only — no schema, no verdict and no runtime behaviour
|
|
1859
|
+
moves.
|
|
1860
|
+
|
|
1861
|
+
- `FormSectionSchema.visibleWhen` (`ui/view.zod.ts`) — the JSDoc and `describe()` now say the
|
|
1862
|
+
root resolves, carrying the two qualifications the field-slot text already carried: the
|
|
1863
|
+
binding is **client-side only** (no write-path evaluator reads a form-view section or field
|
|
1864
|
+
`visibleWhen` — the rule validator's list is field `readonlyWhen` / `requiredWhen` and
|
|
1865
|
+
per-option `visibleWhen`), and the scope is **empty on the public `/f/:slug` route**, which
|
|
1866
|
+
is mounted outside any provider on purpose. The `features.*` refusal sentence is unchanged:
|
|
1867
|
+
that root is unbound on both standalone form routes.
|
|
1868
|
+
- `SelectOptionSchema.visibleWhen` (`data/field.zod.ts`) and
|
|
1869
|
+
`SELECT_OPTION_EDITABILITY_GUIDANCE` (`shared/editability-boundary.ts`) — the retired
|
|
1870
|
+
exclusivity claim ("the one `*When` surface where `current_user` resolves") is trimmed. The
|
|
1871
|
+
durable grounding stays and is now what the prescription rests on: per-option is the one
|
|
1872
|
+
visibility predicate the **server** enforces, so the rule validator refuses a write of a
|
|
1873
|
+
value whose predicate is false.
|
|
1874
|
+
- `@objectstack/lint`'s field-rule message — the `visibleWhen` consequence clause is
|
|
1875
|
+
re-measured. Under a scope-publishing host the predicate no longer faults: it resolves, the
|
|
1876
|
+
control is hidden client-side, and the server still returns the value to every other reader
|
|
1877
|
+
— a silent enforcement gap. The fault-open leg survives wherever no host publishes a scope.
|
|
1878
|
+
The verdict is unchanged and the message says why it is now *more* justified: trading a loud
|
|
1879
|
+
lint error for a gap nobody can see is worse than the error.
|
|
1880
|
+
- 8b04c75: fix(cli,lint): stop lowering hook handlers that call `ctx.api.sudo()` into bodies that cannot run it (#14010)
|
|
1881
|
+
|
|
1882
|
+
`ScopedContext.sudo()` is real in-process and is **not** marshalled into the
|
|
1883
|
+
QuickJS sandbox: the VM's `ctx.api` carries `object()` and the transaction
|
|
1884
|
+
surface, and nothing else. Every consumer of that fact had it backwards.
|
|
1885
|
+
|
|
1886
|
+
The failure this closes is the expensive shape, not a cosmetic one. An author
|
|
1887
|
+
writes an inline `handler`, tests it the way the docs teach — calling
|
|
1888
|
+
`hook.handler(ctx)` natively, against the in-process `ScopedContext`, where
|
|
1889
|
+
`sudo()` exists — and the suite is green. `objectstack build` then lowers that
|
|
1890
|
+
same source into an L2 `body`, and in production the call is
|
|
1891
|
+
`TypeError: ctx.api.sudo is not a function`. Under a hook's default
|
|
1892
|
+
`onError: 'abort'` the TypeError aborts the **triggering write**, so the
|
|
1893
|
+
symptom surfaces as an unrelated save being refused. Green tests, dead feature.
|
|
1894
|
+
|
|
1895
|
+
- **`@objectstack/cli`** — `.sudo(` joins `FORBIDDEN_PATTERNS` in
|
|
1896
|
+
`extractHookBody`, so the build declines to emit such a handler as
|
|
1897
|
+
`body.source`. This is a repair, not just a refusal: `lowerCallables` already
|
|
1898
|
+
registers the callable and ships it through the `.mjs` bundle when extraction
|
|
1899
|
+
throws, so the handler keeps running **in-process, where `sudo()` is real**.
|
|
1900
|
+
The build prints the reason; `--strict-body`, which demands a body for every
|
|
1901
|
+
callable, turns it into a hard failure — correctly, since a body needing
|
|
1902
|
+
elevation genuinely cannot be one. Same family as the `crypto.hash`
|
|
1903
|
+
retirement (#4391): a member advertised ahead of its implementation, where
|
|
1904
|
+
build-time inference was the amplifier rather than the safety net.
|
|
1905
|
+
- **`@objectstack/lint`** — `hook-api-update-readonly-field` (severity
|
|
1906
|
+
`error`, gating) and its `readonlyWhen` sibling both *prescribed*
|
|
1907
|
+
`ctx.api.sudo()` as the remedy. That rule reads L2 body sources and nothing
|
|
1908
|
+
else, so the prescribed shape was a TypeError for **100%** of its population:
|
|
1909
|
+
a gating rule pointing at a dead feature. Both hints now name the own-hook
|
|
1910
|
+
stamp and say plainly that `sudo()` is not reachable from a body. The rule's
|
|
1911
|
+
findings, severities and exclusions are unchanged — only the advice.
|
|
1912
|
+
|
|
1913
|
+
Docs: the `readonly` table in `automation/hook-bodies.mdx` claimed the
|
|
1914
|
+
`sudo()` row **Lands**; it now records what actually happens.
|
|
1915
|
+
|
|
1916
|
+
Not addressed here, and the reason this is only half the card: a hook still has
|
|
1917
|
+
**no declared elevation knob** — there is no hook-side `runAs` the way
|
|
1918
|
+
`FlowSchema` has one — so "this column is computed by automation and never
|
|
1919
|
+
hand-written" remains inexpressible whenever the maintaining write is
|
|
1920
|
+
cross-object. That is a contract-surface decision (see #14010), left to the
|
|
1921
|
+
review chain rather than guessed at here.
|
|
1922
|
+
- 33681ea: feat(hooks): `runAs` on a hook — `'system' | 'user' | 'inherit'`, default `'inherit'`
|
|
1923
|
+
|
|
1924
|
+
A hook's `ctx.api` runs with the context of the write that fired it, so a column
|
|
1925
|
+
an app wants **computed and never hand-written** could not be expressed: author
|
|
1926
|
+
`editable: false` for the persona and the direct `PATCH` is refused — and so is
|
|
1927
|
+
the hook that maintains the column, by the same field-level check. The guard and
|
|
1928
|
+
the legitimate writer were the same door. The only elevation a hook had was the
|
|
1929
|
+
in-process `ctx.api.sudo()`, which is not marshalled into the sandbox (a
|
|
1930
|
+
`TypeError` once a build lowers the handler into a body) and which rides the L3
|
|
1931
|
+
bundle path that is being retired.
|
|
1932
|
+
|
|
1933
|
+
`HookSchema` now accepts `runAs`:
|
|
1934
|
+
|
|
1935
|
+
| value | the hook's `ctx.api` data operations run as |
|
|
1936
|
+
| --- | --- |
|
|
1937
|
+
| `'inherit'` (default) | the context of the triggering write — exactly the behaviour every hook has today |
|
|
1938
|
+
| `'system'` | elevated: a full-access, RLS-bypassing system principal |
|
|
1939
|
+
| `'user'` | the triggering user; a hook whose trigger resolved no user has its data operations **refused** (`HOOK_UNSCOPED_DATA_ACCESS`) rather than run unscoped |
|
|
1940
|
+
|
|
1941
|
+
`'system'` and `'user'` mean here exactly what they mean on `flow.runAs` — same
|
|
1942
|
+
word, same semantics. `'inherit'` is the hook-only third value, because only a
|
|
1943
|
+
hook has a context to inherit; a flow establishes its identity from nothing,
|
|
1944
|
+
which is why its default is `'user'` and this one's is `'inherit'`. Nothing on
|
|
1945
|
+
`FlowSchema` changes.
|
|
1946
|
+
|
|
1947
|
+
**Purely additive: no migration, no behaviour change for any existing hook.**
|
|
1948
|
+
The default reproduces today's behaviour by handing the engine-built `ctx.api`
|
|
1949
|
+
through unchanged, and an absent key parses to it.
|
|
1950
|
+
|
|
1951
|
+
Scope, deliberately narrow: `ctx.api` data operations only. `condition`
|
|
1952
|
+
evaluation, the `readonly` strip applied to the hook's own `ctx.input` payload,
|
|
1953
|
+
`ctx.session` and `async` semantics all keep reading the triggering operation's
|
|
1954
|
+
context, and declaring `runAs: 'system'` does not elevate the write that fired
|
|
1955
|
+
the hook.
|
|
1956
|
+
|
|
1957
|
+
Elevation is authorization, not anonymity: a `runAs: 'system'` write still
|
|
1958
|
+
carries the triggering user, so `created_by` / `updated_by` and the audit row
|
|
1959
|
+
still name the operator.
|
|
1960
|
+
|
|
1961
|
+
Honoured on both execution surfaces — the in-process `handler` and the
|
|
1962
|
+
sandboxed `body`.
|
|
1963
|
+
|
|
1964
|
+
Authoring notes:
|
|
1965
|
+
|
|
1966
|
+
- `sudo`, `elevate`, `elevated` and `isSystem` are refused with a prescription
|
|
1967
|
+
naming `runAs`, and `run_as` is answered as a rename.
|
|
1968
|
+
- `@objectstack/lint`'s gating `hook-api-update-readonly-field` rule now skips a
|
|
1969
|
+
hook that declares `runAs: 'system'` — the static `readonly` strip skips a
|
|
1970
|
+
system context, so the write it exists to catch does not happen — and its
|
|
1971
|
+
hints name the knob. The `readonlyWhen` warning is unchanged: a system context
|
|
1972
|
+
does not waive a conditional lock.
|
|
1973
|
+
- 68c5dba: fix(cli,lint): stop `os lint` demanding translation keys the liveness ledger warns authors for writing (#11624)
|
|
1974
|
+
|
|
1975
|
+
`os lint` computes i18n coverage and runs the authoring-rule registry in a
|
|
1976
|
+
single pass over the same stack, and for the `flows` translation group the two
|
|
1977
|
+
halves pointed opposite ways:
|
|
1978
|
+
|
|
1979
|
+
| the author does | which rule fires | what it says |
|
|
1980
|
+
|---|---|---|
|
|
1981
|
+
| omits `flows.*` from the bundle | `i18n/missing-flow` | the key is missing a translation for locale X |
|
|
1982
|
+
| adds it (`os i18n extract` scaffolds it) | `liveness-planned-property` | the `flows` group is `planned` — nothing reads it |
|
|
1983
|
+
|
|
1984
|
+
Measured on one stack, one run: omitting produced **4** `i18n/missing-flow`
|
|
1985
|
+
findings and 0 liveness findings; authoring produced 0 demands and **2**
|
|
1986
|
+
`liveness-planned-property` findings ("sets `flows` but this translation
|
|
1987
|
+
property is planned"). There is no per-rule suppression in `os lint`, only
|
|
1988
|
+
`--skip-i18n`, which silences the entire `i18n/missing-*` family — so the
|
|
1989
|
+
author's only escape cost them every other coverage signal. Under
|
|
1990
|
+
`--i18n-strict` the demand side is an **error**, so a project could be forced
|
|
1991
|
+
to author keys it is then warned for.
|
|
1992
|
+
|
|
1993
|
+
⛔ The warning is not the bug and is unchanged: no shipped screen-flow runner
|
|
1994
|
+
reads the group, so a translated wizard string is stored and never shown — the
|
|
1995
|
+
failure mode `validationMessages` was removed in 17.0.0 for. The premature half
|
|
1996
|
+
is the demand.
|
|
1997
|
+
|
|
1998
|
+
**The fix.** `collectExpectedEntries` — the single definition of what is
|
|
1999
|
+
translatable at all, shared by the coverage gate and the `os i18n extract`
|
|
2000
|
+
skeleton — now leaves out any translation group the liveness ledger warns
|
|
2001
|
+
authors for authoring. It reads that set from `@objectstack/lint`'s new
|
|
2002
|
+
`authorWarnedProperties(type)`, which returns the very warn-map
|
|
2003
|
+
`lintLivenessProperties` iterates, so the demand side and the warn side cannot
|
|
2004
|
+
drift into disagreeing about the same keys again.
|
|
2005
|
+
|
|
2006
|
+
Two properties fall out of reading the ledger rather than switching on `flows`
|
|
2007
|
+
by name: the bucket **turns itself back on** the day an objectui screen-flow
|
|
2008
|
+
runner lands and the row flips to `live` (no flag, no follow-up edit), and any
|
|
2009
|
+
future group that acquires an `authorWarn` is covered on the day it is marked
|
|
2010
|
+
rather than re-opening this collision one group at a time. Today `flows` is the
|
|
2011
|
+
only such group — pinned as an equality so a second one goes red instead of
|
|
2012
|
+
shipping.
|
|
2013
|
+
|
|
2014
|
+
No other bucket changes: `objects`, `apps`, `pages`, `dashboards`,
|
|
2015
|
+
`globalActions` and `metadataForms` are all `live` and are reported exactly as
|
|
2016
|
+
before. `@objectstack/spec` is untouched — the `flows` row keeps `planned` +
|
|
2017
|
+
`authorWarn: true`.
|
|
2018
|
+
- b2eab95: feat(spec,objectql): give three authored display surfaces a bundle key — bulk-action defs, custom validation messages, dataset labels (#14253)
|
|
2019
|
+
|
|
2020
|
+
Purely additive: three new translation groups, one new dispatch-table entry, one
|
|
2021
|
+
new resolution step on the write path. No existing key changes shape, no
|
|
2022
|
+
resolution order changes, and every surface still falls back to the authored
|
|
2023
|
+
literal when the bundle carries nothing.
|
|
2024
|
+
|
|
2025
|
+
Each of the three carried **authored, user-facing display text that no key in
|
|
2026
|
+
`TranslationDataSchema` could reach** — not a drifted key, no key. Each rendered
|
|
2027
|
+
in the source locale inside an otherwise fully translated screen, which is the
|
|
2028
|
+
bad failure mode: it reads as a styling quirk rather than as a missing
|
|
2029
|
+
translation. Measured on a real `zh-CN` deployment.
|
|
2030
|
+
|
|
2031
|
+
**1. A list view's `bulkActionDefs[]`** —
|
|
2032
|
+
`objects.<object>._views.<view>.bulkActions.<def_name>.{label,confirmText,confirmLabel,params.<p>.{label,help,placeholder}}`,
|
|
2033
|
+
resolved in `translateView` against `config.bulkActionDefs` (the one address a
|
|
2034
|
+
served def has: both `ViewItemSchema` and `expandViewContainer` nest the whole
|
|
2035
|
+
ListView under `config`). A def is part of the *view* document, not an action
|
|
2036
|
+
document, so it never reached `translateAction`; the selection bar read
|
|
2037
|
+
`已选择 1 项 · Complete · Skip · 清除`. The def's `label` deliberately stays a
|
|
2038
|
+
plain `z.string()` on the authoring side — the bar renders it as a React child,
|
|
2039
|
+
so an inline locale map would be a blank cell rather than a parse error — and
|
|
2040
|
+
overlaying at the metadata boundary keeps the wire value a plain string. The
|
|
2041
|
+
documented workaround (`bulkActions: ['<name>']`, promoting a declared action)
|
|
2042
|
+
is not equivalent: it is N elevated per-record dispatches instead of one
|
|
2043
|
+
data-plane `updateMany`.
|
|
2044
|
+
|
|
2045
|
+
**2. A custom validation rule's `message`** —
|
|
2046
|
+
`objects.<object>._validations.<rule_name>.message`, spelled by the new
|
|
2047
|
+
`objectValidationMessageKey` and read on the write path by the rule evaluator.
|
|
2048
|
+
⚠️ **This adds a key shape, not a channel**: the lookup runs on the *existing*
|
|
2049
|
+
`i18nService` hook that has localized built-in field-catalog messages and field
|
|
2050
|
+
labels since #3957. Before it, a deployment got platform-generated refusals in
|
|
2051
|
+
the caller's language and author-written refusals in the source language inside
|
|
2052
|
+
one `400 VALIDATION_FAILED` envelope. All five authored-message emitters route
|
|
2053
|
+
through one seat; a nested `conditional` branch is addressed by the branch's own
|
|
2054
|
+
name; a platform-generated rejection (an unevaluable predicate) is deliberately
|
|
2055
|
+
left alone. `messages['validation.field.*']` is unchanged and still overrides the
|
|
2056
|
+
built-in catalog only.
|
|
2057
|
+
|
|
2058
|
+
**3. Dataset labels** — `datasets.<name>.{label,description,dimensions.<d>.label,measures.<m>.label}`
|
|
2059
|
+
plus `translateDataset` in `METADATA_DOCUMENT_TRANSLATORS`. A dataset reads like
|
|
2060
|
+
a back-office definition, but a measure label is drawn on the dashboard, under
|
|
2061
|
+
every metric tile and on every chart axis. Registering the translator is the
|
|
2062
|
+
whole wiring — `TRANSLATABLE_METADATA_TYPES` is derived from that table and
|
|
2063
|
+
`@objectstack/rest` reads the derived set (#3786) — so `GET /api/v1/meta/datasets?locale=…`
|
|
2064
|
+
localizes with nothing else to remember.
|
|
2065
|
+
|
|
2066
|
+
Key faces are measured against the authoring schemas rather than mirrored from
|
|
2067
|
+
the report, so nothing here parses clean and translates nothing: a bulk param's
|
|
2068
|
+
hint is `help` (not the action-param `helpText`), per-param `options` are refused
|
|
2069
|
+
because `options[].value` is unconstrained and a value-keyed map cannot address
|
|
2070
|
+
`true` and `"true"` apart, a def has no `successMessage`, and a dataset dimension
|
|
2071
|
+
or measure has no `description` — the authoring schema says so itself. Every
|
|
2072
|
+
exclusion carries `guidance` naming the right home.
|
|
2073
|
+
|
|
2074
|
+
Two tombstones stop asserting that no route exists: the retired
|
|
2075
|
+
`validationMessages` and `errors` guidance now point at
|
|
2076
|
+
`objects.<object>._validations.<rule>.message`. Retiring `validationMessages`
|
|
2077
|
+
(17.0.0, #4667, ADR-0049) is **not** reversed — that group was keyed by rule name
|
|
2078
|
+
at the bundle's top level, so it could not tell two objects' rules apart, and,
|
|
2079
|
+
the reason it was retired, nothing read it. Its ADR-0087 conversion still strips
|
|
2080
|
+
it from stored bundles. The replacement is object-scoped and ships its reader in
|
|
2081
|
+
the same change.
|
|
2082
|
+
|
|
2083
|
+
Authors upgrading need do nothing; a bundle that writes none of the three new
|
|
2084
|
+
groups behaves exactly as before.
|
|
2085
|
+
|
|
2086
|
+
<!-- adr-0087: not-required (unpublished) Purely additive: three new optional groups on `TranslationData` / `TranslationItem`, one new dispatch-table entry, and one new lookup on the write path. No authorable key is removed, renamed or re-shaped, so there is no tombstone, no stored shape to rewrite, and nothing mechanical for `objectstack migrate meta` to prescribe. The retired `validationMessages` conversion entry is untouched and still strips the key it always stripped — its guidance text now names a live replacement instead of asserting none exists, which changes what an author is told, not what a stored bundle becomes. -->
|
|
2087
|
+
- 5228e52: fix(lint): `lintDataModel` reads only the canonical `reference` target (#13250)
|
|
2088
|
+
|
|
2089
|
+
`refOf` in `packages/lint/src/data-model-rules.ts` resolved
|
|
2090
|
+
`def?.reference || def?.reference_to`, so a relationship field spelled with the
|
|
2091
|
+
rejected alias resolved a target. #11567 settled that `reference` is the only
|
|
2092
|
+
relationship spelling `@objectstack/spec` declares — `FieldSchema` answers
|
|
2093
|
+
`reference_to` with `unrecognized_keys` and *"Did you mean `reference_to` →
|
|
2094
|
+
`reference`?"* — and put it as "one key, one answer, on both doors".
|
|
2095
|
+
|
|
2096
|
+
`@objectstack/lint` runs over an in-memory, schema-parsed stack, so the alias
|
|
2097
|
+
cannot legitimately appear here at all: the tolerance was inert. Where it did
|
|
2098
|
+
fire, it made the rule whose entire job is to catch a relationship with no
|
|
2099
|
+
target — `relationship/missing-reference` — report a valid target for a field
|
|
2100
|
+
that has none, i.e. the one component that exists to tell an author their
|
|
2101
|
+
metadata is wrong was the component accepting the wrong spelling.
|
|
2102
|
+
|
|
2103
|
+
This mirrors the deliberate canonical-only narrowing already recorded in-file
|
|
2104
|
+
for `refOf` in `packages/lint/src/validate-security-posture.ts`, including its
|
|
2105
|
+
`typeof r === 'string'` guard — which also makes the declared
|
|
2106
|
+
`string | undefined` return type true, where the old `||` chain returned
|
|
2107
|
+
whatever truthy value it found (a non-string `reference` was reported as a
|
|
2108
|
+
resolved target).
|
|
2109
|
+
|
|
2110
|
+
What changes for a consumer, only for metadata the spec already refuses:
|
|
2111
|
+
`relationship/missing-reference` (error) now fires on a relationship field
|
|
2112
|
+
whose only target spelling is `reference_to`, and the rules that need a
|
|
2113
|
+
resolved target (`relationship/master-detail-required`, `rollup/missing-summary`
|
|
2114
|
+
and the rest of the relationship family) no longer treat such a field as
|
|
2115
|
+
pointing anywhere. Canonical `reference` is untouched.
|
|
2116
|
+
|
|
2117
|
+
Scope note: the two remaining tolerant readers named in #13250 —
|
|
2118
|
+
`packages/verify/src/derive.ts` and
|
|
2119
|
+
`packages/plugins/plugin-security/src/security-plugin.ts` — are deliberately
|
|
2120
|
+
NOT narrowed here. Both were measured to sit on populations the alias can
|
|
2121
|
+
actually reach (raw `registerObject`, which skips Zod by design, and an app
|
|
2122
|
+
config that never passes through a `define*` parse), so narrowing them is a
|
|
2123
|
+
triage call rather than a defect fix.
|
|
2124
|
+
- 9057811: fix(lint): `relationship/delete-behavior` suggestion no longer names `set_null` as declarable on a `master_detail`
|
|
2125
|
+
|
|
2126
|
+
`lintDataModel`'s `relationship/delete-behavior` suggestion told an author an
|
|
2127
|
+
undeclared `master_detail.deleteBehavior` could be `cascade`, `restrict`, or
|
|
2128
|
+
`set_null`. Since #9689 (PR #11406, maintainer ruling 2026-08-19), an authored
|
|
2129
|
+
`deleteBehavior: 'set_null'` on a `master_detail` field is a named parse-time
|
|
2130
|
+
rejection — a detail row cannot outlive its master, so the engine resolves
|
|
2131
|
+
every value except `restrict` to `cascade` on this type. Following the
|
|
2132
|
+
suggestion's own `set_null` mention literally walked an author into that
|
|
2133
|
+
rejection at publish time.
|
|
2134
|
+
|
|
2135
|
+
The message now enumerates only the two values `FieldSchema` actually accepts
|
|
2136
|
+
on a `master_detail` (`cascade`/`restrict` — matching the vocabulary already
|
|
2137
|
+
offered by the metadata-admin field form, `object.form.ts`'s `master_detail`
|
|
2138
|
+
`deleteBehavior` options), and keeps the same outcome-naming courtesy as the
|
|
2139
|
+
parse-time rejection message: it still names `set_null` to say plainly that it
|
|
2140
|
+
is not honored on this type, and points to `lookup` for the case where
|
|
2141
|
+
children must survive the parent. The `fix` payload (`deleteBehavior:
|
|
2142
|
+
'cascade'`) was already correct and is unchanged.
|
|
2143
|
+
- 1af8286: fix(lint): a field-level `*When` reading `app` gets the scope diagnostic, not the false `record.app` prescription (#13935)
|
|
2144
|
+
|
|
2145
|
+
`fieldRuleRootIssue` judged field-rule roots against `@objectstack/formula`'s
|
|
2146
|
+
`SCOPE_ROOTS`, which answers "is this root declared **platform-wide**". The
|
|
2147
|
+
question this rule needs answered is "is this root bound at **some** evaluation
|
|
2148
|
+
site". The two agreed for all 27 baseline roots and disagreed for exactly one:
|
|
2149
|
+
`app`, which objectui's `ExpressionProvider` binds on the form-view surface an
|
|
2150
|
+
author migrates a field rule *down* from.
|
|
2151
|
+
|
|
2152
|
+
Falling outside the membership test sent `app` to the generic bare-reference
|
|
2153
|
+
check, whose prescription is ``Write `record.app` `` — and following that
|
|
2154
|
+
advice earns ``unknown field `app` on `invoice` `` from the field-existence
|
|
2155
|
+
pass. A first diagnostic that asserts something false about where the root
|
|
2156
|
+
binds, plus a wasted correction cycle. `current_user`, `user`, `ctx`, `os`,
|
|
2157
|
+
`features` and `data` all got the correct message; `app` alone did not.
|
|
2158
|
+
|
|
2159
|
+
Authoring a field-level `visibleWhen` / `readonlyWhen` / `requiredWhen` on
|
|
2160
|
+
`app` now earns the same scope diagnostic every other unbound root gets —
|
|
2161
|
+
"a field-level conditional rule binds only `record` (plus `previous`, and
|
|
2162
|
+
`parent` on a master-detail line item)" — with a prescription tier of its own
|
|
2163
|
+
that says what is actually true of an ambient root: it is *not* declared
|
|
2164
|
+
platform-wide, it is mounted only by the renderer, and `record.app` is
|
|
2165
|
+
explicitly refused rather than merely omitted, because that is the advice the
|
|
2166
|
+
author just followed out of the old diagnostic.
|
|
2167
|
+
|
|
2168
|
+
**No accept set moves.** `SCOPE_ROOTS` is `@objectstack/formula`'s published
|
|
2169
|
+
strict-lint baseline — adding `app` there would stop *every* surface that
|
|
2170
|
+
judges bare identifiers from faulting it, to fix one surface's wording. The
|
|
2171
|
+
widened vocabulary is assembled in `@objectstack/lint` instead, where the
|
|
2172
|
+
per-surface question is asked, and both diagnostics involved were already
|
|
2173
|
+
`severity: 'error'`, so this changes which message an author reads and nothing
|
|
2174
|
+
about what lints clean.
|
|
2175
|
+
|
|
2176
|
+
`FIELD_RULE_AMBIENT_ROOTS` and `FIELD_RULE_JUDGED_ROOTS` are exported beside
|
|
2177
|
+
the existing `FIELD_RULE_BOUND_ROOTS`.
|
|
2178
|
+
- 365e334: **Fix:** `lintLivenessProperties` walks `stack.translations` as the locale-keyed bundle it is, so the `translation` liveness ledger finally reaches the author (#11288).
|
|
2179
|
+
|
|
2180
|
+
`stack.translations` is `z.array(TranslationBundleSchema)` — each item is a `TranslationBundle`, i.e. `z.record(LocaleSchema, TranslationDataSchema)`, whose top-level keys are locale codes. The lint registered `{ type: 'translation', key: 'translations' }` in `TYPE_COLLECTIONS` and then walked those items flat, the way every other collection there is walked: `checkItem` read `bundle['flows']` for the ledger's one `authorWarn` row. A bundle has no `flows` key at any depth reachable that way — the groups live one level down, under each locale — so every warned lookup missed and the whole `translation` ledger was silent for file-authored bundles, the only way apps author translations today.
|
|
2181
|
+
|
|
2182
|
+
That is the failure mode the comment above `TYPE_COLLECTIONS` names ("a newly governed type needs its collection registered or its ledger warns nobody"), reached from the other side: the collection *was* registered, and the shape underneath it was the mismatch. Registering a collection is only half the contract — the walk has to match the collection's shape — so the row is now a tombstone comment saying exactly that, and `translation` joins `object`/`field` as a bespoke walk: for each bundle, each locale entry's `TranslationData` is checked, with the finding subject naming the bundle index and the locale (`translation bundle #0 · locale 'zh-CN'`).
|
|
2183
|
+
|
|
2184
|
+
Measured on a real app before the fix, as a guarded ablation: injecting a `flows:` section into a locale bundle and re-running `objectstack lint --json` produced **zero** delta — 91 issues before and after, 0 liveness findings naming `flows`. The author who reached for a `planned` translation group got silence, which is strictly worse than the ledger being absent, because the ledger's stated contract is that `authorWarn` is what tells them.
|
|
2185
|
+
|
|
2186
|
+
Advisory-only as before: the finding is a warning, and `os lint` exits on errors, never on warnings.
|
|
2187
|
+
|
|
2188
|
+
The regression test is pinned on the **bundle** shape, and a `TranslationItem`-shaped anti-fixture is pinned alongside it. That shape — `locale` plus the groups at the top level — is the runtime metadata door, and it *warned on the broken walk*, so a fixture written that way would have been green from the day the bug shipped and pinned nothing. Runtime-authored `translation` items are reached by this lint through no door at all: no stack collection carries them, and the rule is `surfaces: CLI_ONLY`, so it does not run at the runtime publish gate either. The two doors share the group vocabulary, not the container; only the file-authored one is lintable, and now it is linted.
|
|
2189
|
+
- 5c28b88: lint: `flow-update-readonly-when-field` now inspects `runAs:'system'` flows
|
|
2190
|
+
|
|
2191
|
+
The `runAs:'system'` exemption in `validate-readonly-flow-writes` was a single
|
|
2192
|
+
flow-level early return, so it removed an elevated flow from **both** branches of
|
|
2193
|
+
the rule. Only the static branch warrants it: the engine skips
|
|
2194
|
+
`stripReadonlyFields` under `if (!opCtx.context?.isSystem)`, but
|
|
2195
|
+
`stripReadonlyWhenFields` runs on the update path with no `isSystem` guard at all
|
|
2196
|
+
(`packages/objectql/src/engine.ts`, the #9107 note: "`isSystem` is still NOT an
|
|
2197
|
+
exemption here, unlike the static strip below"), pinned as "LOCK 2 — isSystem does
|
|
2198
|
+
NOT exempt a caller-supplied value".
|
|
2199
|
+
|
|
2200
|
+
The exemption now gates the static branch only. A `runAs:'system'` flow whose
|
|
2201
|
+
`update_record` node writes a `readonlyWhen` field reports the branch's existing
|
|
2202
|
+
`warning` — the same silent-no-op the rule exists to surface, on the flow class the
|
|
2203
|
+
rule's own hint tells the author elevation cannot save. A system flow writing a
|
|
2204
|
+
static `readonly:true` field stays silent, as before; rule ids and severities are
|
|
2205
|
+
unchanged, and the new finding is advisory and never blocks a build.
|
|
2206
|
+
- 996cb1d: fix(lint): run the flattened-scope shadowing warning on the descriptor-declared predicate slots too (#14288)
|
|
2207
|
+
|
|
2208
|
+
The #14089 shadowing warning — a bare name that is BOTH a declared flow
|
|
2209
|
+
variable AND a field on the bound object, where the variable silently wins at
|
|
2210
|
+
runtime — reached exactly two expression positions: the node `condition` and
|
|
2211
|
+
the edge `condition`. The `#4027` descriptor-declared predicate slots were
|
|
2212
|
+
validated for dialect by the same traversal but were never passed through the
|
|
2213
|
+
shadowing pass, so the identical mistake stayed silent on them.
|
|
2214
|
+
|
|
2215
|
+
The warning is about the **scope** an expression is evaluated in, not the key
|
|
2216
|
+
it was authored under, and the engine measurement says both `predicate` slots
|
|
2217
|
+
on the ledger share the run's one flattened variable map:
|
|
2218
|
+
|
|
2219
|
+
- `decision.conditions[].expression` — the decision executor evaluates against
|
|
2220
|
+
the very `variables` parameter the engine hands every node executor, which is
|
|
2221
|
+
the same `Map` object `seedRunVariables` built and a node `condition` is
|
|
2222
|
+
judged against. Nothing on the path clones or narrows it.
|
|
2223
|
+
- `screen.fields[].visibleWhen` — `refuseInvalidScreenInput` evaluates against
|
|
2224
|
+
`run.variables` (the persisted snapshot of that same seeded map) with the
|
|
2225
|
+
submitted bag overlaid. A superset, so the shadow still reaches it: the
|
|
2226
|
+
overlay carries the screen's own collected values, never the bound record's
|
|
2227
|
+
field, so it can never hand back a field the variable displaced.
|
|
2228
|
+
|
|
2229
|
+
`loop.collection` and `map.collection` are `flow-template`, not `predicate`,
|
|
2230
|
+
and the slot loop already skips them.
|
|
2231
|
+
|
|
2232
|
+
Warning-only and within the 2026-09-01 option-C ruling's letter: one more call
|
|
2233
|
+
site reusing the `declaredVariables` set already collected once per flow, no
|
|
2234
|
+
new rule id, no severity above `warning`, no accept set moved, and no bare
|
|
2235
|
+
identifier judged for being bare. Nothing that linted clean before can newly
|
|
2236
|
+
fail a build.
|
|
2237
|
+
- 6eb8e3c: fix(lint): `sharing-rule-runtime-variable-condition`'s fix-hint no longer sends authors to RLS to widen a `private` object (#14234)
|
|
2238
|
+
|
|
2239
|
+
The hint printed for a sharing-rule `condition` that reads `current_user.*`
|
|
2240
|
+
ended in an **unqualified** remedy: "express per-user access with the mechanism
|
|
2241
|
+
that runs per request instead — an RLS policy on a permission set
|
|
2242
|
+
(`rowLevelSecurity[].using`, where `current_user.*` IS resolved)". That advice
|
|
2243
|
+
is sound on an open OWD and **structurally impossible on `private`**, which is
|
|
2244
|
+
the sharing model an author hitting this rule is most likely to be on.
|
|
2245
|
+
|
|
2246
|
+
Measured, not argued. The security layers are AND-composed —
|
|
2247
|
+
`plugin-security`'s `getReadFilter` returns
|
|
2248
|
+
`andComposeLayers(andComposeLayers(filter, cbpFilter), sharingFilter)`, and its
|
|
2249
|
+
own prose promises "the same filter the engine middleware AND-s into" every
|
|
2250
|
+
find — while `plugin-sharing`'s `buildReadFilter` constrains `private` only
|
|
2251
|
+
(`effectiveSharingModel(schema) !== 'private'` returns `null`). So:
|
|
2252
|
+
|
|
2253
|
+
- `public_read` / `public_read_write` → the sharing layer imposes nothing on
|
|
2254
|
+
reads, and an RLS policy **narrows** an open baseline. The original advice is
|
|
2255
|
+
correct here and is kept, now with the models it holds for attached.
|
|
2256
|
+
- `private` → the sharing layer has already withheld the row, and **an AND term
|
|
2257
|
+
can only remove rows, never add one back**. An RLS "widener" there lints
|
|
2258
|
+
clean, passes every gate, and grants nothing — a silent failure at the
|
|
2259
|
+
security boundary, which is the worst possible feedback shape. The card
|
|
2260
|
+
records an application author who followed this path and shipped the grant
|
|
2261
|
+
unauthored/fail-closed instead.
|
|
2262
|
+
|
|
2263
|
+
The hint now splits on `sharingModel` and, for the `private` case, states what
|
|
2264
|
+
is true: the two doors that widen a private object for a non-admin principal
|
|
2265
|
+
both key on **who owns the row** or on an **explicit share row**, never on a
|
|
2266
|
+
property of the record — the ADR-0057 D1 depth scopes (`readScope`/`writeScope`,
|
|
2267
|
+
which widen the owner-match to `owner_id IN (…)`) and a `sys_record_share` row,
|
|
2268
|
+
which on this path only a criteria sharing rule writes. Where neither fits, it
|
|
2269
|
+
names the gap **in words** — a known platform limitation, not something a
|
|
2270
|
+
different spelling of this rule can close — rather than inventing a mechanism,
|
|
2271
|
+
and it warns that `position` recipients resolve **tenant-wide**: the
|
|
2272
|
+
neighbouring temptation is an over-broad grant that also lints clean, not the
|
|
2273
|
+
narrower one the author meant. The tracker anchor for that gap sits in an
|
|
2274
|
+
adjacent source comment, not in the message, because a runtime string reaches
|
|
2275
|
+
authors and generated surfaces that cannot resolve `#NNNN`
|
|
2276
|
+
(`check:doc-authoring`, maintainer ruling 2026-08-12) — a pin asserts the
|
|
2277
|
+
message carries no tracker id.
|
|
2278
|
+
|
|
2279
|
+
**The explanation is preserved byte-for-byte.** The opening sentence — the
|
|
2280
|
+
MATERIALISED/`criteria_json` explanation of *why* the condition is refused — is
|
|
2281
|
+
the most useful part of the diagnostic and is unchanged; a pin asserts it
|
|
2282
|
+
verbatim. The file's own docblock carried the same unqualified "the fix is RLS"
|
|
2283
|
+
claim one layer up and is corrected in the same edit.
|
|
2284
|
+
|
|
2285
|
+
**No behaviour change**: the rule's accept/reject verdict, its ids, severities,
|
|
2286
|
+
paths and `message` text are untouched — this is `hint` prose only. AND
|
|
2287
|
+
composition is correct by design and is not touched either.
|
|
2288
|
+
|
|
2289
|
+
A new pin holds the split, because prose is invisible to every other gate —
|
|
2290
|
+
nothing else in CI reads a word of this hint. Reverse-verified by restoring the
|
|
2291
|
+
shipped wording: **7 legs go red** (the unqualified-RLS leg, the AND-composition
|
|
2292
|
+
leg, the widening-doors leg, the `position` warning, the #14103 gap, and both
|
|
2293
|
+
vocabulary legs), while the verbatim-explanation leg correctly stays green. The
|
|
2294
|
+
vocabulary legs check every sharing model and depth scope the hint names against
|
|
2295
|
+
the **spec-owned** `OWDModel` / `ObjectAccessScopeSchema` enums, so a rename in
|
|
2296
|
+
the spec reds this file instead of leaving the hint quoting values the platform
|
|
2297
|
+
no longer has — without it the hint and its expectations would move together and
|
|
2298
|
+
nothing would go red.
|
|
2299
|
+
- 3a5b8c9: fix(lint): consolidate five more private "Did you mean?" copies onto the shared `suggestName` (#14577, follow-up to #14268/#14575)
|
|
2300
|
+
|
|
2301
|
+
`validate-action-name-refs.ts`, `validate-chart-bindings.ts` and
|
|
2302
|
+
`validate-searchable-fields.ts` each carried a private `suggest`/`distance`
|
|
2303
|
+
pair, byte-for-byte re-deriving the edit-distance-only budget that
|
|
2304
|
+
`object-graph.ts` already exports as `suggestName` (the shared helper
|
|
2305
|
+
#14268/#14575 consolidated three other rules onto). All three now import
|
|
2306
|
+
`suggestName` from `./object-graph` and their private copies are deleted.
|
|
2307
|
+
|
|
2308
|
+
`validate-ai-tool-references.ts` and `validate-translation-references.ts`
|
|
2309
|
+
each carry a one-line pre-pass ahead of the private pair — the `action_<name>`
|
|
2310
|
+
tool-family prefix, and a snake_case namespace-segment match — that is
|
|
2311
|
+
rule-local knowledge, not the shared helper's business. Both keep that
|
|
2312
|
+
pre-pass and now delegate the fallback to `suggestName` instead of a private
|
|
2313
|
+
Levenshtein copy.
|
|
2314
|
+
|
|
2315
|
+
The shared helper's containment pre-pass (a candidate that contains the
|
|
2316
|
+
target, or vice versa, scores ahead of any edit-distance match) is now every
|
|
2317
|
+
one of these five rules' behaviour too, so a hint may now appear where one was
|
|
2318
|
+
previously absent — it never removes a hint the private copy gave. Per site:
|
|
2319
|
+
|
|
2320
|
+
- `validate-action-name-refs.ts` — `archive` → `archive_completed_deals`
|
|
2321
|
+
(17 edits, over budget) now gets a hint; unaffected cases unchanged.
|
|
2322
|
+
- `validate-chart-bindings.ts` — the issue's own headline example,
|
|
2323
|
+
`amount` → `sum_amount` (4 edits, over the budget of 2) now gets a hint on
|
|
2324
|
+
a raw-field-instead-of-measure binding.
|
|
2325
|
+
- `validate-searchable-fields.ts` — `amount` → `sum_amount` (4 edits) now
|
|
2326
|
+
gets a hint on a stale `searchableFields` entry.
|
|
2327
|
+
- `validate-ai-tool-references.ts` — the `action_<name>` prefix pre-pass is
|
|
2328
|
+
unchanged and still wins first; a miss with no prefix match now also
|
|
2329
|
+
reaches `suggestName`'s containment scan (e.g. `knowledge_base` →
|
|
2330
|
+
`search_knowledge_base`), where the old private copy gave nothing.
|
|
2331
|
+
- `validate-translation-references.ts` — the namespace-segment pre-pass is
|
|
2332
|
+
unchanged and still wins first; a miss with no segment match now also
|
|
2333
|
+
reaches `suggestName`'s containment scan (e.g. `amount` →
|
|
2334
|
+
`amountsummary`), where the old private copy gave nothing.
|
|
2335
|
+
|
|
2336
|
+
`object-graph.ts`'s helper is untouched (already ruled by #14268/#14575);
|
|
2337
|
+
`validate-react-page-props.ts` and `validate-rule-schema-formats.ts` stay out
|
|
2338
|
+
— both are a different contract on purpose (see #14577's triage).
|
|
2339
|
+
- d754829: fix(lint): consolidate the four hand-copied "Did you mean?" helpers into `object-graph.ts`'s shared `nearestName`/`suggestName` (#14268)
|
|
2340
|
+
|
|
2341
|
+
`validate-object-references.ts`, `validate-sortable-fields.ts` and
|
|
2342
|
+
`validate-widget-bindings.ts` each carried a private `suggest`/`distance` (or
|
|
2343
|
+
`didYouMean`/`levenshtein`) pair, byte-for-byte re-deriving the same
|
|
2344
|
+
edit-distance budget `object-graph.ts` already exported on the package barrel
|
|
2345
|
+
as `nearestName`/`suggestName` — the same drift #4330 fixed one constant over
|
|
2346
|
+
for `SYSTEM_FIELDS`. All three now import `suggestName` (and `nearestName`
|
|
2347
|
+
where a rule needs the bare name) from `./object-graph` and their private
|
|
2348
|
+
copies are deleted.
|
|
2349
|
+
|
|
2350
|
+
The one decision the consolidation forced: `validate-widget-bindings.ts`
|
|
2351
|
+
scored a containment match (e.g. `amount` → `sum_amount`, the ADR-0021
|
|
2352
|
+
base-column → prefixed-measure-name drift) ahead of edit distance; the other
|
|
2353
|
+
two rules had no such pre-pass and suggested nothing for the same class of
|
|
2354
|
+
typo. That containment pre-pass is now `nearestName`'s behaviour for every
|
|
2355
|
+
caller — it only ever *adds* a suggestion where the edit-distance budget
|
|
2356
|
+
previously returned none, so a "Did you mean?" hint may now appear where one
|
|
2357
|
+
was previously absent. The full `@objectstack/lint` suite (93 files / 2812
|
|
2358
|
+
tests) was run against the pre-change and post-change trees and produced
|
|
2359
|
+
identical results, so no existing suggestion assertion was affected in
|
|
2360
|
+
practice.
|
|
2361
|
+
- fa7292c: fix(lint): wire `packages/lint`'s test layer into `check:test-typecheck`, so its 2,700-line rule pin file is actually type-checked (#14173)
|
|
2362
|
+
|
|
2363
|
+
`packages/lint/tsconfig.json` excluded `**/*.test.ts` and `**/*.spec.ts`, and
|
|
2364
|
+
the package's `typecheck` script was a bare `tsc --noEmit` against that very
|
|
2365
|
+
config — so no gate anywhere read a lint test file with a type checker.
|
|
2366
|
+
`src/validate-expressions.test.ts` alone is ~2,700 lines built almost entirely
|
|
2367
|
+
out of compile-time and meta pins (the #5017 receiver scan, the
|
|
2368
|
+
`TRACKED_UNDECLARED_READS` shrink-only list, the residual-root table), and none
|
|
2369
|
+
of it was type-checked by anything: vitest transpiles through esbuild (types
|
|
2370
|
+
stripped, never resolved), so a wrong key or a signature drift in a pin's own
|
|
2371
|
+
scaffolding was caught by nobody.
|
|
2372
|
+
|
|
2373
|
+
Onboarded by *wiring* to the mechanism #14062 (PR #14420) landed on
|
|
2374
|
+
(`scripts/check-test-typecheck.mts`), per the triage ruling on this card: a
|
|
2375
|
+
sibling `tsconfig.test.json` matching vitest's real module semantics
|
|
2376
|
+
(`module: esnext`, `moduleResolution: bundler`, `lib: ["ES2022"]`; strictness
|
|
2377
|
+
and `rootDir` untouched, inherited), named by `typecheck`. Measured (workspace
|
|
2378
|
+
closure built first): 6 residual errors over 2 files, all TS6059 (imports from
|
|
2379
|
+
`examples/app-showcase`, outside this package's `rootDir` — pre-existing,
|
|
2380
|
+
config-tier, not a lint defect), recorded EXACT and shrink-only in the new
|
|
2381
|
+
`test-typecheck-debt.json`.
|
|
2382
|
+
|
|
2383
|
+
This is a CONVERSION of the coverage gate's existing `@objectstack/lint`
|
|
2384
|
+
TEST_DEBT entry (`errors: 16`), not a new debt-opening decision: the same
|
|
2385
|
+
authority that recorded the 16 now holds the residue one level finer, per file
|
|
2386
|
+
and per signature, and the coverage-gate entry is deleted as the graduation
|
|
2387
|
+
that pairing forces. No test file is edited — opening the ratchet is not the
|
|
2388
|
+
same job as paying it down.
|
|
2389
|
+
- 22b0081: `os validate` now refuses a react-page `<ListView>` bound only by the metadata-tier data source instead of accepting it. The react-blocks contract deprecates `objectName` in favour of `data={{ provider: 'object', object }}`, but no renderer reads that spelling yet — so a page written to the contract's own summary validated green and then rendered an empty list with no diagnostic, and the check tightens back to what actually renders. The refusal names `objectName` as the spelling to write, and field-name props (`columns`, `searchableFields`, filter positions, …) again resolve against it, which is also the correct object when a page carries both spellings.
|
|
2390
|
+
|
|
2391
|
+
Scoped deliberately: pages binding with `objectName` are unaffected, and so are the `value` and `api` data providers, a plain-array `data`, and a non-static `data` prop — all of those do render. The deprecation warnings on `objectName` and `viewType` remain, with their text corrected; both are still the spellings to write until the renderer folds the canonical data source in. Pages that were green on the canonical spelling alone now fail by design.
|
|
2392
|
+
- 20a452e: `lintLivenessProperties` now honours its own docblock contract ("Advisory only
|
|
2393
|
+
— returns findings, never throws") when a collection item is `null` or
|
|
2394
|
+
otherwise not an object. The object walk, the field walk nested under it, and
|
|
2395
|
+
the flat `TYPE_COLLECTIONS` loop that covers every other governed type (flow,
|
|
2396
|
+
action, agent, tool, …) each read `item.name`/`item.object` straight off every
|
|
2397
|
+
element with no record guard, throwing `TypeError: Cannot read properties of
|
|
2398
|
+
null (reading 'name')` on a malformed item instead of skipping it — reachable
|
|
2399
|
+
via the exported `stack: AnyRec` signature on an unparsed or hand-built stack.
|
|
2400
|
+
The translation bundle walk already guarded its two levels (#11383); this
|
|
2401
|
+
closes the same hole on the three walks that did not (#11385).
|
|
2402
|
+
- de3c52b: Fix `validate-translation-references` reporting a nested `conditional` validation branch's legitimate `_validations` bundle entry as an orphan `translation-target-unknown`, with inverted advice.
|
|
2403
|
+
|
|
2404
|
+
The rule built its `_validations` universe with a flat walk of `objects[].validations[]`. A `conditional` rule's `then` / `otherwise` branch is itself a full rule carrying its own `name`, and that branch name — not the wrapper's — is the address `checkConditional` delegates to and `authoredRuleMessage` keys on at runtime (`packages/objectql/src/validation/rule-validator.ts`). The flat walk never saw a branch name, so a correct bundle entry for one was flagged as an orphan, and the finding's own text ("keeps its source locale in every refusal") was the opposite of the truth for that key — acting on the advice (deleting the entry) reintroduced the exact defect it fixed.
|
|
2405
|
+
|
|
2406
|
+
The walk now descends into `then` / `otherwise`, mirroring `evaluateRule`'s recursion (a branch may itself be a nested `conditional`, so depth is unbounded). The wrapper's own name stays in the universe, unchanged: its message is structurally unreachable at runtime, but a bundle entry for it is deliberately kept elsewhere so the bundle mirrors the declared rule set 1:1.
|
|
2407
|
+
- 289cf91: fix(lint): guard `walkPageComponents` against component cycles (#13217)
|
|
2408
|
+
|
|
2409
|
+
`walkPageComponents` — the one shared page-component traversal under every
|
|
2410
|
+
page-shaped lint rule and the CLI's i18n object-sections pass — descended the
|
|
2411
|
+
untyped composition slots inside `properties` with no cycle guard. Every one of
|
|
2412
|
+
those slots is `z.array(z.unknown())` authored data, so a component whose
|
|
2413
|
+
`properties.children` contains itself is **legal input**, and feeding one in
|
|
2414
|
+
recursed until the stack died with `RangeError: Maximum call stack size
|
|
2415
|
+
exceeded`. Because the walk is shared rather than copied, that crash was not
|
|
2416
|
+
scoped to one rule: it took every rule standing on the walk down in the same
|
|
2417
|
+
process.
|
|
2418
|
+
|
|
2419
|
+
The descent now carries an **ancestor set** — the node is added before
|
|
2420
|
+
descending and removed on the way out — so a node that is its own ancestor
|
|
2421
|
+
stops the descent. Measured on the shapes that matter: a direct self-reference,
|
|
2422
|
+
an indirect cycle (`A -> B -> A`) and a longer chain (`A -> B -> C -> A`) all
|
|
2423
|
+
terminate, through every descended slot (`properties.children`,
|
|
2424
|
+
`properties.items[].children`, `properties.body`, `properties.footer`).
|
|
2425
|
+
|
|
2426
|
+
Two deliberate non-changes, both pinned:
|
|
2427
|
+
|
|
2428
|
+
- **An ancestor set, not a visited set.** A component object placed twice as a
|
|
2429
|
+
*sibling*, or reached down two different branches, is legitimate re-use at two
|
|
2430
|
+
distinct config paths, and every rule built on this walk must see both
|
|
2431
|
+
placements. A visited set would yield the first and silently drop the rest —
|
|
2432
|
+
trading a loud crash for missing lint coverage. This matches the predicate the
|
|
2433
|
+
sibling resolver `translatePage` already settled on.
|
|
2434
|
+
- **No depth cap.** A cap and a cycle guard are different instruments. On a
|
|
2435
|
+
resolver a cap leaves copy untranslated; on a lint walk it would drop real
|
|
2436
|
+
components from the walk output and every rule would go quiet about them — a
|
|
2437
|
+
silent truncation that reads exactly like a clean page. With the cycle guard
|
|
2438
|
+
the descent is bounded by the document's own finite nesting, so a cap could
|
|
2439
|
+
only ever fire on acyclic input, which is the input it must not truncate.
|
|
2440
|
+
|
|
2441
|
+
The guard is silent: a cycle stops the descent and yields nothing extra, and no
|
|
2442
|
+
finding or warning is produced. Deciding that a self-referential page is itself
|
|
2443
|
+
an authoring error would be new reject behaviour on authored input, which is a
|
|
2444
|
+
contract call and not this walk's to make. Measured on a cyclic-but-otherwise
|
|
2445
|
+
valid page, all six rules that route through the walk report exactly what they
|
|
2446
|
+
report for the equivalent acyclic document (zero findings either way).
|
|
2447
|
+
|
|
2448
|
+
No authored page in this repo carries such a cycle — swept across 57
|
|
2449
|
+
page-shaped objects with a positive control, zero hits — so this fixes a
|
|
2450
|
+
reachable crash, not an active incident.
|
|
2451
|
+
- ba8420b: `lintLivenessProperties` no longer tells authors a `planned` property is `dead`
|
|
2452
|
+
|
|
2453
|
+
`describe()` in `lint-liveness-properties.ts` only knew two verdicts
|
|
2454
|
+
(`experimental`, everything else → `dead`), while the liveness ledger ships a
|
|
2455
|
+
third: `status: 'planned'` (declared, and a consumer is being built against
|
|
2456
|
+
it — contract-first, the opposite of `dead`). Every `planned` row fell through
|
|
2457
|
+
into the `dead` branch, so the finding's own **message** told the author to
|
|
2458
|
+
remove metadata the platform had asked them to write, while the same finding's
|
|
2459
|
+
**hint** (when the row carried one) said the opposite one sentence later. Three
|
|
2460
|
+
shipped rows hit this: `field.relatedListFilter`, `object.externalSharingModel`,
|
|
2461
|
+
`translation.flows`.
|
|
2462
|
+
|
|
2463
|
+
`describe()` now has a third branch: `status === 'planned'` gets its own rule
|
|
2464
|
+
id (`liveness-planned-property`, mirroring `liveness-dead-property` /
|
|
2465
|
+
`liveness-experimental-property`'s advisory-only posture — nothing downstream
|
|
2466
|
+
keys off these ids today) and its own message/default hint ("keep it — a
|
|
2467
|
+
consumer is being built against this property", never "Remove it").
|
|
2468
|
+
|
|
2469
|
+
The ledger's `status` field is a documented vocabulary, not a Zod-enforced
|
|
2470
|
+
enum — nothing rejects a ledger entry with an unrecognised status. `describe()`
|
|
2471
|
+
previously graded any such entry `dead` silently; it now throws, naming the
|
|
2472
|
+
offending status, so a ledger-authoring mistake (a typo, or a new status added
|
|
2473
|
+
without teaching this file about it) fails loudly at test time instead of
|
|
2474
|
+
mislabelling a finding.
|
|
2475
|
+
- b003cf2: Refuse an undeclared field a `before*` hook writes, identically on every driver
|
|
2476
|
+
|
|
2477
|
+
**BREAKING** accept-set narrowing at the post-hook write door, shipped as `minor`
|
|
2478
|
+
under the repo's launch-window convention for breaking changes.
|
|
2479
|
+
|
|
2480
|
+
**Bump level, argued**: `@objectstack/objectql` is `minor`, not `patch`. A
|
|
2481
|
+
`before*` hook or an L2 (`language:'js'`) body writing a key the object never
|
|
2482
|
+
declares **used to succeed** on the `memory` family — the value reached the
|
|
2483
|
+
store and persisted as a shadow column — and now **throws**, `INVALID_FIELD` /
|
|
2484
|
+
**400**, on every driver. That is a narrowing of the accept set on the record
|
|
2485
|
+
payload, a surface every hook body touches; it is not an instrument or a message
|
|
2486
|
+
fix, and a hook that relied on either driver-dependent outcome stops working at
|
|
2487
|
+
run time. The same-package sibling `.changeset/hook-input-symbol-key-refusal.md`
|
|
2488
|
+
argues exactly this shape — "used to succeed, and now throw. That is a narrowing
|
|
2489
|
+
of the accept set" — to `minor`, and the launch-window convention is what keeps
|
|
2490
|
+
it off `major` (pre-1.0 lockstep semantics: a breaking change does not burn a
|
|
2491
|
+
major version while the stack versions in lockstep — see
|
|
2492
|
+
`scripts/check-changeset-no-major.mjs`). `patch` would under-declare a change
|
|
2493
|
+
that turns a passing hook into a throwing one.
|
|
2494
|
+
|
|
2495
|
+
`'@objectstack/lint': patch` is deliberate and stays. That half of the diff is
|
|
2496
|
+
message and comment prose only: `validateHookBodyWrites` reports the same
|
|
2497
|
+
findings on the same bodies at the same severity, with wording that now names
|
|
2498
|
+
the runtime refusal instead of the driver split this change retires.
|
|
2499
|
+
|
|
2500
|
+
The declared-field door (#8682 on insert, #8738 on update) runs before the
|
|
2501
|
+
`before*` hooks — deliberately, so a payload about to be refused never consumes
|
|
2502
|
+
an autonumber (#8737). That left the payload the hooks themselves produce
|
|
2503
|
+
unjudged: a key a `beforeInsert` / `beforeUpdate` hook or an L2 (`language:'js'`)
|
|
2504
|
+
body wrote went straight to the driver, and the drivers disagreed. `memory`
|
|
2505
|
+
accepted it and stored a shadow column; `driver-sql` threw a raw `SQLITE_ERROR`
|
|
2506
|
+
with no `status` and the bound statement and its values quoted back in the
|
|
2507
|
+
message; `sqlite-wasm` threw a bare `Error` with neither. One app and one hook
|
|
2508
|
+
meant different things on two deployments, and nothing in the app could tell
|
|
2509
|
+
which one it was running on.
|
|
2510
|
+
|
|
2511
|
+
The same check now runs a second time over the post-hook payload, before any
|
|
2512
|
+
statement is built, so a hook-written undeclared key is refused with the caller
|
|
2513
|
+
path's envelope — `INVALID_FIELD` / **400**, `Unknown field 'x' on object 'y'` —
|
|
2514
|
+
on every driver, because none of them is reached. The existing pre-hook door is
|
|
2515
|
+
unchanged and stays exactly where it is.
|
|
2516
|
+
|
|
2517
|
+
This is a security fix as well as a consistency one: `fieldPermissions` is keyed
|
|
2518
|
+
by declared field name and reports only fields explicitly marked non-editable, so
|
|
2519
|
+
a key the object never declares can carry no entry and could never be gated by
|
|
2520
|
+
field-level security. On `memory`-family stores such a value was persisted where
|
|
2521
|
+
no view, formula, index or permission could name it.
|
|
2522
|
+
|
|
2523
|
+
The platform's own stamps are unaffected. `created_at` / `updated_at` — the two
|
|
2524
|
+
the built-in audit hook writes unconditionally, because SQL drivers create them
|
|
2525
|
+
as built-in columns on every table — are already tolerated by this check
|
|
2526
|
+
alongside `id`; every other stamp (`created_by`, `updated_by`, `tenant_id`) is
|
|
2527
|
+
guarded by an explicit declaration test in the hook that writes it.
|
|
2528
|
+
|
|
2529
|
+
<!-- adr-0087: not-required (no-migration-prescription) No metadata key, spec symbol, Zod schema, object definition or stored representation is added, removed or renamed. This narrows which run-time record payload the engine accepts after the `before*` hooks have run; an undeclared key was never a declarable metadata surface, so `objectstack migrate meta` has nothing in a stored source to rewrite. The remedy for an affected hook body is to declare the field on the object or stop writing it, which is authoring guidance, not a mechanical rewrite of stored metadata. -->
|
|
2530
|
+
- b5a2398: Correct three stale `current_user` binding claims about a form FIELD `visibleWhen`
|
|
2531
|
+
|
|
2532
|
+
A runtime form field's `visibleWhen` has resolved `current_user` — and the ADR-0068 D1 aliases `user` / `ctx.user` / `os.user` — since objectui#6010, but three texts shipped by these two packages still told authors the root was unbound there, and that per-option `visibleWhen` was "the only `*When` surface where it resolves".
|
|
2533
|
+
|
|
2534
|
+
- `@objectstack/spec`: `FormFieldSchema.visibleWhen`'s doc block and its `describe()` now state the binding together with the two limits it does not remove — it is a rendering rule that nothing on the write path evaluates, so a role test written there protects no data; and the scope belongs to the host, so it is empty on the console's public standalone form route, where the predicate faults and visibility fails open. The generated `content/docs/references/ui/view.mdx` rows follow from the `describe()`.
|
|
2535
|
+
- `@objectstack/lint`: the field-rule prescription no longer grounds "move it to the option's own `visibleWhen`" on exclusivity. It grounds it on enforcement — the rule validator evaluates a per-option predicate on every write — and names the form-view field predicate only to refuse it as a destination for a server-enforced object rule, since moving one there would trade a loud lint error for a silent enforcement gap.
|
|
2536
|
+
|
|
2537
|
+
No schema, validation or verdict change: the set of accepted metadata is byte-identical, and the rule still refuses a user root on an object field-level `*When`.
|
|
2538
|
+
- b992b1d: fix(lint): stop the `readonlyWhen` hints ruling out the remedy that works and offering one that does not (#13832)
|
|
2539
|
+
|
|
2540
|
+
Message text only. Rule ids, severities and match sets are untouched, and no
|
|
2541
|
+
finding changes shape — but the hint **is** the whole product of an advisory
|
|
2542
|
+
rule (neither finding blocks a build), so the sentence is all the author acts
|
|
2543
|
+
on, and both of these sentences were measured false against the engine.
|
|
2544
|
+
|
|
2545
|
+
`flow-update-readonly-flow-writes`' `flow-update-readonly-when-field` hint said:
|
|
2546
|
+
|
|
2547
|
+
> If automation must maintain this field regardless of record state, run the flow runAs:'system'.
|
|
2548
|
+
|
|
2549
|
+
It does not. The conditional strip has **no `isSystem` guard at all** —
|
|
2550
|
+
`stripReadonlyWhenFields` runs unconditionally on the update path, unlike the
|
|
2551
|
+
static `readonly` strip beside it that really is skipped for system callers.
|
|
2552
|
+
So the advice bought the author a `runAs:'system'` flow, a re-run, the same
|
|
2553
|
+
missing column, and an elevated run identity in the tree with no compensating
|
|
2554
|
+
behaviour: **a privilege widening for no effect**. Pinned as "LOCK 2 — isSystem
|
|
2555
|
+
does NOT exempt a caller-supplied value" in
|
|
2556
|
+
`engine-readonly-when-derived-writes.test.ts`, and from the strict-mode side as
|
|
2557
|
+
"covers readonlyWhen too — the arm a trusted (isSystem) caller can still hit".
|
|
2558
|
+
|
|
2559
|
+
Both the `hook-api-update-readonly-when-field` hint and the matching
|
|
2560
|
+
`content/docs/automation/hook-bodies.mdx` bullet carried the same defect from
|
|
2561
|
+
the other direction — they **ruled out the remedy that works**:
|
|
2562
|
+
|
|
2563
|
+
> readonlyWhen strips even a beforeUpdate-derived value, so an own-hook stamp is NOT a workaround here
|
|
2564
|
+
|
|
2565
|
+
That is the behaviour #9107 removed. The conditional strip now judges the
|
|
2566
|
+
*caller's* entry snapshot, so a value a `beforeUpdate` hook **derives** is not
|
|
2567
|
+
caller-supplied and lands even on a locked record —
|
|
2568
|
+
`engine-readonly-when-derived-writes.test.ts` opens with "THE REPORT: a
|
|
2569
|
+
hook-derived value on a TRUE readonlyWhen field now LANDS", and pins the bulk
|
|
2570
|
+
path on the same terms. Between them the two halves left the author's only
|
|
2571
|
+
working option struck out and a useless one recommended.
|
|
2572
|
+
|
|
2573
|
+
All three hints now name the same two measured remedies — confirm the write
|
|
2574
|
+
only targets records whose predicate is FALSE, or derive the field in a
|
|
2575
|
+
`beforeUpdate` hook on the target object — and refuse elevation explicitly,
|
|
2576
|
+
matching the shape `action-api-update-readonly-when-field` already shipped.
|
|
2577
|
+
The hook hint keeps its stronger, separate reason that `sudo()` is a
|
|
2578
|
+
`TypeError` from a sandboxed body, and now also carries the reason that
|
|
2579
|
+
survives if that one is ever fixed: a system context does not waive the
|
|
2580
|
+
conditional lock either.
|
|
2581
|
+
|
|
2582
|
+
Deliberately **not** flattened: the static-`readonly` hints and docs rows that
|
|
2583
|
+
recommend elevation stay exactly as they are, because for *that* strip
|
|
2584
|
+
elevation is the intended channel. The two disagree for a reason, and a pin now
|
|
2585
|
+
holds them apart.
|
|
2586
|
+
- b2dea86: Retire the "no `current_user` at section level" claim from the three prose sites the
|
|
2587
|
+
re-measurement left unswept
|
|
2588
|
+
|
|
2589
|
+
A form-view **section** `visibleWhen` binds `current_user` today. That was measured and
|
|
2590
|
+
landed for the schema text and the field-rule lint message, but three hand-written sites
|
|
2591
|
+
still taught the retired claim, so an author reading the docs or hitting the gate was told
|
|
2592
|
+
the opposite of what the platform does. Text only — no schema, no verdict and no runtime
|
|
2593
|
+
behaviour moves.
|
|
2594
|
+
|
|
2595
|
+
Re-verified at source in `objectui` before trimming anything, because a prose trim applied
|
|
2596
|
+
to a claim someone had since fixed would silently regress their work:
|
|
2597
|
+
|
|
2598
|
+
- `apps/console/src/components/FormPage.tsx` threads the host shell's scope into
|
|
2599
|
+
`isSectionVisible`, which forwards it to `evalFieldPredicate` (objectui#6110).
|
|
2600
|
+
- `packages/plugin-form/src/ObjectForm.tsx` copies an authored section `visibleWhen` onto
|
|
2601
|
+
the `section-divider` pseudo-field the renderer evaluates with that scope bound
|
|
2602
|
+
(objectui#6111); `SplitForm` / `ModalForm` / `DrawerForm` carry the same line.
|
|
2603
|
+
|
|
2604
|
+
The three sites:
|
|
2605
|
+
|
|
2606
|
+
- `content/docs/ui/views.mdx` listed *"section-level predicates (objectui#6111)"* as a
|
|
2607
|
+
surface that still evaluates the predicate unbound — naming as evidence the very PR that
|
|
2608
|
+
bound it. The same sentence also listed `/forms/:name` as unbound; that route renders
|
|
2609
|
+
inside `InternalFormRoute`, which publishes the session principal and binds normally, so
|
|
2610
|
+
the public `/f/:slug` route is now the only unbound surface named.
|
|
2611
|
+
- `content/docs/protocol/objectui/layout-dsl.mdx` carried the claim four times — a code
|
|
2612
|
+
comment, the binding-root table row, the paragraph under it, and the "two limits" prose —
|
|
2613
|
+
where the card recorded three. All four are re-measured together.
|
|
2614
|
+
- `packages/lint/scripts/check-doc-formula-expressions.mjs`'s field-rule epilogue still said
|
|
2615
|
+
a faulting field-level `visibleWhen` is simply fail-OPEN. Under a host that publishes a
|
|
2616
|
+
scope the predicate RESOLVES instead: the control is hidden in that one form while no
|
|
2617
|
+
server-side gate evaluates a field-level `visibleWhen` at all, so every other reader still
|
|
2618
|
+
returns the value — a silent enforcement gap, and the worse of the two outcomes. The
|
|
2619
|
+
fault-open leg is kept rather than replaced, because it is still what happens wherever no
|
|
2620
|
+
host publishes a scope. The verdict is untouched and the message says why it is now *more*
|
|
2621
|
+
justified.
|
|
2622
|
+
|
|
2623
|
+
Both replacement texts carry the two qualifications the retired claim's correction needs, so
|
|
2624
|
+
"sections bind `current_user`" cannot be read as an authorization primitive: the binding is
|
|
2625
|
+
**client-side only** (nothing on the write path evaluates a form-view field or section
|
|
2626
|
+
`visibleWhen` — it evaluates field `readonlyWhen` / `requiredWhen` and per-option
|
|
2627
|
+
`visibleWhen`, and that is the whole list), and **the scope belongs to the host**, so it is
|
|
2628
|
+
empty on the public `/f/:slug` route and the predicate faults open there.
|
|
2629
|
+
|
|
2630
|
+
The epilogue is a plain string nobody else read — deleting the re-measured clause broke no
|
|
2631
|
+
assertion and turned no gate red, which is exactly how the stale claim outlived its sibling.
|
|
2632
|
+
It is now pinned by a `--self-test` case that scopes itself to the real epilogue (so it
|
|
2633
|
+
cannot satisfy itself from its own literal) and asserts both outcomes plus the surviving
|
|
2634
|
+
fault-open leg. Proven capable of failing by ablation: reverting the clause on disk turns
|
|
2635
|
+
the self-test red, and restoring returns it to green.
|
|
2636
|
+
- 8e03393: Derive the runtime publish gate's context-collection set from `RuntimeStackContext` instead of hand-listing it.
|
|
2637
|
+
|
|
2638
|
+
`CONTEXT_STACK_KEYS` carried `as const satisfies readonly (keyof RuntimeStackContext)[]`, which asks that every entry it names is a real context key (validity) and never that every context key has an entry (completeness) — while the docblock on `RuntimeStackContext` claimed it was "derived from this shape and keeps the two from drifting". A collection declared on the interface but missing from the list was never carried into the per-write snapshot: the host passed it in, the gate dropped it, and every rule resolving references into that collection judged an empty universe and emitted findings that look correct. Measured, adding a collection to the interface left the package building at exit 0 with nothing red inside it.
|
|
2639
|
+
|
|
2640
|
+
The set is now derived from a keyed record typed `{ [K in keyof RuntimeStackContext]-?: true }` — the `-?` mechanism already proven at `@objectstack/metadata-protocol`'s `protocol.ts` — so a new context collection without its row is a type error naming that collection at `tsc --noEmit` and at the build. Declaration order is preserved and pinned, since it feeds both the snapshot's key order and the derived top-level-index alternation. No behaviour change: the same five collections are carried, in the same order.
|
|
2641
|
+
- f394614: fix(lint): give `validate-translation-references` its `_tabs` leg, and pin the object branch against the schema (#13835)
|
|
2642
|
+
|
|
2643
|
+
`validate-translation-references` walked the object branch's `fields`,
|
|
2644
|
+
`fields.*.options`, `_views`, `_sections` and `_actions`, but never `_tabs` —
|
|
2645
|
+
the string did not appear in the rule at all. The result was an **asymmetry**
|
|
2646
|
+
rather than a coverage gap: `collectExpectedEntries` already emits
|
|
2647
|
+
`objects.<object>._tabs.<tab>.label` and `os i18n check` demands a translation
|
|
2648
|
+
for it, while nothing told an author that a `_tabs` key they wrote named a
|
|
2649
|
+
filter preset that no longer exists. Rename a preset and the bundle keeps its
|
|
2650
|
+
old key: the tab bar renders in the source locale above a fully localized grid,
|
|
2651
|
+
with every gate green. This is the same shape #11608 identified for `flows`, one
|
|
2652
|
+
group over, and it was missed only because `_tabs` arrived after the object
|
|
2653
|
+
branch was written.
|
|
2654
|
+
|
|
2655
|
+
The object branch now reports `translation-target-unknown` for a `_tabs` key no
|
|
2656
|
+
page declares, with the usual near-miss suggestion and an enumeration of the
|
|
2657
|
+
real preset names.
|
|
2658
|
+
|
|
2659
|
+
The universe is collected from `page.interfaceConfig.userFilters.tabs[].name`,
|
|
2660
|
+
de-duplicated per object, mirroring `walkObjectTabs`
|
|
2661
|
+
(`packages/cli/src/utils/i18n-extract.ts`) rather than re-deriving it. Three
|
|
2662
|
+
shape facts are asked of the code that owns them:
|
|
2663
|
+
|
|
2664
|
+
- **`ViewTabSchema` has two carriers and only one is live.** The page-only
|
|
2665
|
+
preset bar (`UserFiltersSchema.tabs`, ADR-0047) is what objectui's
|
|
2666
|
+
`TabFilters` draws and what `translateInterfaceTabs` resolves;
|
|
2667
|
+
`ListViewSchema.tabs` has no renderer in either repo. Registering the dead
|
|
2668
|
+
carrier would make legal a key nothing resolves, so only the live one counts —
|
|
2669
|
+
and the hint says so when an object declares no preset bar, since otherwise an
|
|
2670
|
+
author reads the finding as a bug in the rule.
|
|
2671
|
+
- **The object binds through `interfaceConfig.source` first, then the page's own
|
|
2672
|
+
`object`** — the order both the resolver and the extractor use.
|
|
2673
|
+
- **Source-authored pages (`kind: 'html' | 'react' | 'jsx'`) are read here**,
|
|
2674
|
+
unlike in the component walk that skips their derived `regions` cache:
|
|
2675
|
+
`interfaceConfig` is authored metadata at the page root, and neither consumer
|
|
2676
|
+
consults `kind`.
|
|
2677
|
+
|
|
2678
|
+
Also lands the hardening the issue proposed: the object branch's coverage is now
|
|
2679
|
+
**pinned against `ObjectTranslationDataSchema`'s key set**, so the next group
|
|
2680
|
+
added to the shape cannot land without a leg here. The ledger classifies every
|
|
2681
|
+
declared key as either reference-checked or leaf copy, holds its key set equal to
|
|
2682
|
+
the schema's, and requires each `reference-checked` claim to be backed by a
|
|
2683
|
+
bundle that actually produces a finding — so claiming coverage costs a working
|
|
2684
|
+
leg rather than a line in a table.
|
|
2685
|
+
|
|
2686
|
+
Advisory-only, as the whole rule is: findings are warnings, nothing new is
|
|
2687
|
+
refused at publish, and no bundle that resolved before reports now.
|
|
2688
|
+
- f213793: `validate-translation-references` now checks the `flows` group — an authored key naming a
|
|
2689
|
+
flow, screen node or screen field that does not exist warns instead of resolving to nothing
|
|
2690
|
+
|
|
2691
|
+
The rule walked `objects`, `globalActions`, `apps` and `dashboards`; an unrecognised
|
|
2692
|
+
top-level namespace is skipped and never reported, and `flows` was one of them. So a
|
|
2693
|
+
bundle keyed to `flows.<name>.screens.<node_id>.fields.<field_name>` parsed, shipped, and
|
|
2694
|
+
silently resolved to nothing — the wizard rendering its source-locale string while every
|
|
2695
|
+
other label on the screen was translated, which is the exact failure this rule exists for,
|
|
2696
|
+
one namespace over.
|
|
2697
|
+
|
|
2698
|
+
All three levels are exact-match identifiers with an enumerable universe, so the leg
|
|
2699
|
+
mirrors the `dashboards` → `widgets` leg one level further: flow → `Flow.name`, screen →
|
|
2700
|
+
`FlowNode.id` on `type: 'screen'` nodes, field → `ScreenFieldConfig.name`. Findings are
|
|
2701
|
+
`warning`, like every other finding in this rule (ADR-0072 D1 — an orphan key is inert,
|
|
2702
|
+
not broken), and each names the declared universe it resolved against.
|
|
2703
|
+
|
|
2704
|
+
Two shape facts the collector respects, both measured against the schemas rather than
|
|
2705
|
+
assumed — either one read the obvious way would have made the leg a false-positive
|
|
2706
|
+
generator:
|
|
2707
|
+
|
|
2708
|
+
- **Screen nodes nest.** A screen inside an ADR-0031 region (`loop.config.body`,
|
|
2709
|
+
`parallel.config.branches[].nodes`, `try_catch.config.try`/`.catch`) is a real screen the
|
|
2710
|
+
runner pauses on, so the universe is collected through `walkFlowNodes` rather than the
|
|
2711
|
+
flat `flow.nodes`.
|
|
2712
|
+
- **`ScreenConfigSchema` has two mutually exclusive shapes.** An object-form screen
|
|
2713
|
+
(`config.objectName`) renders that object's own create/edit form and declares no
|
|
2714
|
+
`config.fields`; its input labels resolve through `objects.<objectName>.fields.*`, so a
|
|
2715
|
+
field key there is reported with that redirect rather than a bare "not declared".
|
|
2716
|
+
|
|
2717
|
+
A key naming a node that exists but is not a `screen` is diagnosed as the wrong node type,
|
|
2718
|
+
not as a missing node.
|
|
2719
|
+
- d2b5ba8: `view/layout-without-binding` now covers every view type that carries a binding block, and a new `view/tree-without-parent-field` rule catches the silently flat tree
|
|
2720
|
+
|
|
2721
|
+
`checkViewCompleteness`'s `VIEW_BINDING_BLOCKS` table named `kanban` / `calendar` / `gantt` only, while
|
|
2722
|
+
`ListViewSchema.type` has six members with a type-specific binding block. The other three fell through
|
|
2723
|
+
the same trapdoor the rule exists to close: objectui's ListView adapter falls back to literal field
|
|
2724
|
+
names (`timeline` → `startDateField || 'created_at'`, `titleField || 'name'`; `map` →
|
|
2725
|
+
`locationField || 'location'`; `tree` → `labelField || titleField || 'name'`), so a view authored
|
|
2726
|
+
without its block rendered empty — `timeline` drops every row whose start date fails to parse —
|
|
2727
|
+
while `os validate --json` reported `warnings: []` and `valid: true`. Measured both ways: deleting a
|
|
2728
|
+
`timeline` block was silent, deleting the sibling `gantt` block warned as designed.
|
|
2729
|
+
|
|
2730
|
+
- The table now names all six. Each new entry carries a `fix` hint naming the keys that make the block
|
|
2731
|
+
a binding (`timeline`'s two schema-required keys; either coordinate form for `map`; `parentField` +
|
|
2732
|
+
`labelField` for `tree`). Severity stays `warning` (ADR-0078 §1 — the view degrades, it does not die).
|
|
2733
|
+
- `map` is read for its coordinate binding, not merely for block presence: `ListMapConfigSchema` requires
|
|
2734
|
+
no key, so a `map` block declaring neither `locationField` nor the `latitudeField`/`longitudeField`
|
|
2735
|
+
pair is the same unbound view with braces and is warned about at `map.locationField`.
|
|
2736
|
+
- **New rule `view/tree-without-parent-field`** (warning, path `tree.parentField`): a `type: 'tree'` view
|
|
2737
|
+
with no declared `parentField` on an object that carries neither a `tree` field nor a
|
|
2738
|
+
`lookup`/`master_detail` back to itself renders FLAT — every record at depth 0, a correct-looking table
|
|
2739
|
+
whose expand slot never opens. Every `TreeConfigSchema` key is optional, so `tree: {}` satisfies the
|
|
2740
|
+
block check and still renders flat; this rule mirrors objectui's `detectParentField` exactly, so a
|
|
2741
|
+
view the renderer resolves by auto-detection is never warned about.
|
|
2742
|
+
- `checkViewCompleteness(view, boundObject?)` takes the bound object definition as an optional second
|
|
2743
|
+
argument (additive; one-argument callers are unchanged and the tree rule stays silent for them).
|
|
2744
|
+
`@objectstack/lint`'s `validate-functional-completeness` resolves the object by name from
|
|
2745
|
+
`stack.objects` — the list view's own `data.object` first, then the container's binding — and hands
|
|
2746
|
+
it over; no rule logic moved into lint.
|
|
2747
|
+
- `gallery` is measured (`titleField || 'name'`) and deliberately not added: its schema has no binding key
|
|
2748
|
+
to demand. `page` stays deliberately absent (a `page` view refuses at parse via `checkListViewPageMount`).
|
|
2749
|
+
|
|
2750
|
+
Under `os validate --strict` the new warnings are failures, as every warning in this family is.
|
|
2751
|
+
- 4b2cbf7: fix(lint): the three write-set rule messages now state the refusal authors actually get, not a retired driver split (#13858)
|
|
2752
|
+
|
|
2753
|
+
Message text only. Rule ids, severities, match sets and hints are untouched, and
|
|
2754
|
+
no finding changes shape — but a lint's own header states why the prose is
|
|
2755
|
+
governed: *"a lint that misdescribes the failure it is warning about teaches the
|
|
2756
|
+
wrong debugging instinct"*. These three sentences did.
|
|
2757
|
+
|
|
2758
|
+
`validate-hook-body-writes` (the `ctx.api` branch), `validate-action-body-writes`
|
|
2759
|
+
and `validate-flow-node-writes` all told the author that an undeclared write has
|
|
2760
|
+
a **driver-dependent** outcome:
|
|
2761
|
+
|
|
2762
|
+
> on a SQL driver the whole call then fails with a driver-level error far from here; on a schemaless driver (memory, MongoDB) the stray key is persisted
|
|
2763
|
+
|
|
2764
|
+
For the paths those three rules judge, that has not been true since the
|
|
2765
|
+
declared-field door landed (#8682 insert, #8738 update). All three describe a
|
|
2766
|
+
write whose payload is **caller-supplied**, not a mutation of an in-flight
|
|
2767
|
+
`ctx.input`: `ctx.api` is a `ScopedContext` over the running engine, and a flow
|
|
2768
|
+
node hands its `fields` map to the data engine directly. The door refuses a
|
|
2769
|
+
caller-named undeclared key from the object's field map **before any statement is
|
|
2770
|
+
built**, so no driver is reached and there is no split to observe.
|
|
2771
|
+
|
|
2772
|
+
Measured before the prose was rewritten — all three paths, both driver families,
|
|
2773
|
+
through a real QuickJS sandbox, a real `ObjectQL` engine, the real
|
|
2774
|
+
`AutomationEngine` with the real builtin CRUD node executors, real
|
|
2775
|
+
`@objectstack/driver-sql` (better-sqlite3) and real `@objectstack/driver-memory`:
|
|
2776
|
+
|
|
2777
|
+
| path | driver-sql | driver-memory |
|
|
2778
|
+
|---|---|---|
|
|
2779
|
+
| hook body `ctx.api.object(x).update({…})` | `INVALID_FIELD` / 400 | `INVALID_FIELD` / 400 |
|
|
2780
|
+
| action body `ctx.api.object(x).update({…})` | `INVALID_FIELD` / 400 | `INVALID_FIELD` / 400 |
|
|
2781
|
+
| flow `create_record` / `update_record` `fields` | `INVALID_FIELD` / 400 | `INVALID_FIELD` / 400 |
|
|
2782
|
+
|
|
2783
|
+
Every run answered `Unknown field 'stagee' on object 'deal'`; nothing was stored
|
|
2784
|
+
on either family, and the schemaless family kept **no** shadow column — the half
|
|
2785
|
+
the old message promised and the runtime no longer delivers.
|
|
2786
|
+
|
|
2787
|
+
The three messages now name that refusal in the vocabulary the `ctx.input`
|
|
2788
|
+
sibling landed with (`REFUSED at run time — INVALID_FIELD / 400, identically on
|
|
2789
|
+
every driver`), say why the door and not a driver answers, and keep each path's
|
|
2790
|
+
own blast radius: the hook refusal fails the operation that triggered the hook,
|
|
2791
|
+
the action refusal fails the action, and the flow node's refusal is whole — the
|
|
2792
|
+
correctly named fields in the same payload never land either, `create_record`
|
|
2793
|
+
never creates the row, and the step fails the run. That last clause is why the
|
|
2794
|
+
flow rule still gates at `error`; the severity is unchanged.
|
|
2795
|
+
|
|
2796
|
+
`unprovisionedAnchorWriteConsequence()` in the same files is **untouched**: an
|
|
2797
|
+
ADR-0015 external object's injected anchor *is* declared in the registered
|
|
2798
|
+
schema, so it passes the door by construction and the remote database really is
|
|
2799
|
+
what refuses it. That message was already correct.
|
|
2800
|
+
- Updated dependencies [809d417]
|
|
2801
|
+
- Updated dependencies [387e231]
|
|
2802
|
+
- Updated dependencies [f794e4e]
|
|
2803
|
+
- Updated dependencies [cae2169]
|
|
2804
|
+
- Updated dependencies [b812a54]
|
|
2805
|
+
- Updated dependencies [2d4fa75]
|
|
2806
|
+
- Updated dependencies [0e4e51b]
|
|
2807
|
+
- Updated dependencies [e84bbf6]
|
|
2808
|
+
- Updated dependencies [effae80]
|
|
2809
|
+
- Updated dependencies [d62f990]
|
|
2810
|
+
- Updated dependencies [c45d8e6]
|
|
2811
|
+
- Updated dependencies [2e3e8c7]
|
|
2812
|
+
- Updated dependencies [e621291]
|
|
2813
|
+
- Updated dependencies [40a93b5]
|
|
2814
|
+
- Updated dependencies [d5b330d]
|
|
2815
|
+
- Updated dependencies [dda969c]
|
|
2816
|
+
- Updated dependencies [1f45690]
|
|
2817
|
+
- Updated dependencies [277948f]
|
|
2818
|
+
- Updated dependencies [8bdd955]
|
|
2819
|
+
- Updated dependencies [f3bbbef]
|
|
2820
|
+
- Updated dependencies [4f24e9d]
|
|
2821
|
+
- Updated dependencies [474242f]
|
|
2822
|
+
- Updated dependencies [63cd487]
|
|
2823
|
+
- Updated dependencies [bd4aa4e]
|
|
2824
|
+
- Updated dependencies [803eaab]
|
|
2825
|
+
- Updated dependencies [f8e8f03]
|
|
2826
|
+
- Updated dependencies [eae824e]
|
|
2827
|
+
- Updated dependencies [f6fa22c]
|
|
2828
|
+
- Updated dependencies [8a483b3]
|
|
2829
|
+
- Updated dependencies [97bcd99]
|
|
2830
|
+
- Updated dependencies [df59de0]
|
|
2831
|
+
- Updated dependencies [96e25a8]
|
|
2832
|
+
- Updated dependencies [713f83f]
|
|
2833
|
+
- Updated dependencies [77d4b3c]
|
|
2834
|
+
- Updated dependencies [f75a38a]
|
|
2835
|
+
- Updated dependencies [7a25e7d]
|
|
2836
|
+
- Updated dependencies [1fa05a6]
|
|
2837
|
+
- Updated dependencies [c85a265]
|
|
2838
|
+
- Updated dependencies [dcb10a5]
|
|
2839
|
+
- Updated dependencies [773a999]
|
|
2840
|
+
- Updated dependencies [35dffea]
|
|
2841
|
+
- Updated dependencies [776a098]
|
|
2842
|
+
- Updated dependencies [5060877]
|
|
2843
|
+
- Updated dependencies [4f6325d]
|
|
2844
|
+
- Updated dependencies [52954c0]
|
|
2845
|
+
- Updated dependencies [2aa8456]
|
|
2846
|
+
- Updated dependencies [93809a3]
|
|
2847
|
+
- Updated dependencies [7c0d0c3]
|
|
2848
|
+
- Updated dependencies [daae7aa]
|
|
2849
|
+
- Updated dependencies [8dc22d6]
|
|
2850
|
+
- Updated dependencies [279431e]
|
|
2851
|
+
- Updated dependencies [948dd6b]
|
|
2852
|
+
- Updated dependencies [3b4c56c]
|
|
2853
|
+
- Updated dependencies [ae8edd2]
|
|
2854
|
+
- Updated dependencies [e25403c]
|
|
2855
|
+
- Updated dependencies [64baa68]
|
|
2856
|
+
- Updated dependencies [9fa70d7]
|
|
2857
|
+
- Updated dependencies [09db64a]
|
|
2858
|
+
- Updated dependencies [92916e7]
|
|
2859
|
+
- Updated dependencies [a84f3ea]
|
|
2860
|
+
- Updated dependencies [f2eaae8]
|
|
2861
|
+
- Updated dependencies [c09451b]
|
|
2862
|
+
- Updated dependencies [ba64877]
|
|
2863
|
+
- Updated dependencies [7345308]
|
|
2864
|
+
- Updated dependencies [79b6a22]
|
|
2865
|
+
- Updated dependencies [30d96ab]
|
|
2866
|
+
- Updated dependencies [f658793]
|
|
2867
|
+
- Updated dependencies [c95ad19]
|
|
2868
|
+
- Updated dependencies [e58ea8b]
|
|
2869
|
+
- Updated dependencies [4a17645]
|
|
2870
|
+
- Updated dependencies [3795c5f]
|
|
2871
|
+
- Updated dependencies [8ab926b]
|
|
2872
|
+
- Updated dependencies [7317cf2]
|
|
2873
|
+
- Updated dependencies [e25e839]
|
|
2874
|
+
- Updated dependencies [5997207]
|
|
2875
|
+
- Updated dependencies [8b13cc8]
|
|
2876
|
+
- Updated dependencies [4a4a35d]
|
|
2877
|
+
- Updated dependencies [4a4a35d]
|
|
2878
|
+
- Updated dependencies [86e765a]
|
|
2879
|
+
- Updated dependencies [1d7e76a]
|
|
2880
|
+
- Updated dependencies [53dc739]
|
|
2881
|
+
- Updated dependencies [fd289be]
|
|
2882
|
+
- Updated dependencies [03bf7b1]
|
|
2883
|
+
- Updated dependencies [f90e820]
|
|
2884
|
+
- Updated dependencies [18d816a]
|
|
2885
|
+
- Updated dependencies [e8bd715]
|
|
2886
|
+
- Updated dependencies [b91c351]
|
|
2887
|
+
- Updated dependencies [a28a3c0]
|
|
2888
|
+
- Updated dependencies [daeaaf9]
|
|
2889
|
+
- Updated dependencies [c459da6]
|
|
2890
|
+
- Updated dependencies [e914733]
|
|
2891
|
+
- Updated dependencies [1d8ad0f]
|
|
2892
|
+
- Updated dependencies [9738c35]
|
|
2893
|
+
- Updated dependencies [f887e52]
|
|
2894
|
+
- Updated dependencies [881f8d8]
|
|
2895
|
+
- Updated dependencies [3bfa1e6]
|
|
2896
|
+
- Updated dependencies [901355c]
|
|
2897
|
+
- Updated dependencies [34ce8e7]
|
|
2898
|
+
- Updated dependencies [33681ea]
|
|
2899
|
+
- Updated dependencies [4635f3e]
|
|
2900
|
+
- Updated dependencies [ee3595c]
|
|
2901
|
+
- Updated dependencies [b2eab95]
|
|
2902
|
+
- Updated dependencies [93940d4]
|
|
2903
|
+
- Updated dependencies [3a04b01]
|
|
2904
|
+
- Updated dependencies [45b9051]
|
|
2905
|
+
- Updated dependencies [b9e9227]
|
|
2906
|
+
- Updated dependencies [d395692]
|
|
2907
|
+
- Updated dependencies [5894d30]
|
|
2908
|
+
- Updated dependencies [a3765f6]
|
|
2909
|
+
- Updated dependencies [e22158f]
|
|
2910
|
+
- Updated dependencies [7404925]
|
|
2911
|
+
- Updated dependencies [0c2334f]
|
|
2912
|
+
- Updated dependencies [778c59f]
|
|
2913
|
+
- Updated dependencies [d2619fd]
|
|
2914
|
+
- Updated dependencies [6acb11a]
|
|
2915
|
+
- Updated dependencies [33c5fd3]
|
|
2916
|
+
- Updated dependencies [20b0fdb]
|
|
2917
|
+
- Updated dependencies [905019b]
|
|
2918
|
+
- Updated dependencies [a286411]
|
|
2919
|
+
- Updated dependencies [98c0d33]
|
|
2920
|
+
- Updated dependencies [368a82e]
|
|
2921
|
+
- Updated dependencies [a3d5724]
|
|
2922
|
+
- Updated dependencies [93ea19b]
|
|
2923
|
+
- Updated dependencies [9ee2dcf]
|
|
2924
|
+
- Updated dependencies [8cb96ec]
|
|
2925
|
+
- Updated dependencies [8f10a79]
|
|
2926
|
+
- Updated dependencies [6269a55]
|
|
2927
|
+
- Updated dependencies [0fb8760]
|
|
2928
|
+
- Updated dependencies [e5ce2ed]
|
|
2929
|
+
- Updated dependencies [be21955]
|
|
2930
|
+
- Updated dependencies [bc56e18]
|
|
2931
|
+
- Updated dependencies [be21955]
|
|
2932
|
+
- Updated dependencies [a9ee989]
|
|
2933
|
+
- Updated dependencies [4d0d944]
|
|
2934
|
+
- Updated dependencies [15d58db]
|
|
2935
|
+
- Updated dependencies [d63b014]
|
|
2936
|
+
- Updated dependencies [9abe4e4]
|
|
2937
|
+
- Updated dependencies [2cc7122]
|
|
2938
|
+
- Updated dependencies [50d6c92]
|
|
2939
|
+
- Updated dependencies [9e0ba21]
|
|
2940
|
+
- Updated dependencies [311433f]
|
|
2941
|
+
- Updated dependencies [3e5ad08]
|
|
2942
|
+
- Updated dependencies [9abe4e4]
|
|
2943
|
+
- Updated dependencies [b7131f3]
|
|
2944
|
+
- Updated dependencies [e5812fa]
|
|
2945
|
+
- Updated dependencies [7085f90]
|
|
2946
|
+
- Updated dependencies [dee4dd4]
|
|
2947
|
+
- Updated dependencies [ce7e497]
|
|
2948
|
+
- Updated dependencies [51ecb2f]
|
|
2949
|
+
- Updated dependencies [9086761]
|
|
2950
|
+
- Updated dependencies [42a117b]
|
|
2951
|
+
- Updated dependencies [1401ae7]
|
|
2952
|
+
- Updated dependencies [4297fe7]
|
|
2953
|
+
- Updated dependencies [e398863]
|
|
2954
|
+
- Updated dependencies [d16df74]
|
|
2955
|
+
- Updated dependencies [f11fc61]
|
|
2956
|
+
- Updated dependencies [e808890]
|
|
2957
|
+
- Updated dependencies [8f79379]
|
|
2958
|
+
- Updated dependencies [e6ca40e]
|
|
2959
|
+
- Updated dependencies [0c77ea4]
|
|
2960
|
+
- Updated dependencies [52954c0]
|
|
2961
|
+
- Updated dependencies [89eb997]
|
|
2962
|
+
- Updated dependencies [aa5994e]
|
|
2963
|
+
- Updated dependencies [be93457]
|
|
2964
|
+
- Updated dependencies [a65db76]
|
|
2965
|
+
- Updated dependencies [15eb2c9]
|
|
2966
|
+
- Updated dependencies [5691b07]
|
|
2967
|
+
- Updated dependencies [2a6122b]
|
|
2968
|
+
- Updated dependencies [225e769]
|
|
2969
|
+
- Updated dependencies [8af88dd]
|
|
2970
|
+
- Updated dependencies [fb5fbb8]
|
|
2971
|
+
- Updated dependencies [d7b3963]
|
|
2972
|
+
- Updated dependencies [b72db01]
|
|
2973
|
+
- Updated dependencies [dce5cd4]
|
|
2974
|
+
- Updated dependencies [177ebdc]
|
|
2975
|
+
- Updated dependencies [8d237b4]
|
|
2976
|
+
- Updated dependencies [2d2e6f0]
|
|
2977
|
+
- Updated dependencies [2d8dd8d]
|
|
2978
|
+
- Updated dependencies [22d573e]
|
|
2979
|
+
- Updated dependencies [b5a2398]
|
|
2980
|
+
- Updated dependencies [348860c]
|
|
2981
|
+
- Updated dependencies [5383fa6]
|
|
2982
|
+
- Updated dependencies [5b3ff63]
|
|
2983
|
+
- Updated dependencies [1a6a19c]
|
|
2984
|
+
- Updated dependencies [527e050]
|
|
2985
|
+
- Updated dependencies [dd33bf9]
|
|
2986
|
+
- Updated dependencies [4cb2a90]
|
|
2987
|
+
- Updated dependencies [74a7804]
|
|
2988
|
+
- Updated dependencies [53d3689]
|
|
2989
|
+
- Updated dependencies [b3a63d3]
|
|
2990
|
+
- Updated dependencies [033a34c]
|
|
2991
|
+
- Updated dependencies [4d25d22]
|
|
2992
|
+
- Updated dependencies [1ffee51]
|
|
2993
|
+
- Updated dependencies [5ae4303]
|
|
2994
|
+
- Updated dependencies [ece4dad]
|
|
2995
|
+
- Updated dependencies [e9b377e]
|
|
2996
|
+
- Updated dependencies [146f448]
|
|
2997
|
+
- Updated dependencies [735f5c7]
|
|
2998
|
+
- Updated dependencies [a7e18de]
|
|
2999
|
+
- Updated dependencies [366f895]
|
|
3000
|
+
- Updated dependencies [dc75ba8]
|
|
3001
|
+
- Updated dependencies [2182bd1]
|
|
3002
|
+
- Updated dependencies [2a5c1cd]
|
|
3003
|
+
- Updated dependencies [34f60b7]
|
|
3004
|
+
- Updated dependencies [0e68ed2]
|
|
3005
|
+
- Updated dependencies [8beb3de]
|
|
3006
|
+
- Updated dependencies [4a9f461]
|
|
3007
|
+
- Updated dependencies [cce0aa9]
|
|
3008
|
+
- Updated dependencies [e764507]
|
|
3009
|
+
- Updated dependencies [cff17af]
|
|
3010
|
+
- Updated dependencies [39404f3]
|
|
3011
|
+
- Updated dependencies [ca1965f]
|
|
3012
|
+
- Updated dependencies [8619f95]
|
|
3013
|
+
- Updated dependencies [b706af9]
|
|
3014
|
+
- Updated dependencies [fc9ba76]
|
|
3015
|
+
- Updated dependencies [0f94cc7]
|
|
3016
|
+
- Updated dependencies [a11c1a5]
|
|
3017
|
+
- Updated dependencies [71f9cd1]
|
|
3018
|
+
- Updated dependencies [ee17d86]
|
|
3019
|
+
- Updated dependencies [cdbd920]
|
|
3020
|
+
- Updated dependencies [18c432e]
|
|
3021
|
+
- Updated dependencies [3c418c4]
|
|
3022
|
+
- Updated dependencies [fa8715a]
|
|
3023
|
+
- Updated dependencies [a933ed7]
|
|
3024
|
+
- Updated dependencies [b3ca463]
|
|
3025
|
+
- Updated dependencies [a933ed7]
|
|
3026
|
+
- Updated dependencies [0d4a6a8]
|
|
3027
|
+
- Updated dependencies [518d5e5]
|
|
3028
|
+
- Updated dependencies [6643ba1]
|
|
3029
|
+
- Updated dependencies [eeba2ef]
|
|
3030
|
+
- Updated dependencies [ec4c4d2]
|
|
3031
|
+
- Updated dependencies [424f73c]
|
|
3032
|
+
- Updated dependencies [cccbe51]
|
|
3033
|
+
- Updated dependencies [a8d6b1d]
|
|
3034
|
+
- Updated dependencies [e4a7695]
|
|
3035
|
+
- Updated dependencies [87075b1]
|
|
3036
|
+
- Updated dependencies [fc58a99]
|
|
3037
|
+
- Updated dependencies [14cfc00]
|
|
3038
|
+
- Updated dependencies [1c6f7b4]
|
|
3039
|
+
- Updated dependencies [e854a53]
|
|
3040
|
+
- Updated dependencies [dfebfc8]
|
|
3041
|
+
- Updated dependencies [d028b37]
|
|
3042
|
+
- Updated dependencies [122ef38]
|
|
3043
|
+
- Updated dependencies [4a37870]
|
|
3044
|
+
- Updated dependencies [428f9b2]
|
|
3045
|
+
- Updated dependencies [aa7ff56]
|
|
3046
|
+
- Updated dependencies [c41b42e]
|
|
3047
|
+
- Updated dependencies [c4db311]
|
|
3048
|
+
- Updated dependencies [750fff5]
|
|
3049
|
+
- Updated dependencies [c19035e]
|
|
3050
|
+
- Updated dependencies [ececf7a]
|
|
3051
|
+
- Updated dependencies [d173125]
|
|
3052
|
+
- Updated dependencies [8eeca27]
|
|
3053
|
+
- Updated dependencies [8425c17]
|
|
3054
|
+
- Updated dependencies [a5ef1d8]
|
|
3055
|
+
- Updated dependencies [772d5de]
|
|
3056
|
+
- Updated dependencies [ce80ec2]
|
|
3057
|
+
- Updated dependencies [b372318]
|
|
3058
|
+
- Updated dependencies [97a2263]
|
|
3059
|
+
- Updated dependencies [29d0676]
|
|
3060
|
+
- Updated dependencies [0169d49]
|
|
3061
|
+
- Updated dependencies [6bd3231]
|
|
3062
|
+
- Updated dependencies [d2b5ba8]
|
|
3063
|
+
- Updated dependencies [b799ac5]
|
|
3064
|
+
- Updated dependencies [8f74307]
|
|
3065
|
+
- Updated dependencies [d23dc08]
|
|
3066
|
+
- Updated dependencies [038f333]
|
|
3067
|
+
- Updated dependencies [644ad50]
|
|
3068
|
+
- Updated dependencies [0da7cd2]
|
|
3069
|
+
- Updated dependencies [28a5c3e]
|
|
3070
|
+
- Updated dependencies [4bc18e5]
|
|
3071
|
+
- @objectstack/spec@17.3.0
|
|
3072
|
+
- @objectstack/formula@17.3.0
|
|
3073
|
+
- @objectstack/sdui-parser@17.3.0
|
|
3074
|
+
|
|
3
3075
|
## 17.2.0
|
|
4
3076
|
|
|
5
3077
|
### Minor Changes
|