@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 +445 -0
- package/dist/index.cjs +201 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +154 -12
- package/dist/index.d.ts +154 -12
- package/dist/index.js +200 -40
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|