@objectstack/service-analytics 17.0.0-rc.1 → 17.0.0-rc.3

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,450 @@
1
1
  # Changelog — @objectstack/service-analytics
2
2
 
3
+ ## 17.0.0-rc.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [2e284b2]
8
+ - @objectstack/spec@17.0.0-rc.3
9
+ - @objectstack/core@17.0.0-rc.3
10
+
11
+ ## 17.0.0-rc.2
12
+
13
+ ### Major Changes
14
+
15
+ - 3c7bcc0: feat(spec)!: converge the 11 contracts-vs-domain dual-source type names (#4538)
16
+
17
+ `packages/spec/src/contracts/` hand-wrote parameter/result interfaces whose
18
+ names collided with same-named zod-derived types in the domains — the #4411
19
+ trap, tracked as 11 rows of `dual-source-exports.baseline.json`. Each name was
20
+ judged individually against a three-repo import-level scan (framework, cloud,
21
+ objectui): which declaration actually flows at runtime decides the direction.
22
+ All 11 rows are deleted from the baseline; no name below is exported twice
23
+ anymore.
24
+
25
+ **Converged — `./contracts` now re-exports the domain zod type (same
26
+ declaration on both entries, imports keep compiling from either):**
27
+
28
+ - `NotificationChannel` → `system/notification.zod`'s
29
+ `z.infer<NotificationChannelSchema>` (member sets were identical).
30
+ - `ValidationResult` → `kernel/plugin-validator.zod` (shapes were identical).
31
+ - `HealthStatus` → `kernel/startup-orchestrator.zod` (`details` narrows
32
+ `Record<string, any>` → `Record<string, unknown>`).
33
+ - `PluginStartupResult` → `kernel/startup-orchestrator.zod`. FROM `plugin:
34
+ Plugin` (live object) and `error?: Error` TO the serializable projection
35
+ (`plugin: { name, version? }`-passthrough, `error?: { name, message,
36
+ stack?, code? }`). Neither side had any consumer outside spec; the
37
+ zod-validatable shape wins.
38
+ - `StartupOptions` → `kernel/startup-orchestrator.zod` — the PARSED tier
39
+ (defaults applied). `IStartupOrchestrator.orchestrateStartup` now takes
40
+ `StartupOptionsInput` (the caller-authored all-optional tier, also
41
+ re-exported from `./contracts`). Fix for callers typed to the old
42
+ all-optional `StartupOptions`: rename to `StartupOptionsInput`.
43
+ - `JobExecution` → `system/job.zod`. The system schema's `duration` field is
44
+ RENAMED `durationMs` — that is what every job adapter produces and what the
45
+ `sys_job_run.duration_ms` column round-trips; the schema described records
46
+ nothing ever wrote. Fix: `duration` → `durationMs` when parsing
47
+ `JobExecutionSchema` payloads.
48
+ - `AnalyticsQuery` → `data/analytics.zod`. The domain schema aligned to the
49
+ contract's semantics first: `timezone` LOST its `.default('UTC')` — absence
50
+ is meaningful (the engine resolves org timezone, #1982/#2018; the
51
+ `/analytics` entry always refused to apply that default). The schema is now
52
+ transform-free, so `AnalyticsQuery` ≡ `AnalyticsQueryInput` (both kept
53
+ exported). Fix for code that relied on `.parse()` injecting `timezone:
54
+ 'UTC'`: pass the timezone explicitly or resolve it via the engine chain
55
+ (`selection.timezone ?? context.timezone ?? 'UTC'`).
56
+
57
+ **Renamed — two genuinely different concepts were sharing one name (both
58
+ flow at runtime):**
59
+
60
+ - `./contracts` `DriverCapabilities` → **`AnalyticsDriverCapabilities`**
61
+ (`{ nativeSql, objectqlAggregate, inMemory }`, the analytics strategy-chain
62
+ execution-path probe). The `DriverCapabilities` name now belongs solely to
63
+ the data domain's driver feature-flag record (`DriverCapabilitiesSchema`,
64
+ what `IDataDriver.supports` declares). Fix: importers of the trio from
65
+ `@objectstack/spec/contracts` (or `@objectstack/service-analytics`, whose
66
+ re-export is renamed in lockstep) rename the import; importers who meant
67
+ the driver flags import `DriverCapabilities` from `@objectstack/spec/data`.
68
+
69
+ **Removed — the domain-side declaration was dead (zero import-level consumers
70
+ in framework/cloud/objectui; the #4411 family's last survivors):**
71
+
72
+ - `system` `MetadataExportOptionsSchema` / `MetadataExportOptions` and
73
+ `MetadataImportOptionsSchema` / `MetadataImportOptions` (the
74
+ `output`/`source`-directory bags). The names now have ONE declaration each:
75
+ the `IMetadataService.exportMetadata` / `importMetadata` parameter
76
+ interfaces on `./contracts` (`types`/`namespaces`/`format` and
77
+ `conflictResolution`/`validate`/`dryRun`), which `MetadataManager`
78
+ implements. No tombstone/D2 conversion, deliberately — these are runtime
79
+ option-bag types, not authorable metadata (same reasoning as #4458).
80
+ `@objectstack/metadata` re-exports the two names from `./contracts` now
81
+ (it previously re-exported the dead system-side shapes its own manager
82
+ did not accept).
83
+ - `system` `JobSchedule` (the `= Schedule` back-compat alias). The name's one
84
+ declaration is the `IJobService.schedule` boundary shape on `./contracts`
85
+ (plain-string cron `expression`); the authored metadata type keeps its real
86
+ name `Schedule`. Fix: `import type { JobSchedule } from
87
+ '@objectstack/spec/system'` → `Schedule` (authoring tier) or the
88
+ `./contracts` `JobSchedule` (service boundary), whichever you meant.
89
+
90
+ ### Minor Changes
91
+
92
+ - fa94b2c: fix(service-analytics): a measure a query never reported reads 0 for a count/sum on every merge seam (#4708)
93
+
94
+ A dataset measure carrying its own `filter` runs as a separate grouped
95
+ sub-query and is merged back onto the selected dimensions. A `GROUP BY` over a
96
+ filtered row set emits **no group at all** for a dimension value the filter
97
+ excludes entirely, so the measure comes back **absent**, not `0` — and
98
+ `computeDerived` treats an absent operand as unknowable, so every ratio over it
99
+ goes null too. The cell then renders blank, which is visually identical to "no
100
+ data for this row" and means the opposite.
101
+
102
+ The bias runs the worst possible way: the rows that blank are the ones whose
103
+ numerator matched nothing — the **worst-performing rows**. A `lead_source` that
104
+ won nothing rendered as "no data" while one that won everything rendered fine.
105
+
106
+ The empty-group value is now filled **by aggregate kind** into every measure
107
+ column the assembled grid lists but no query reported:
108
+
109
+ | aggregate | over an excluded group | why |
110
+ | :------------------------ | :--------------------- | :------------------------------------------------------------------ |
111
+ | `count`, `count_distinct` | `0` | "how many rows matched" has an exact answer when the answer is none |
112
+ | `sum` | `0` | the identity element of the empty set |
113
+ | `avg`, `min`, `max` | stays `null` | genuinely undefined — there is nothing to average |
114
+
115
+ Filling all five with `0` would trade this lie for its mirror image, reporting a
116
+ measurement nobody made, so the kinds are judged separately (via
117
+ `emptyGroupValueFor`, shared with the authoring-side coherence checks).
118
+
119
+ **Only cells are filled, never rows.** A dimension value no query reported at
120
+ all has genuinely no data and stays out of the grid.
121
+
122
+ **What changes beyond the measure-scoped seam.** The fill previously ran before
123
+ the `compareTo` merge, and that merge _appends_ a row for every bucket the
124
+ PREVIOUS window had and this one does not. Every base measure on those rows —
125
+ including unfiltered ones — was absent, so a lead source that sold last month
126
+ and nothing this month rendered as "no data" instead of `0`: the same worst-row
127
+ bias, one merge later. The fill now runs after every merge and covers all base
128
+ measures plus their `<measure>__compare` columns.
129
+
130
+ Widgets that worked around this with `?? 0` in the consumer or a `coalesce` in
131
+ the measure can drop it; the coercion belongs in the executor, which is the only
132
+ layer that knows which aggregate produced the gap.
133
+
134
+ **New export.** `fillEmptyGroups(rows, columnAggregates)` is exported from the
135
+ package root beside `mergeByDimensions`, so a host assembling a grid outside
136
+ `DatasetExecutor` can apply the same aggregate-kind rule rather than
137
+ reimplementing it — which is what makes this a `minor` rather than a `patch`.
138
+
139
+ - 328ccc5: fix(security,analytics): scope /analytics/query to the caller's readable records, and refuse a measure over a missing field (#4467, #4437)
140
+
141
+ Two defects on the analytics query path, both found by the v17 verification run
142
+ (#3909 / #4482), both reproduced against a live showcase server before the fix
143
+ and re-verified with the same requests after.
144
+
145
+ ## #4467 — `/analytics/query` applied no record-level scoping
146
+
147
+ `ISecurityService.getReadFilter` documents itself as "the same filter the engine
148
+ middleware AND-s into every find", and exists precisely for paths that bypass
149
+ that middleware — its own doc comment names the analytics raw-SQL path. But the
150
+ chain it mirrors is TWO sibling middlewares: plugin-security's RLS injection and
151
+ plugin-sharing's owner/share visibility filter (`buildSharingMiddleware` AND-s
152
+ `buildReadFilter` into `ast.where` for `find`/`findOne`/`count`/`aggregate`).
153
+ Only the RLS half was ever computed here, and analytics has no other source of
154
+ scope, so the OWD/share predicate simply never existed on that path.
155
+
156
+ Live repro: `showcase_private_note` is `sharingModel: 'private'`; an admin owns
157
+ 5 notes, a member holds read shares on exactly 2 and no `viewAllRecords`.
158
+ `GET /data/showcase_private_note` correctly returned 2 for the member, while
159
+ `POST /analytics/query {measures:['count']}` returned 5 — and adding
160
+ `dimensions:['title']` returned all five titles, i.e. the VALUES of a column
161
+ that caller may not read, not merely a bad count. Any authenticated caller who
162
+ could reach `/analytics` could enumerate the field values of every row of any
163
+ object exposed as a cube, regardless of OWD, sharing rules, or RLS.
164
+
165
+ `getReadFilter` now resolves plugin-sharing's `buildReadFilter` through the
166
+ late-bound `sharing` service and AND-composes it with the RLS filter — the same
167
+ composition the two middlewares reach by both writing into `ast.where`. It also
168
+ computes the ADR-0057 D1 `__readScope` depth that the security middleware
169
+ normally stashes on the context for plugin-sharing to widen its owner-match
170
+ with, using the same `getEffectiveScope` call the middleware makes: no
171
+ middleware runs on this path, and without it a caller granted `unit`/`org` read
172
+ depth would be silently narrowed to `own`. The sharing predicate is resolved for
173
+ every non-system caller AHEAD of the RLS stand-down branches, because those are
174
+ the RLS middleware's own early exits and none of them is a reason to drop a
175
+ sibling middleware's predicate; a sharing-resolution failure denies outright
176
+ rather than falling through to half a scope.
177
+
178
+ **Why `minor` rather than `patch`.** This is an observable behaviour change on a
179
+ public read surface, in the narrowing direction: analytics results that a
180
+ principal could previously read they now cannot. Counts drop, `dimensions`
181
+ groupings lose rows, and any dashboard, report, or export built on
182
+ `/analytics/query` over an owner-private object will show smaller numbers for
183
+ non-superuser principals — correctly, but visibly. Deployments that had (however
184
+ unknowingly) come to depend on the unscoped totals will see them change on
185
+ upgrade, so this warrants more than a patch-level note even though it is a
186
+ security fix. No API signature changed: `ISecurityService.getReadFilter`'s
187
+ declaration is untouched — the implementation merely started honouring the
188
+ contract it already documented.
189
+
190
+ ## #4437 — a measure naming a missing field 500'd with SQLITE_ERROR
191
+
192
+ `inferMeasure('ghost_sum')` maps a suffix convention onto a field name and has
193
+ no way to know the field exists, so it built `SUM(ghost)`, the driver threw
194
+ `no such column`, and the caller got
195
+ `500 {"code":"SQLITE_ERROR","message":"Internal server error"}` — a driver error
196
+ class as the `error.code` for what is a plain typo, which ADR-0112 forbids. A
197
+ dotted spelling took the same path (`measures:['total.sum']` prefix-strips to
198
+ `sum` → `SUM(sum)` → 500). The DATA route has refused the identical mistake with
199
+ a `400 INVALID_FIELD` naming the field since #4315/#4254.
200
+
201
+ `AnalyticsService.ensureCube` now validates each measure's resolved source field
202
+ against the backing object's field names before any SQL is built, and rejects
203
+ with the same envelope the data route produces (`400 INVALID_FIELD` carrying
204
+ `field`, `object`, `param`, `measure`) so one mistake has one shape across
205
+ `/data` and `/analytics`. The new `getObjectFieldNames` config hook reads the
206
+ same schema registry `isRegisteredObject` already consults and the data path's
207
+ own gate reads, so "which fields exist" has a single answer across both routes.
208
+
209
+ The gate is tiered exactly like the #3867 cube-inference gate, deliberately
210
+ narrow: it applies only when the cube's `sql` is a bare object name (an authored
211
+ cube whose `sql` is a real SQL expression has no field list to check against),
212
+ only when the probe answers (no data engine, or an external datasource whose
213
+ columns are not mirrored locally, stands down), and only to measures whose
214
+ source is a bare column — `count(*)` has no source field, and a dotted
215
+ cross-object reference resolves through a join this layer cannot see, so both
216
+ pass through untouched. `id`/`created_at`/`updated_at` are admitted
217
+ unconditionally, matching the data path's `resolveQueryFields`: a gate stricter
218
+ than the engine it guards would reject queries that used to work. Validation
219
+ runs before the cube is registered, so a rejected query leaves no trace in the
220
+ registry — otherwise a retry would find a "registered" cube carrying the bogus
221
+ measure and sail straight into SQL.
222
+
223
+ This half is `minor` for the same envelope reason: a request that used to return
224
+ 500 now returns 400 with a different `code`, which is a visible contract change
225
+ for any caller branching on the response.
226
+
227
+ - 6117f7b: fix(spec,service-analytics): a percentage measure carries its SCALE, so a ratio of 1 is 100% (objectui#3136)
228
+
229
+ A `%` format string says how to PRINT a number, not what scale that number is
230
+ on — and the two readings collide at exactly `1`, which is both "100%" (a 0–1
231
+ ratio at full compliance) and "1%" (a single percentage point). With nothing on
232
+ the wire to tell them apart, renderers guessed from the value's magnitude and
233
+ resolved the collision the wrong way: an SLA / pass-rate dashboard reporting
234
+ `sla_rate = 1` displayed **"1.0%"** — "everything met the SLA" read as "1% met
235
+ the SLA" — on both the KPI card and the dataset table.
236
+
237
+ The scale was never actually unknowable; it just never left the server. A
238
+ measure declaring `derived: { op: 'ratio' }` is a 0–1 fraction _by definition_,
239
+ and a measure aggregating a `percent` field has whatever scale that field
240
+ stores. Both facts sit in metadata the enrichment pass already reads for the
241
+ ADR-0053 currency chain — which walks back to the source field, checks
242
+ `type === 'currency'`, and rides the resolved code onto the result column.
243
+ Percentages got no such treatment. They do now, through the same seam.
244
+
245
+ **`percentScaleOf(field)` (`@objectstack/spec/data`)** is the one place the
246
+ question is answered. A `percent` field stores a FRACTION unless it declares
247
+ `max > 1` (e.g. `min: 0, max: 100`), which marks whole-percent storage — the
248
+ same rule the percent edit widget already writes by, so a value round-trips.
249
+ Non-`percent` fields get no opinion: a plain `number` an author formatted with
250
+ a `%` keeps meaning exactly what their format string says.
251
+
252
+ **`AnalyticsResult.fields[].percentScale`** carries the answer: `'fraction'`
253
+ (`1` ⇒ "100%") or `'whole'` (`1` ⇒ "1%"), absent when the column is not a
254
+ percentage. `queryDataset` sets it from the measure's `derived.op === 'ratio'`
255
+ first, then the source field's scale. `currency` — emitted since ADR-0053 but
256
+ only ever written through a cast — is now declared on the same interface.
257
+
258
+ The config seam `measureCurrency` is renamed **`sourceFieldMeta`** and returns
259
+ `max` alongside `type`/`defaultCurrency`. The old name had already outgrown
260
+ itself: the date-bucketing path reads `type` through it to tell a `date`
261
+ dimension from a `datetime` one, and the percent chain is its third consumer.
262
+
263
+ Renderers that receive `percentScale` must scale by it rather than inferring
264
+ from the value; one that does not receive it (an older server) keeps whatever
265
+ fallback it has, so this is additive on the wire.
266
+
267
+ **Same widget family, second fix: an empty filtered group is a measured zero.**
268
+ A measure-scoped filter can exclude every row of a group the grid still lists,
269
+ and the database reports that by omitting the group from the supplementary
270
+ result — after the merge, indistinguishable from "not measured". For a COUNT or
271
+ a SUM it _is_ measured: the answer is 0. `emptyGroupValueFor(aggregate)`
272
+ (`spec/data/aggregation-policy`) states which aggregates have an identity over
273
+ the empty set, and `queryDataset` fills it in once all supplementary merges are
274
+ done (a later measure's merge can append rows no earlier query saw). So
275
+ "0 of 12 paid" now reports `0` instead of blank, and a ratio built on it
276
+ computes to `0` instead of going null — the difference between a dashboard
277
+ saying "0% met the SLA" and saying nothing at all. `avg`/`min`/`max` keep their
278
+ null: there is nothing to average over an empty group, and flattening that to
279
+ zero would invent a measurement.
280
+
281
+ ### Patch Changes
282
+
283
+ - 2f05139: fix(service-analytics): `compareTo` applies measure-scoped filters, so `<measure>__compare` is the same measure as the column beside it (#4820)
284
+
285
+ A dataset measure declared with its own `filter` is scoped by running a
286
+ supplementary grouped sub-query — `combineFilters(baseFilter, measureFilters[m])`
287
+ — and merging it back by dimension key. The `compareTo` pass did not: it issued
288
+ **one** shifted query over every base measure with only the base filter as its
289
+ `where`, and never consulted `compiled.measureFilters` at all.
290
+
291
+ For a dataset like
292
+
293
+ ```ts
294
+ measures: [
295
+ { name: "revenue", aggregate: "sum", field: "amount" },
296
+ { name: "won_count", aggregate: "count", filter: { stage: "closed_won" } },
297
+ ];
298
+ ```
299
+
300
+ the current-period column was scoped and the comparison column was not — two
301
+ different measures rendered side by side under one label:
302
+
303
+ | # | measures | where | |
304
+ | :-- | :--------------------- | :----------------------- | :------ |
305
+ | 1 | `revenue` | — | current |
306
+ | 2 | `won_count` | `{"stage":"closed_won"}` | current |
307
+ | 3 | `revenue`, `won_count` | **absent** | shifted |
308
+
309
+ `won_count__compare` was therefore a count of **every** opportunity in the
310
+ previous window, inflated by exactly the rows the measure exists to exclude.
311
+ The error runs one way: the comparison period always looks better, so a "won
312
+ deals vs. last month" tile reads as a collapse when nothing went wrong. Only
313
+ filter-scoped measures were affected — the unfiltered ones next to them compared
314
+ correctly, which is what made it survive.
315
+
316
+ The comparison window now runs the **same pass** as the current period —
317
+ unfiltered measures in one shifted query plus one shifted sub-query per
318
+ filter-scoped measure, merged by dimension key — through a single shared
319
+ implementation, so the two paths cannot re-diverge at the next change. The
320
+ dataset filter, the presentation's `runtimeFilter` and the measure's own filter
321
+ compose identically in both windows; the only difference between them is the
322
+ shifted `dateRange`.
323
+
324
+ Numbers reported by existing dashboards change where a filtered measure was
325
+ compared: with 3 won deals this month against 1 won of 5 opportunities last
326
+ month, `won_count__compare` was `5` and is now `1`.
327
+
328
+ Cost: one extra query per filter-scoped measure when `compareTo` is set.
329
+ Selections whose measures carry no filter are untouched and still compare in a
330
+ single shifted query.
331
+
332
+ The empty-group fill (#4708) covers the new seam: a group the measure's filter
333
+ empties in the _previous_ window now reports `0` for a `count`/`sum` compare
334
+ column rather than blanking it, exactly as it already did for the current period.
335
+
336
+ - 9fd9ae7: Init-time service consumption is now declared everywhere, and the declaration is enforced (#4471, ADR-0116). A new CI gate (`check:init-service-contract`) walks every plugin's `init()` call graph — including private helpers, the shape that shipped #4420 — and errors on any init-reachable `getService('X')` of a workspace-provided service that is not covered by `dependencies`, `optionalDependencies`, or `requiresServices`. Eleven previously undeclared init-time consumers (metadata, rest, cli serve plugins, and seven services) now declare `optionalDependencies` on their providers, so the kernel orders them deterministically instead of by registration luck; each still degrades on purpose when the provider is not composed. Plugin authors: a best-effort init-time `getService` must declare its provider in `optionalDependencies` (declared tolerance) — the checker never exempts it.
337
+ - Updated dependencies [430dcc2]
338
+ - Updated dependencies [e6ac4bd]
339
+ - Updated dependencies [80334c7]
340
+ - Updated dependencies [ce5242c]
341
+ - Updated dependencies [a7163ea]
342
+ - Updated dependencies [e6e9379]
343
+ - Updated dependencies [98877c9]
344
+ - Updated dependencies [98877c9]
345
+ - Updated dependencies [e6b1b69]
346
+ - Updated dependencies [ad047d2]
347
+ - Updated dependencies [2826d1e]
348
+ - Updated dependencies [5a84d41]
349
+ - Updated dependencies [20b1a9e]
350
+ - Updated dependencies [203a449]
351
+ - Updated dependencies [ac37fc6]
352
+ - Updated dependencies [4820f55]
353
+ - Updated dependencies [462d9c4]
354
+ - Updated dependencies [7d21581]
355
+ - Updated dependencies [f2445c9]
356
+ - Updated dependencies [23338c3]
357
+ - Updated dependencies [5b843fb]
358
+ - Updated dependencies [b4487aa]
359
+ - Updated dependencies [65ca83a]
360
+ - Updated dependencies [67bf2e2]
361
+ - Updated dependencies [c6d1cb4]
362
+ - Updated dependencies [36030ff]
363
+ - Updated dependencies [6117f7b]
364
+ - Updated dependencies [e533b0b]
365
+ - Updated dependencies [cdf4d9a]
366
+ - Updated dependencies [aee1806]
367
+ - Updated dependencies [c13350b]
368
+ - Updated dependencies [c13350b]
369
+ - Updated dependencies [9ca2d85]
370
+ - Updated dependencies [c13350b]
371
+ - Updated dependencies [891d345]
372
+ - Updated dependencies [a52e2ef]
373
+ - Updated dependencies [5293114]
374
+ - Updated dependencies [20bc357]
375
+ - Updated dependencies [5966c2a]
376
+ - Updated dependencies [2382580]
377
+ - Updated dependencies [d9fa683]
378
+ - Updated dependencies [3c7bcc0]
379
+ - Updated dependencies [4b6cac7]
380
+ - Updated dependencies [7631964]
381
+ - Updated dependencies [ac471a0]
382
+ - Updated dependencies [60ae58e]
383
+ - Updated dependencies [ce92674]
384
+ - Updated dependencies [9f601e8]
385
+ - Updated dependencies [51c5227]
386
+ - Updated dependencies [a4a85c8]
387
+ - Updated dependencies [07a4e26]
388
+ - Updated dependencies [ec975f1]
389
+ - Updated dependencies [eb4204b]
390
+ - Updated dependencies [4f13be2]
391
+ - Updated dependencies [61cc079]
392
+ - Updated dependencies [0e96e46]
393
+ - Updated dependencies [d52d4fe]
394
+ - Updated dependencies [742cebb]
395
+ - Updated dependencies [ce92674]
396
+ - Updated dependencies [cf2c9b7]
397
+ - Updated dependencies [833b512]
398
+ - Updated dependencies [0f9faa2]
399
+ - Updated dependencies [7cf42fe]
400
+ - Updated dependencies [5966c2a]
401
+ - Updated dependencies [f78dd83]
402
+ - Updated dependencies [a2cd18a]
403
+ - Updated dependencies [4638aaa]
404
+ - Updated dependencies [0222d3c]
405
+ - Updated dependencies [071d0dc]
406
+ - Updated dependencies [0a936ea]
407
+ - Updated dependencies [023c00b]
408
+ - Updated dependencies [155507e]
409
+ - Updated dependencies [7bba90b]
410
+ - Updated dependencies [7e05d8e]
411
+ - Updated dependencies [061406d]
412
+ - Updated dependencies [c1f344b]
413
+ - Updated dependencies [9c93465]
414
+ - Updated dependencies [ebb209c]
415
+ - Updated dependencies [63b33e6]
416
+ - Updated dependencies [2a44c1d]
417
+ - Updated dependencies [695cfbd]
418
+ - Updated dependencies [7445149]
419
+ - Updated dependencies [071d0dc]
420
+ - Updated dependencies [0848bea]
421
+ - Updated dependencies [d51bed2]
422
+ - Updated dependencies [b8b3c64]
423
+ - Updated dependencies [0c0fbd9]
424
+ - Updated dependencies [f3141d8]
425
+ - Updated dependencies [5a84d41]
426
+ - Updated dependencies [fd3013a]
427
+ - Updated dependencies [21676eb]
428
+ - Updated dependencies [e336549]
429
+ - Updated dependencies [d40f43a]
430
+ - Updated dependencies [e5e7ee0]
431
+ - Updated dependencies [a2ebea2]
432
+ - Updated dependencies [800bdb0]
433
+ - Updated dependencies [04f1182]
434
+ - Updated dependencies [5647006]
435
+ - Updated dependencies [38f7e4f]
436
+ - Updated dependencies [c57f3cf]
437
+ - Updated dependencies [97faca3]
438
+ - Updated dependencies [ad5fe25]
439
+ - Updated dependencies [ea90179]
440
+ - Updated dependencies [ce92674]
441
+ - Updated dependencies [5ef0b5b]
442
+ - Updated dependencies [48fbacb]
443
+ - Updated dependencies [355e951]
444
+ - Updated dependencies [dadb43f]
445
+ - @objectstack/spec@17.0.0-rc.2
446
+ - @objectstack/core@17.0.0-rc.2
447
+
3
448
  ## 17.0.0-rc.1
4
449
 
5
450
  ### Minor Changes