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

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