@objectstack/lint 17.0.0 → 17.2.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 CHANGED
@@ -1,5 +1,856 @@
1
1
  # @objectstack/lint
2
2
 
3
+ ## 17.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 78818ec: Report an unparseable source instead of scoring it CLEAN (#10653).
8
+
9
+ Four validators parsed authored source with `ts.createSourceFile` and never read
10
+ `parseDiagnostics`. That call **cannot throw**, so a source with syntax errors
11
+ came back as a tree built by error recovery, got walked like any other, and
12
+ produced no findings — a source the validator could not read, reported as a
13
+ source with nothing to report. Two of the sites carried a `try/catch` around the
14
+ parse that never once ran.
15
+
16
+ Each now reports what it could not read, as a finding the author receives rather
17
+ than as an exit — a publish-time validator is handed metadata by someone else,
18
+ so ending the process on their input is not its call. Four new advisory
19
+ (`warning`) rule ids, all additive: every finding these rules produce today they
20
+ still produce, including from a partially recovered tree.
21
+
22
+ - `react-page-source-unparseable` — `kind:'react'` page source
23
+ (`validateReactPageProps`)
24
+ - `startup-source-unparseable` — plugin source (`findStartupRegistryVerdicts`)
25
+ - `hook-body-source-unparseable` — L2 hook body (`validateHookBodyWrites`)
26
+ - `action-body-source-unparseable` — L2 action body (`validateActionBodyWrites`)
27
+
28
+ New exports: the four rule-id constants, plus `describeParseFailure`,
29
+ `PARSE_FAILURE_HINT` and the `SourceParseFailure` / `CheckedParse` /
30
+ `CheckedParseOptions` types. `ExtractedHookBodyWriteSet` gains an optional
31
+ `parseFailure`, so a consumer of the extractor can tell "wrote nothing" from
32
+ "could not be read" — the distinction that was missing.
33
+
34
+ Nothing is removed or renamed, and no source that parses gains a finding. A
35
+ stack whose authored sources all parse lints exactly as before; one carrying a
36
+ source with a syntax error gains a warning that names the file, line and column
37
+ instead of silently skipping the checks.
38
+ - def0d3e: Runtime publish-gate findings for collection-resident write types (`object` /
39
+ `permission` / `book`) now key the top-level collection entry in
40
+ `issues[].path` / `advisories[].path` by NAME —
41
+ `objects.acme_invoice.sharingModel` — instead of by the gate's private
42
+ per-write snapshot index (`objects[417].sharingModel`), which no caller could
43
+ resolve: that index numbered an in-memory array a Studio / MCP / REST receiver
44
+ has never seen. Single-member write types keep their trivially-stable
45
+ positional form (`flows[0].nodes[1]…`), and nested positions inside one named
46
+ item (`objects.acme_invoice.indexes[1]`) stay positional — they index the
47
+ author's own document. An entry with no splice-safe name falls back to the
48
+ positional spelling. The accepted metadata set is unchanged; only the spelling
49
+ of the emitted finding `path` changes, and `RuntimeAuthoringIssueSchema.path`'s
50
+ description now states the convention. CLI (`os validate` / `os lint`) output
51
+ is unchanged — there the index resolves against the author's own config file.
52
+ - e2bb237: The SORT axis now asks the #8116 provenance question about a name the blanket
53
+ `SYSTEM_FIELDS` union told it not to flag — new rule `sort-field-unprovisioned`
54
+ (#10474), the twin of `searchable-field-unprovisioned` on the identical index
55
+ (#8404).
56
+
57
+ `validate-sortable-fields` consulted the union and stopped there, so a list view
58
+ ordering by a registry-injected anchor on an ADR-0015 `external` object was
59
+ skipped in silence. The #8999 consumer census recorded that gap with the reason
60
+ that such an object never reaches the union branch at all — skip (2) was believed
61
+ to catch it. **That reason was measured wrong.** `declaredFieldTarget` returns
62
+ `null` on exactly one condition (`fields` missing, unreadable, or naming
63
+ nothing) and nothing in it tests `external`, so the shipped shape — a federated
64
+ object that declares a mapped field map, as `examples/app-showcase`'s
65
+ `showcase_ext_customer` does — is indexed like any other object and lands
66
+ squarely in the skip. The census ledger entry now carries the correction rather
67
+ than the inherited reason.
68
+
69
+ Why the authoring gate is the only door available for it: both runtime doors on
70
+ this axis judge `formula` alone (`UNMATERIALIZED_SORT_TYPES`) — the REST ingress
71
+ `assertSortFieldsExist` (#6994) and the engine's `assertOrderByIsMaterializable`
72
+ (#7095). An injected anchor is a `datetime` or `lookup`, it *is* in `gate.known`
73
+ because the registry injected it into the served schema, and it is undotted, so
74
+ it clears every verdict and reaches the driver. Measured with a real `SqlDriver`
75
+ over better-sqlite3, the object declared exactly as the showcase declares it,
76
+ against a remote `customers` table carrying `[id, name, email, region,
77
+ lifetime_value]` and none of the seven injected anchors:
78
+
79
+ ```
80
+ orderBy name asc -> [c1,c2,c3] desc -> [c3,c2,c1] (a real column: reverses)
81
+ orderBy created_at asc -> [c1,c2,c3] desc -> [c1,c2,c3] asc === desc, 3 rows, no error
82
+ orderBy owner_id asc -> [c1,c2,c3] desc -> [c1,c2,c3] asc === desc, 3 rows, no error
83
+ ```
84
+
85
+ `asc` and `desc` byte-identical while the baseline reverses is what makes it a
86
+ dropped sort rather than a coincidence — the same signature this rule already
87
+ records for `formula`, reached by a second route, except that a formula sort is
88
+ refused at both doors and this one is not. A list view ordered by an anchor with
89
+ no storage answers `200` with the rows in the driver's arbitrary order, on the
90
+ view's first fetch and every fetch after it, which `limit`/`offset` then slice
91
+ into an arbitrary page.
92
+
93
+ `warning`, never `error` and never gating (#4330's cost asymmetry, the call every
94
+ sibling makes): the remote schema is invisible to this pass, so the remote table
95
+ may genuinely carry a `created_at` of its own. Declaring that column — the first
96
+ remedy the shared hint prescribes — silences the finding, because
97
+ `unprovisionedInjectedColumnsFor` excludes an author-declared column of the same
98
+ name (#7859's security direction). The runtime publish gate sorts on severity, so
99
+ this lands as an advisory and refuses no write.
100
+
101
+ Two deliberate narrowings, both pinned:
102
+
103
+ - **Undotted names only** — the one place this axis departs from the SEARCH twin.
104
+ `resolveSearchFields` matches by exact string and drops a dotted entry like a
105
+ typo, but a dotted SORT name is refused by the ingress gate as its own verdict
106
+ (`400 INVALID_SORT`, loudly, on every fetch), so the silent degradation this
107
+ finding reports cannot happen there. Answering would give the SORT axis its own
108
+ dotted verdict, which is exactly the posture the rule shares with the FILTER
109
+ and PROJECTION axes (#4256 / #7532 / #7589) and declines to break.
110
+ - **`checkSortDeclaration`'s new anchor-index parameter is optional**, with the
111
+ same meaning `checkSearchableFieldList`'s carries: an out-of-repo caller that
112
+ never built the index keeps its pre-#10474 answers. Every in-repo caller passes
113
+ it.
114
+
115
+ Also re-ruled, with fresh eyes and on evidence rather than inheritance:
116
+ `validate-translation-references` still correctly asks nothing. It reads the
117
+ union at exactly one site (the `fields.<name>` orphan test), and the key it
118
+ decides about is derived from the *registered* metadata, into which the registry
119
+ injects the anchor on a federated object just as on a local one — so the key
120
+ resolves and the label renders. Warning there would flag a translation that
121
+ works. The blank-column consequence belongs to the surface that renders the
122
+ anchor (`validate-page-field-bindings`, #8340), not to the bundle that names it.
123
+ - adbcbfd: feat(lint): the two list-view field rules reach a standalone list view at the runtime publish gate — `view` writes are now judged by `validateSearchableFields` and `validateSortableFields` (#9313)
124
+
125
+ An `active`-state `view` save through `saveMetaItem` (Studio, REST `/meta` item
126
+ CRUD, an MCP/AI author) is now refused with the existing 422 `invalid_metadata`
127
+ envelope when its list view declares a `sort` or `searchableFields` entry the
128
+ bound object cannot honor — an unknown field name, a virtual (`formula`) sort
129
+ target with no stored column to ORDER BY, or a search narrowing the #4254
130
+ ingress gate would refuse on every toolbar search. Both rules already gated
131
+ `os validate` / `os build` / `os lint`; the runtime door — the only door a
132
+ Studio tenant or an MCP/AI author has — ran neither, and an author writing the
133
+ exact declaration these rules exist to refuse got it accepted.
134
+
135
+ Two halves, because either alone is a silent no-op: the reference-integrity
136
+ suite's registry entry gains `runtimeTypes: ['view']`, and both rules' metadata
137
+ walks gain the SELF rung — a `views[]` entry that IS a flattened standalone
138
+ list overlay (`ViewMetadataSchema`'s list-overlay member: `viewKind: 'list'`,
139
+ no nested `config`), the shape a standalone list view takes on the wire and the
140
+ shape the gate snapshots as `views: [item]`.
141
+
142
+ The suite dispatches per member on this door: a `view` snapshot reaches exactly
143
+ the two list-view field rules (`ReferenceIntegrityRule.runtimeTypes`, default
144
+ `['flow']`), never the members whose resolution universe the per-write snapshot
145
+ does not carry — `validateActionNameRefs` resolving against `stack.actions`
146
+ would otherwise refuse legitimate view writes. CLI behaviour is unchanged (the
147
+ commands run the full suite as before); `flow` snapshots keep every member.
148
+ Measured before crossing: 0 refusals and 0 advisories over 50 shipped
149
+ view-door bodies (11 containers + 39 console-shaped personalization overlays,
150
+ `sort[].id` decorations included) across four authoring lineages — a lower
151
+ bound, as every authored corpus is. Draft saves are untouched (D1), stored rows
152
+ keep being served (ADR-0087 asymmetry), and
153
+ `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still degrades the refusal to a loud log.
154
+ - f1b5ad3: feat(lint): a standalone ViewItem record's nested `config.sort` / `config.searchableFields` reach the runtime publish gate (#10001)
155
+
156
+ An `active`-state `view` save through `saveMetaItem` (Studio, REST `/meta`
157
+ item CRUD, an MCP/AI author) whose body is a standalone ViewItem RECORD —
158
+ `ViewMetadataSchema`'s member 1, `{ name, object, viewKind: 'list', config }`,
159
+ the shape a Studio-saved view takes and the shape objectui's `updateView`
160
+ round-trips on every pin/reorder toggle — is now refused with the existing
161
+ 422 `invalid_metadata` envelope when its `config.sort` / `config.searchableFields`
162
+ declares a field the bound object cannot honor: an unknown name, a virtual
163
+ (`formula`) sort target with no stored column to ORDER BY, or a search
164
+ narrowing the #4254 ingress gate would refuse on every toolbar search. #9313
165
+ closed the same gap for the flattened list overlay, one union member over;
166
+ the record's declarations live one level down, inside `config`, and were
167
+ judged by neither list-view field rule — so a record write carrying
168
+ `config.sort: [{ field: '' }]` published in silence and answered
169
+ `400 INVALID_SORT` (#6994/#7095) on the view's first fetch, every load.
170
+
171
+ Walk-only, by design: #9313 already widened the reference-integrity suite
172
+ entry and exactly these two members onto `view` writes, so this change adds
173
+ the RECORD rung to both twin walks — recognised by the wire union's own
174
+ member discrimination (`viewKind: 'list'` AND a record-shaped `config`; the
175
+ flattened-overlay rung keeps its `no nested config` guard, a strict container
176
+ carries neither key, and a `form` record has no list-field surface), judged
177
+ against `listViewObject(config) ?? record.object` at path
178
+ `views[i].config.sort[…]` / `views[i].config.searchableFields[…]`. The
179
+ per-member granularity split is unchanged: no further suite member crosses
180
+ onto `view`. Measured before shipping: 0 refusals and 0 advisories over 39
181
+ record-shaped console round-trip bodies (one per shipped list surface,
182
+ `config.sort[].id` decorations and `isPinned`/`sortOrder` riding along, the
183
+ shape `saveMetaItem` really stores) across the four shipped stacks — a lower
184
+ bound, as every authored corpus is. Draft saves are untouched (D1), stored
185
+ rows keep being served (ADR-0087 asymmetry), and
186
+ `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still degrades the refusal to a loud log.
187
+
188
+ ### Patch Changes
189
+
190
+ - Updated dependencies [6936d07]
191
+ - Updated dependencies [59eb04d]
192
+ - Updated dependencies [9f05b7d]
193
+ - Updated dependencies [7d2d112]
194
+ - Updated dependencies [5fa0d72]
195
+ - Updated dependencies [02b3b07]
196
+ - Updated dependencies [914c413]
197
+ - Updated dependencies [55809a0]
198
+ - Updated dependencies [52db1d1]
199
+ - Updated dependencies [5649efb]
200
+ - Updated dependencies [2306a76]
201
+ - Updated dependencies [e5ea701]
202
+ - Updated dependencies [a40dcc1]
203
+ - Updated dependencies [def0d3e]
204
+ - Updated dependencies [8d0bb79]
205
+ - Updated dependencies [5acb58d]
206
+ - Updated dependencies [2e3cf95]
207
+ - Updated dependencies [4c93387]
208
+ - Updated dependencies [a037f7c]
209
+ - Updated dependencies [3ee8ddf]
210
+ - Updated dependencies [16cef97]
211
+ - Updated dependencies [a79bd35]
212
+ - Updated dependencies [6ceaa4b]
213
+ - Updated dependencies [15ea214]
214
+ - Updated dependencies [de19489]
215
+ - Updated dependencies [c684d00]
216
+ - Updated dependencies [923c424]
217
+ - Updated dependencies [1ec36b7]
218
+ - Updated dependencies [5f2e54c]
219
+ - Updated dependencies [189373b]
220
+ - Updated dependencies [35ad101]
221
+ - Updated dependencies [ceb33a9]
222
+ - Updated dependencies [73d9795]
223
+ - Updated dependencies [8012960]
224
+ - Updated dependencies [f34f56b]
225
+ - Updated dependencies [f399618]
226
+ - Updated dependencies [75e9301]
227
+ - Updated dependencies [2810695]
228
+ - @objectstack/spec@17.2.0
229
+ - @objectstack/formula@17.2.0
230
+ - @objectstack/sdui-parser@17.2.0
231
+
232
+ ## 17.1.0
233
+
234
+ ### Minor Changes
235
+
236
+ - 13d7864: Dashboard writes are now judged by `validateWidgetBindings` at the runtime publish gate (#7529). A dashboard widget bound to a dataset that resolves to nothing — previously a `200` on both save and publish, failing only as a runtime error on the live board — is refused at **publish** with a located 422 (`INVALID_METADATA`, the offending key path named). Drafts are unaffected: a draft may still hold a forward reference to a dataset not yet authored, and only the draft→active promotion runs the gate.
237
+
238
+ Because rule surfaces are registered per-rule, all six of the rule's error-tier findings now gate a dashboard publish as one reference-integrity class: `widget-dataset-unknown`, `widget-dimension-unknown`, `widget-measure-unknown`, `chart-field-unknown`, `widget-legacy-analytics-unrenderable`, `dashboard-filter-field-unknown`. Warning-tier findings (`table-count-only`, `chart-config-missing`, …) ride the non-blocking `advisories` channel on the save response. Config-authored stacks are unaffected — `os validate` / `os build` / `os lint` already ran this rule; the newly gated population is exactly the `sys_metadata` overlay writes (Studio / REST `/meta` / MCP) that previously bypassed it.
239
+
240
+ The per-write snapshot (`RuntimeStackContext`) now carries the live `datasets` collection so bindings resolve against the real dataset universe — without it every legitimate board would read as dangling. Existing stored rows are untouched (the gate blocks new publishes only), and `OS_ALLOW_UNLINTED_METADATA_WRITES=1` remains the migration-window escape hatch.
241
+ - 8640fb2: `os validate`: a dashboard header `modal` action's target resolves against declared PAGES, only (#9013)
242
+
243
+ `validateDashboardActionRefs` resolved an `actionType: 'modal'` header button's
244
+ `actionUrl` the way objectui's `DashboardView` used to dispatch it: a defined
245
+ action name, a bare object name, or the `<verb>_<object>` prefix form
246
+ (`create_`/`new_`/`add_`/`edit_`/`update_` + a defined object) all passed, and a
247
+ target naming a declared page ERRORED unless it collided with one of those.
248
+
249
+ That mirror is gone. Maintainer ruling objectstack#6739-A (2026-08-09): a
250
+ `type: 'modal'` string target names a PAGE, only — the spec TSDoc, the published
251
+ docs and `defineStack`'s cross-reference walk already said so, and objectui#4764
252
+ / objectui#4782 retired the renderer's object fallback and `DashboardView`'s
253
+ second copy of the prefix convention (enumerated across both repos' corpora:
254
+ zero producers). After that, `os validate` blessed exactly the buttons the
255
+ runtime refuses — the false affordance the rule exists to eliminate — while
256
+ refusing the one shape the runtime serves.
257
+
258
+ **BREAKING** accept-set change on the `os validate` gating tier (landing after
259
+ the v17.0.0 cut; the lockstep launch-window convention ships it as `minor`):
260
+
261
+ - A `modal` header target naming a defined action, a bare object, or a
262
+ `<verb>_<object>` form now **fails** validation. Those buttons already
263
+ dispatch to a named refusal at runtime.
264
+ - A `modal` header target naming a declared page now **passes** — it was
265
+ wrongly refused before.
266
+
267
+ ## FROM → TO
268
+
269
+ ```ts
270
+ // before — passed validation; the runtime now refuses the click
271
+ header: {
272
+ actions: [{ label: 'New Deal', actionType: 'modal', actionUrl: 'create_opportunity' }],
273
+ }
274
+
275
+ // after — name a declared page…
276
+ header: {
277
+ actions: [{ label: 'Intake', actionType: 'modal', actionUrl: 'deal_intake' }], // pages: [{ name: 'deal_intake' }]
278
+ }
279
+ // …or, to open an object's form, use the validated first-class shape
280
+ header: {
281
+ actions: [{ label: 'New Deal', actionType: 'form', actionUrl: 'opportunity.edit' }],
282
+ }
283
+ ```
284
+
285
+ There is deliberately no automatic rewrite: a retired-shape target is a
286
+ name-shaped guess (`create_opportunity` names the page `create_opportunity`, or
287
+ it names nothing — the ruling explicitly declined keeping the prefix), and only
288
+ the author knows whether the button meant a page or an object form.
289
+ `objectstack migrate meta` surfaces the change as a structured TODO (semantic
290
+ entry `dashboard-header-modal-target-page-only`, protocol major 18).
291
+
292
+ <!-- adr-0087: registered dashboard-header-modal-target-page-only -->
293
+ - 8b9eba5: feat(spec): field-level `relatedListFilter` — a declarative default filter for auto-derived related lists (#8704)
294
+
295
+ <!-- adr-0087: not-required (no-migration-prescription) Pure accept-set
296
+ widening: one new optional key joins the existing field-level related-list
297
+ family. Nothing is renamed, retired, narrowed or tombstoned, so there is no
298
+ conversion to register and no retirement registry entry. -->
299
+
300
+ The field-level related-list family (`relatedList` / `relatedListTitle` /
301
+ `relatedListColumns`) gains its fourth member, `relatedListFilter` — closing the
302
+ gap where the only way to filter an auto-derived related list was to abandon the
303
+ auto-derived record page for a hand-written `record:related_list` page
304
+ (maintainer ruling 2026-08-15 on #8704).
305
+
306
+ - **No new filter dialect**: the key carries the canonical Query-DSL
307
+ `FilterCondition` (the same authoring face as a query `where`, dataset scope
308
+ filters, and `summaryOperations.filter`). The FILTER-axis doors therefore
309
+ apply automatically — the schema door refuses bare date-range preset
310
+ comparands in ordering positions at parse (#8793), and the engine doors judge
311
+ the composed query at run time (`formula` keys refused `INVALID_FIELD`,
312
+ #8296).
313
+ - **Contract semantics, pinned**: the declared constraint is AND-composed with
314
+ the parent-relationship condition `{ [referenceField]: parentId }` — an
315
+ authored constraint, never a user-editable suggestion — and the related-list
316
+ tab badge count honors the same composed filter, so counts match visible
317
+ rows. Both clauses are normative in the key's contract text and pinned by
318
+ tests.
319
+ - **`@objectstack/lint`**: the shared authored-filter walk (`FILTER_KEYS`) now
320
+ recognizes `relatedListFilter`, extending the filter-token, empty-combinator
321
+ and preset-comparand rules to the new position.
322
+
323
+ The consumption half (RecordDetailView auto-derivation + tab badge) is
324
+ objectui#4664, `Blocked-by:` this change; until it lands the key is ledgered
325
+ `planned` with an author warning.
326
+ - a777944: feat(spec,lint): refuse a bare date-range preset name in an ordering filter comparand at publish time (#8793 — the ruled C half of #8690)
327
+
328
+ **BREAKING** accept-set narrowing on a published authoring surface, landing
329
+ after the v17.0.0 cut (the lockstep launch-window convention ships it as
330
+ `minor`; the migration prescription is registered under protocol major 18).
331
+
332
+ `last_7_days` / `last_30_days` / `last_90_days` and their ten calendar
333
+ siblings are real, declared preset names — for the dashboard date-filter
334
+ positions, where the console lowers them to `{date-macro}` bounds before any
335
+ query is sent. Authored as a bare filter comparand nothing resolves them:
336
+ measured on #8690, `$gte "last_30_days"` returned HTTP 200 with 0 of 51 rows
337
+ where `$gte "{30_days_ago}"` returned the 38 in-window. The engine now
338
+ refuses the bare name on a declared temporal field at query time
339
+ (`INVALID_FILTER` / 400, PR #8808 — the B half); this change is the
340
+ authoring-time half the same ruling shipped alongside it.
341
+
342
+ **What is refused — ordering positions only, in all three authored filter
343
+ shapes:** a `$gt` / `$gte` / `$lt` / `$lte` comparand or `$between` endpoint
344
+ on every carrier of `FilterConditionSchema` (dashboard widget filter, dataset
345
+ filter, report `runtimeFilter`, page/component filter, rollup filter), a
346
+ `greater_than` / `less_than` / `before` / `after` / `between` view filter
347
+ rule value, and an ordering `[field, op, value]` filter triple (the latter
348
+ two via `@objectstack/lint`'s new gating rule `filter-preset-comparand`,
349
+ which also runs at the runtime publish gate for `dashboard` / `view` /
350
+ `object` / `page` / `flow` writes). The refusal names the offending value,
351
+ the position, and the exact `{date-macro}` window that works.
352
+
353
+ **What stays accepted:** the preset names in the dashboard date-filter
354
+ positions (`dateRange.defaultRange`, a date global filter's `defaultValue`) —
355
+ the only positions any layer ever resolved them; equality and membership
356
+ comparands (`{ period: 'this_quarter' }`, `$in: [...]`) — a select/picklist
357
+ column legitimately stores colliding values, and the engine's field-typed
358
+ door already covers the temporal case; undeclared strings
359
+ (`'not-a-date-at-all'`) — the field-typed engine door owns those; and the
360
+ empty-string cell, which stays its own card by ruling.
361
+
362
+ ## FROM → TO
363
+
364
+ ```ts
365
+ // before — parsed green, returned a silent zero (or 400 at query time since #8808)
366
+ filter: { closed_at: { $gte: 'last_30_days' } }
367
+
368
+ // after — rejected naming the window; write the date-macro spelling
369
+ filter: { closed_at: { $gte: '{30_days_ago}' } }
370
+ // calendar presets prescribe their pair:
371
+ filter: { closed_at: { $between: ['{week_start}', '{week_end}'] } }
372
+ ```
373
+
374
+ `DATE_RANGE_PRESETS` moved to `@objectstack/spec/data`
375
+ (`data/date-range-presets.ts`) with `ui` re-exporting it, so both import
376
+ paths keep working; `DATE_RANGE_PRESET_MACRO_WINDOWS` (the per-preset macro
377
+ window table the refusals quote) and `isDateRangePresetName` are new exports.
378
+
379
+ <!-- adr-0087: registered filter-preset-ordering-comparand-refused -->
380
+ - 73cfddf: fix(lint): the ADR-0091 D3 "delegation row needs a reason" rule is scoped to `sys_user_position` (#9730)
381
+
382
+ `delegated_from` was retired from `sys_user_permission_set` (ADR-0049
383
+ enforce-or-remove, maintainer ruling 2026-08-18), so the security-posture
384
+ lint's D3 dual-audit rule no longer reads the key on that table — linting a
385
+ retired column would imply it still exists, and on that table this rule was
386
+ the column's *only* enforcement, which is exactly the advisory-security shape
387
+ the ruling removed. A seed row that still carries the key is refused loudly
388
+ downstream by the engine's schema preflight (`400 INVALID_FIELD`).
389
+
390
+ The D2 rule (a seed grant whose `valid_until` is already past or unparseable
391
+ is dead on arrival) still covers **both** grant tables — `valid_until` remains
392
+ declared and resolution-enforced on both. Only the two rules' object scopes
393
+ diverge; no rule id, severity or message changed.
394
+ - 1408ae3: feat(lint): the five gating object rules cross the runtime publish gate — `object` writes are now judged by `validateFunctionalCompleteness`, `validateManagedApiMethods`, `lintAutonumberFormats`, `validateRuleCompilability` and `validateRuleSchemaFormats` (#4716)
395
+
396
+ An `active`-state `object` save through `saveMetaItem` (Studio's field editor,
397
+ REST `/meta` item CRUD, an MCP/AI author) is now refused with the existing 422
398
+ `invalid_metadata` envelope when it carries a defect these five rules judge:
399
+ an inert `summary`/`lookup`/`select` shape, a managed-API verb the object's own
400
+ affordances refuse, an autonumber format referencing an unknown field, a
401
+ `format` regex or `json_schema` schema the runtime's own compilers reject, or a
402
+ `json_schema` `format` name ajv would silently drop. All five already gated
403
+ `os validate` / `os build` / `os lint`; the runtime door — the only door a
404
+ tenant overlay row has — ran none of them.
405
+
406
+ Scope is deliberately the five **gating** rules only (the #4716 adjudication):
407
+ the six advisory-tier object rules stay off the runtime surface, so a clean
408
+ save's response is byte-identical and no new advisory volume reaches Studio's
409
+ designer. Draft saves are untouched (D1), stored rows keep being served
410
+ (ADR-0087 asymmetry — the gate's differential blames a write only for what it
411
+ adds), and `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still degrades the refusal to
412
+ a loud log for migration windows.
413
+
414
+ Boot-path note: the two schema-judging rules load ajv lazily, only when the
415
+ judged snapshot actually carries a `json_schema` validation — an ordinary
416
+ field edit still loads no compiler, which `runtime-lazy-deps.test.ts` now pins
417
+ as a three-tier contract (parsers never; ajv never without a schema; ajv
418
+ required, on demand, when one is present).
419
+ - 6f5a449: The runtime publish gate judges a package write against that package's own closure (#9612)
420
+
421
+ The gate handed every rule the tenant's **entire** `objects` collection on every
422
+ publish. That is the wrong validation unit, not merely a large one: a tenant
423
+ that has grown to hundreds of objects is many packages, and judging one
424
+ package's write against all of them asks a question nobody wanted answered.
425
+ Per the maintainer's ruling, the unit is now the package —
426
+ 「客户开发开发,校验是否也应该基于软件包」·「当然这里面要考虑系统对象」.
427
+
428
+ `buildRuntimeWriteSnapshots` accepts an optional `packageScope`
429
+ (`{ packageId, dependencies }`) and reduces `objects` to that closure:
430
+
431
+ - the package being written;
432
+ - the transitive closure of its **declared** `manifest.dependencies` — a
433
+ package's declared dependencies bound what it may reference, so this is a set
434
+ the platform computes exactly rather than estimates;
435
+ - platform / system objects, **unconditionally** — a package legitimately
436
+ references `sys_*` objects it never declares, and a closure that dropped them
437
+ would report unresolved references that are not there;
438
+ - rows carrying no package provenance (tenant-authored overlays), because
439
+ nothing declares what they may reference and so nothing bounds them.
440
+
441
+ `ObjectStackProtocolImplementation` resolves that scope from the package
442
+ registry and passes it through `evaluateRuntimeAuthoringGate`.
443
+
444
+ **A write that names no package, or names one the registry cannot produce,
445
+ narrows nothing** and is judged exactly as before. That direction is the whole
446
+ design: an unresolvable package buys a write *more* validation input, never
447
+ less. There is no branch that skips rules, and none that skips them past a
448
+ size.
449
+
450
+ One behaviour change follows from the unit being right: a **package-scoped**
451
+ write that references an object in a package it never declared a dependency on
452
+ is now judged against a closure that does not contain it, so the reference is
453
+ reported. That is the ruling's intended consequence — such a reference is not
454
+ resolvable by declaration — and it applies only to writes that state a package.
455
+
456
+ Also exported: `narrowObjectsToPackageClosure` and the `RuntimePackageScope`
457
+ type from `@objectstack/lint` and `@objectstack/lint/runtime`, and
458
+ `isSystemObject` from the security-posture rule module so the closure and the
459
+ rules share one reading of what "system" means rather than two.
460
+ - b849e69: fix(lint): ask the provenance question at the fifth blanket-`SYSTEM_FIELDS` read site — `searchableFields` (#8404)
461
+
462
+ `validate-searchable-fields.ts` judged a declared `searchableFields` entry
463
+ against the object-independent `SYSTEM_FIELDS` union, exactly as the four
464
+ filter/page-binding rules did before #8340 wired them to the per-object index.
465
+ Both of its gates were correct about EXISTENCE and structurally blind to
466
+ PROVENANCE: `:345` keeps `searchable-field-unknown` silent for any name in the
467
+ union, and `resolveAllowedSet` goes further — it manufactures a stub meta for
468
+ such an entry so it survives the resolution's existence filter exactly as it
469
+ does at runtime.
470
+
471
+ On an ADR-0015 `external` object the platform registers its injected anchors
472
+ (`owner_id`, `organization_id`, the audit family, …) and provisions no storage
473
+ behind them (#7865 / #8116), so:
474
+
475
+ ```
476
+ searchableFields: ['name', 'owner_id'] // external object
477
+ ```
478
+
479
+ linted clean, the stub kept the entry in the resolved allow-list, and the
480
+ view's `$searchFields` narrowing then scanned a column empty on every record —
481
+ #4830's own failure mode (a narrower search than declared, silently) reached by
482
+ a different route.
483
+
484
+ A new `searchable-field-unprovisioned` rule now warns on such an entry, on the
485
+ object's own canonical set and on a list view's narrowing alike, reusing
486
+ `unprovisionedAnchorCause` / `unprovisionedAnchorHint` so the sentence matches
487
+ the four #8340 rules verbatim rather than becoming a second copy (#4830). WARN,
488
+ never gating, per #4330's cost asymmetry: the remote schema is not visible to
489
+ this pass, so the finding describes a degradation rather than a refusal.
490
+
491
+ **The `:239` stub is KEPT.** It is not incidental — it is what makes the linter's
492
+ resolution agree with the runtime's, which resolves the declared branch against
493
+ the registry field map. Measured by disabling it: the existing "keeps runtime
494
+ parity when the object declares system columns searchable" test goes red
495
+ (`expected [] to have a length of 1 but got +0`), because the declaration
496
+ existence-filters to empty and resolution falls through to the auto-default.
497
+ Dropping it would have been a behaviour change dressed as a warning.
498
+
499
+ The warning is emitted per declared entry in the checker's entry loop, never
500
+ inside `resolveAllowedSet` — that helper reads the OBJECT's declaration and runs
501
+ once per narrowing, so warning there would repeat one object-level fact for
502
+ every view and attribute it to the view's path.
503
+
504
+ `checkSearchableFieldList` takes the index as an OPTIONAL trailing parameter,
505
+ the same shape #8340 gave `checkFieldRefs`: its absence means the caller did not
506
+ build the index and the provenance question goes unasked — the previous
507
+ behaviour, preserved for out-of-repo callers (cloud graph-lint, the AI authoring
508
+ path). Both in-repo callers pass it.
509
+ - 71ac21c: feat(lint): a sharing rule anchored where sharing has nothing to widen is now an authoring-time error (#9698)
510
+
511
+ `validateSharingRuleEnforceability` gains its second arm. It already judged a
512
+ sharing rule's `condition` against the compiler that lowers it; it now judges
513
+ the rule's `object` against the verdict that decides whether the grant can
514
+ exist at all.
515
+
516
+ Two new `error` ids, both decidable from authored metadata before anything
517
+ boots, and both mirroring `SharingService.inertGrantReason` (ADR-0111 D7)
518
+ rather than modelling it:
519
+
520
+ - **`sharing-rule-object-not-shareable`** — the anchor object's effective
521
+ sharing model is `public` (an explicit `sharingModel: 'public_read_write'`,
522
+ or no `sharingModel` on a system object, which ADR-0090 D1 resolves to
523
+ public). Sharing only ever WIDENS an OWD baseline, so on the widest baseline
524
+ there is nothing to widen.
525
+ - **`sharing-rule-object-controlled-by-parent`** — the anchor is a
526
+ master-detail detail, whose visibility is derived from its master
527
+ (ADR-0055). It gets its own id and its own fix-it ("share the master
528
+ record instead"), because `effectiveSharingModel` collapses it onto the same
529
+ `public` verdict while the correct repair is completely different.
530
+
531
+ Both were previously accepted by `SharingRuleSchema`, accepted by `defineRule`,
532
+ seeded into `sys_sharing_rule`, and only then refused — once per boot, as a
533
+ WARN line inside the boot diagnostics block. That WARN is not a sufficient
534
+ diagnostic, and the reason is measured rather than argued: a rule whose criteria
535
+ match no seeded row never reaches `grant`, so it never throws and warns nothing
536
+ while being exactly as dead. The WARN is a function of the DATA; the defect is a
537
+ property of the DECLARATION.
538
+
539
+ **Blast radius, measured through `objectstack build` before deciding the
540
+ severity:** 5 sharing rules are declared in this repo. 3 fire, all of them in
541
+ `examples/app-crm` — `share_high_value_opps_with_managers`,
542
+ `share_active_leads_with_manager` and `share_won_deal_activities`, anchored on
543
+ `crm_opportunity`, `crm_lead` and `crm_activity`, every one of them
544
+ `sharingModel: 'public_read_write'`. They have been failing their boot backfill
545
+ on every boot of that app since they were written, and they are removed here
546
+ under ADR-0049 enforce-or-remove — the same call #9237 made for the two
547
+ equivalent rules in `app-showcase`. The other 2 (app-showcase's, both on
548
+ `private` objects) stay silent, which is the direction that had to be proven
549
+ rather than hoped for.
550
+
551
+ The CRM's smoke test used to assert that these rules existed and were of the
552
+ enforced `criteria` type. Both assertions passed while all three rules enforced
553
+ nothing, so the assertion is replaced by the property their greenness hid: no
554
+ declared rule may be anchored where sharing has nothing to widen.
555
+
556
+ Deliberately NOT judged, because they are not decidable from authored metadata:
557
+ the `owner_id` arm (`owner_id` is injected by the schema registry, so asserting
558
+ it would fail every object that correctly does not declare it by hand), the
559
+ `bypassObjects` arm (plugin configuration, not stack metadata), and the
560
+ federated phantom-anchor arm (a provenance test over that same injected column).
561
+ - 192213f: Three write-surface lint rules now ask provenance, not just membership, before exempting a system column (#8663).
562
+
563
+ `validate-hook-body-writes`, `validate-action-body-writes` and `validate-flow-node-writes` share one `IMPLICIT_FIELDS` set, which is object-INDEPENDENT: it answers "could this name be implicitly writable somewhere", never "did the platform provision a column for it on THIS object". On an ADR-0015 `external` object those diverge — the registry injects `owner_id` / `organization_id` / the audit family onto a federated object exactly as onto a local one, but the remote database owns the schema and no column exists behind them.
564
+
565
+ Each rule now emits a new advisory finding on that path instead of staying silent — `hook-body-write-unprovisioned-anchor`, `action-body-write-unprovisioned-anchor`, `flow-node-write-unprovisioned-anchor` — sharing the `unprovisionedAnchorCause` / `unprovisionedAnchorHint` wording the read-axis rules already use. All three are `warning`: the flow-node rule's existence finding still gates at `error`, and its provenance finding deliberately does not, because the claim is about a remote schema this repo cannot see.
566
+
567
+ An author-DECLARED column of the same name is untouched — on a federated object it maps a remote column the author vouches for. `FlowNodeWriteSeverity` widens from `'error'` to `'error' | 'warning'` accordingly.
568
+ - 42d8990: feat(lint): refuse a list-view `sort` that names a formula field, or no field at all, at authoring time (#9257)
569
+
570
+ <!-- adr-0087: not-required (already-registered engine-find-formula-order-by-refused)
571
+ This rule refuses no shape the runtime accepts — it moves an EXISTING refusal
572
+ earlier. `engine-find-formula-order-by-refused` (semantic, protocol 17) already
573
+ registers the condition and carries the identical FROM → TO prescription
574
+ ("denormalise the value onto the object — a stored field, written when the
575
+ source changes — and sort by that", with `summary` explicitly unaffected); the
576
+ FROM → TO block below restates that entry's remedy for the list-view position
577
+ rather than prescribing a second, different one. The `sort-field-unknown` half
578
+ is covered by `assertSortFieldsExist` (#6994), a REST ingress refusal already
579
+ shipped. Nothing authorable is renamed, retired or tombstoned, and no
580
+ `sys_metadata` row changes shape, so there is no new conversion to register —
581
+ what changes is only WHEN the author is told. -->
582
+
583
+ **BREAKING** accept-set narrowing on a published authoring surface, shipped as
584
+ `minor` under the same lockstep launch-window convention the sibling
585
+ `filter-preset-comparand` refusal used. Measured against the shipped corpus
586
+ before landing at `error`: **56 reachable `sort` declarations across
587
+ `examples/app-showcase`, `examples/app-crm`, `examples/app-todo` and
588
+ `packages/platform-objects`, 0 violations** — so this narrows the accept set
589
+ without failing any metadata that ships today.
590
+
591
+ The SORT axis had a runtime refusal on both doors and no authoring gate. This
592
+ adds the missing half, which is the exact shape #6674 closed for the SEARCH
593
+ axis one axis over.
594
+
595
+ **What was broken.** `ListViewSchema.sort` is
596
+ `z.union([z.string(), Array<{ field, order }>])`, so the field name is a bare
597
+ string and Zod validates only the shape. A list view authored with
598
+ `sort: 'expected_revenue desc'` — a `formula` field — validated, published, and
599
+ reported valid, then answered `400 INVALID_SORT` on **first load and every
600
+ load**: the declared sort is the view's initial fetch, not an optional
601
+ interaction, so the whole view fails with a status the author cannot connect to
602
+ the declaration. Both runtime doors already refuse it — `assertSortFieldsExist`
603
+ (`@objectstack/metadata-protocol`, #6994) at the REST ingress and
604
+ `assertOrderByIsMaterializable` (`@objectstack/objectql`, #7095) on the engine's
605
+ own boundary — and neither can reach the author.
606
+
607
+ **What is refused**, at `error`, on every list-view sort a stack declares
608
+ (`objects[].listViews.*.sort`, `views[].list.sort`, `views[].listViews.*.sort`):
609
+
610
+ - `sort-field-unknown` — the name resolves to no field on the bound object.
611
+ Judged on the head segment, matching the ingress gate's own rule so the two
612
+ doors cannot disagree about which names are unknown.
613
+ - `sort-field-unsortable` — the name is a real field whose type is **virtual**:
614
+ computed on read, no stored column, nothing for any driver to `ORDER BY`. An
615
+ unrefused sort on one returns `asc` and `desc` in byte-identical order.
616
+
617
+ **What stays accepted, and this is the load-bearing half:** `summary` and
618
+ `autonumber` sorts. Virtuality is judged by `isVirtualSearchField` /
619
+ `SEARCH_VIRTUAL_TYPES` (`@objectstack/spec/data`), pinned to `formula` alone —
620
+ the same spec storage fact the search ingress gate, the engine's search
621
+ resolution and the FILTER axis' dotted-head classifier already read. It is
622
+ deliberately **not** the spec's `COMPUTED_VALUE_TYPES`: that set is the WRITE
623
+ contract ("never client-written") and gating a sort with it would refuse the two
624
+ types that sort correctly — `summary` is a `table.float` the engine maintains,
625
+ `autonumber` a `table.string` the engine assigns. Both directions are pinned by
626
+ test, and the predicate boundary itself is pinned alongside them so the two
627
+ "must not flag" cases cannot quietly stop meaning anything.
628
+
629
+ Registry-injected system columns (`created_at`, `owner_id`, …) are skipped:
630
+ they are real at runtime, never appear in authored `fields`, and `created_at` is
631
+ the single most common ordering in the platform's own list views.
632
+
633
+ ## FROM → TO
634
+
635
+ ```ts
636
+ // before — parsed green, published, then 400 INVALID_SORT on every load
637
+ listViews: {
638
+ forecast: { type: 'grid', sort: [{ field: 'expected_revenue', order: 'desc' }] },
639
+ }
640
+
641
+ // after — refused at authoring time, naming the field, the position and the fix
642
+ listViews: {
643
+ // denormalise the computed value onto a stored column and sort by that
644
+ forecast: { type: 'grid', sort: [{ field: 'expected_revenue_stored', order: 'desc' }] },
645
+ }
646
+ ```
647
+
648
+ The rule joins `REFERENCE_INTEGRITY_RULES`, so it runs on `os validate`,
649
+ `os lint` and `os compile` at once rather than being wired per command.
650
+
651
+ ### Patch Changes
652
+
653
+ - 34392a1: docs(lint): the `readonlyWhen` field-rule diagnostic no longer cites `ADR-0057 D10` (#9255)
654
+
655
+ The author-visible consequence text for a faulting `readonlyWhen` predicate said
656
+ "Per ADR-0057 D10 the server is the one that decides". The rule it states is
657
+ correct and unchanged — the server locks the field while the form still renders
658
+ it editable — but the citation does not resolve: `D10` of the ERP-authorization
659
+ `ADR-0057` decides Setup-nav capability surfacing, and the other `ADR-0057`
660
+ (system data lifecycle) carries no D-numbered decisions at all. An author who
661
+ followed the anchor landed on an unrelated decision and had no way to tell
662
+ whether the code or their search was wrong.
663
+
664
+ The diagnostic now states the rule on its own authority, which is where it
665
+ always rested. No behaviour, no message semantics and no rule changed — only
666
+ the traceability claim. Recording the rule as an actual decision is tracked
667
+ separately in #9628.
668
+ - 62b1427: fix(lint): drop the `element:filter` entry from `COMPONENT_FIELD_SPECS` (#9220)
669
+
670
+ The whole `element:filter` element retired at element grain (ADR-0049 — no
671
+ renderer ever shipped for it), so every `ElementFilterProps` key is a
672
+ `retiredKey()` tombstone and no spec-conformant page carries `fields` on it.
673
+ The field-binding rule's job (resolve a field NAME against the object) is not
674
+ the question a retired key raises: an authored key is already reported by name
675
+ with the element-retirement prescription through the #5068 props gate, and the
676
+ binding entry would only add a second finding about a key that no longer
677
+ exists — the #5775/#6629 residue class the package's own
678
+ `component-field-specs-liveness` gate refuses.
679
+ - 818c27c: fix(lint): the three ADR-0120 uniqueness rules name the object in the `where` slot instead of repeating the config path (#9600)
680
+
681
+ `AuthoringFinding` declares two location slots with different jobs — `where`
682
+ ("human-readable location", e.g. `object "leave_request"`) and `path` ("config
683
+ path", e.g. `objects[3].sharingModel`). Three registry adapters set the first
684
+ from the second (`where: f.path`), so every CLI command printed the same
685
+ positional string twice and the only human-readable slot said nothing the `at`
686
+ clause did not already say:
687
+
688
+ ```
689
+ • objects[44].indexes[1]: "sys_account" declares index [provider_id, account_id] with bare `unique: true` …
690
+ rule: unique/unscoped-declared-index at objects[44].indexes[1]
691
+ ```
692
+
693
+ That index is a position in the MERGED object array, which appears in no file
694
+ the author wrote. `unique/unscoped-declared-index`, `unique/double-declaration`
695
+ and `unique/legacy-organization-composite` now spell it the way the rest of the
696
+ table does:
697
+
698
+ ```
699
+ • object "sys_account" · index [provider_id, account_id]: "sys_account" declares index …
700
+ rule: unique/unscoped-declared-index at objects[44].indexes[1]
701
+ ```
702
+
703
+ An index is identified by its `name` when it has one, and otherwise by the
704
+ columns the author actually wrote (`· index [provider_id, account_id]`) — both
705
+ searchable in their source, which a bare ordinal is not.
706
+
707
+ `where` is stated by the rule functions themselves rather than reconstructed in
708
+ the adapter, because only the rule still holds the object it walked. Their
709
+ return type is now `LocatedLintIssue` (a `LintIssue` with a REQUIRED `where`),
710
+ newly exported, so a fourth rule joining this family cannot reach the adapter
711
+ without one — a `f.where ?? f.path` fallback at the adapter would have let the
712
+ positional spelling ship again silently.
713
+
714
+ Display text only, and the rules' population is unchanged: measured over the 45
715
+ object declarations `@objectstack/platform-objects` and
716
+ `@objectstack/metadata-core` ship, the registry produced 1050 findings from the
717
+ same 5 rules before and after, with the count of findings whose `where` was a
718
+ bare config path going 72 to 0. `path` is deliberately untouched and stays
719
+ positional — it is the slot that is supposed to be a config path, and the
720
+ runtime gate's `fingerprint` reads `where` and `path` together, so making
721
+ `where` more specific cannot merge two findings that were distinct.
722
+ - e43b211: fix(spec): the retirement prescriptions state what `os migrate meta` actually does (#9529)
723
+
724
+ Every `retiredKey()` prescription whose surface an ADR-0087 conversion covers
725
+ closed with a maintainer-ruled sentence (2026-08-09, #6856):
726
+
727
+ > Run `os migrate meta --from N` to rewrite existing sources automatically.
728
+
729
+ The command has never rewritten an authored source file. It replays the
730
+ conversion chain over the loaded stack **in memory**, prints the attributed
731
+ mechanical change list (`Applied N mechanical change(s)`, one line per site as
732
+ `path: from → to (conversionId)`), and writes exactly one file — the `--out`
733
+ JSON snapshot, when you ask for it. Every write site in
734
+ `packages/cli/src/commands/migrate/meta.ts` is that snapshot; there is no
735
+ `--write` / `--fix` / in-place flag. So an author who followed the prescription
736
+ got the chain replayed, a printed diff and optionally a JSON document in a shape
737
+ their per-artifact `.ts` modules are not written in — and then still edited every
738
+ file by hand, with nothing in the message saying so.
739
+
740
+ Under the maintainer's ruling of 2026-08-18 the sentence is withdrawn in favour
741
+ of an honest one, class-wide:
742
+
743
+ > Run `os migrate meta --from N` to list the mechanical edits for existing
744
+ > sources; apply them by hand.
745
+
746
+ The partial-value conversions keep their two-clause shape, reworded the same way
747
+ (`… to list the mechanical edits for the \`1y\` case; the other durations are
748
+ reported for you to re-state.`). Behaviour is unchanged in both packages — this
749
+ is message text only, and no accept/reject verdict moves.
750
+
751
+ The claim is withdrawn from every shipped site, not only the canonical sentence:
752
+ the variant phrasings in tombstone and conversion-registry prose ("rewrites
753
+ author sources", "rewrites it for you", "only `os migrate meta` rewrites
754
+ sources") go with it, as do the upgrade-path statements in the hand-written docs
755
+ (`upgrading.mdx` now carries the same "does not rewrite your source files" fact
756
+ the `objectstack-upgrade` skill already told operators). The class-wide pin
757
+ `packages/spec/src/shared/retired-key-migrate-sentence.test.ts` moves in
758
+ lockstep and now holds **both** directions: the new sentence is required where a
759
+ prescription names the command, and the withdrawn claim is a hard failure
760
+ wherever it reappears — including in a prescription that spells the bare command
761
+ without `--from N`, which the sentence-shape check alone would not have seen.
762
+
763
+ The in-place AST codemod that would make the original claim true is commissioned
764
+ separately for v18 (#9591); when it lands, the sentence may be restored by
765
+ editing that one pin in the same PR.
766
+ - Updated dependencies [56656aa]
767
+ - Updated dependencies [07e630e]
768
+ - Updated dependencies [2f65b1b]
769
+ - Updated dependencies [720ee95]
770
+ - Updated dependencies [f287435]
771
+ - Updated dependencies [9aa8890]
772
+ - Updated dependencies [7c9c1dd]
773
+ - Updated dependencies [75b7c24]
774
+ - Updated dependencies [d5552ca]
775
+ - Updated dependencies [d9813a9]
776
+ - Updated dependencies [8640fb2]
777
+ - Updated dependencies [2420641]
778
+ - Updated dependencies [2ad91c3]
779
+ - Updated dependencies [f57fb38]
780
+ - Updated dependencies [00777a0]
781
+ - Updated dependencies [d491625]
782
+ - Updated dependencies [420804d]
783
+ - Updated dependencies [716ac9b]
784
+ - Updated dependencies [62b1427]
785
+ - Updated dependencies [7ea1372]
786
+ - Updated dependencies [23abe27]
787
+ - Updated dependencies [985a9cd]
788
+ - Updated dependencies [a8189ae]
789
+ - Updated dependencies [26e70fb]
790
+ - Updated dependencies [42b05af]
791
+ - Updated dependencies [2b292ce]
792
+ - Updated dependencies [abcf853]
793
+ - Updated dependencies [8b9eba5]
794
+ - Updated dependencies [d575779]
795
+ - Updated dependencies [94f7ef8]
796
+ - Updated dependencies [c5ac5e4]
797
+ - Updated dependencies [a777944]
798
+ - Updated dependencies [dd88e1c]
799
+ - Updated dependencies [856527c]
800
+ - Updated dependencies [870f710]
801
+ - Updated dependencies [79c46da]
802
+ - Updated dependencies [7ff3975]
803
+ - Updated dependencies [29d055b]
804
+ - Updated dependencies [65589d6]
805
+ - Updated dependencies [2c86fe3]
806
+ - Updated dependencies [e196c6a]
807
+ - Updated dependencies [4ab7523]
808
+ - Updated dependencies [19539b4]
809
+ - Updated dependencies [11b779e]
810
+ - Updated dependencies [739fe5b]
811
+ - Updated dependencies [4bfe1a5]
812
+ - Updated dependencies [2065e31]
813
+ - Updated dependencies [b69d0f5]
814
+ - Updated dependencies [4d47afe]
815
+ - Updated dependencies [e4e5c6e]
816
+ - Updated dependencies [9a56784]
817
+ - Updated dependencies [d00d2f6]
818
+ - Updated dependencies [df0c12d]
819
+ - Updated dependencies [d31785f]
820
+ - Updated dependencies [c308a4f]
821
+ - Updated dependencies [e2899f6]
822
+ - Updated dependencies [3851f87]
823
+ - Updated dependencies [2a29caa]
824
+ - Updated dependencies [09a6eee]
825
+ - Updated dependencies [1a7f907]
826
+ - Updated dependencies [cd455c8]
827
+ - Updated dependencies [30d3752]
828
+ - Updated dependencies [c80e7ae]
829
+ - Updated dependencies [09a9a8a]
830
+ - Updated dependencies [07026cf]
831
+ - Updated dependencies [5d4f3d5]
832
+ - Updated dependencies [4d80e8b]
833
+ - Updated dependencies [30b1c63]
834
+ - Updated dependencies [079b457]
835
+ - Updated dependencies [e43b211]
836
+ - Updated dependencies [890b38f]
837
+ - Updated dependencies [8bee54b]
838
+ - Updated dependencies [7a537ce]
839
+ - Updated dependencies [593c4bf]
840
+ - Updated dependencies [ff08691]
841
+ - Updated dependencies [60e0f90]
842
+ - Updated dependencies [90c5285]
843
+ - Updated dependencies [7901b2d]
844
+ - Updated dependencies [56bca91]
845
+ - Updated dependencies [79394d7]
846
+ - Updated dependencies [730fd9a]
847
+ - Updated dependencies [44bc51d]
848
+ - Updated dependencies [73cfddf]
849
+ - Updated dependencies [d634e66]
850
+ - @objectstack/spec@17.1.0
851
+ - @objectstack/formula@17.1.0
852
+ - @objectstack/sdui-parser@17.1.0
853
+
3
854
  ## 17.0.0
4
855
 
5
856
  ### Minor Changes