Repository navigation
Commit 5d5e679
Closes #20943
Clause-②: yes (narrowing)
Ruling D on #20943 (comment `5921156712`, execution parameters),
dispatched by the claim `5921298734`: an analytics cube member's `sql`
is a column reference, and a SQL expression there is refused at parse
with a prescription naming the ADR-0021 dataset form. The showcase's one
expression member, `done_rate`, moves to its dataset in the same PR.
## What changes
**`@objectstack/spec`, `data/analytics.zod.ts`**
- `MetricSchema.sql` and `DimensionSchema.sql` (every member of a cube's
`measures` and `dimensions`) admit exactly the accept set the execution
parameters name: a bare identifier (`amount`), a dotted identifier path
(`account.amount`, `account.owner.region`), and `'*'`. Any other value
is refused at `measures.METRIC.sql` / `dimensions.DIMENSION.sql` with
code `invalid_format`. That covers a CASE expression, an aggregate or a
ratio of aggregates, a quoted or `$`-prefixed spelling, an empty string
and a broken path. A column reference parses byte-identically to before.
- The identifier half is the pattern the readers already use to tell a
column path from an expression: `IDENTIFIER_PATH` in
`native-sql-strategy.ts`, and the field gate's bare-identifier /
identifier-path pair. So the contract admits exactly the values those
readers resolve to fields.
- The rule is a `.regex()`, not a refinement, so the published JSON
Schema carries it as a `pattern`. A first cut used refinements, and the
build's dropped-refinement gate refused it: a JSON-Schema-validated
document would have been judged differently from the parse. The pattern
closes that gap, and `dropped-refinements.baseline.json` is untouched.
- Each prescription opens with the contract sentence. It names the
dataset form: a measure with its own structured `filter` for a
conditional count or sum, and `derived: { op, of: [...] }` over named
measures for a ratio, sum, difference or product. It also states the
ratio's 0–1 scale. The dimension prescription says that a CASE bucket
has no expression form in either layer.
- Neighbouring prescriptions no longer offer "fold the condition into
the metric's own `sql` expression" as a live channel. This covers the
retired metric `filters` guidance, the analytics query `filters`
guidance, the `metric-filters-removed` conversion summary, its D3 entry
`cube-metric-filters-retired`, and its step-18 rationale fragment. All
of them are unreleased major-18 text.
**ADR-0087.** The D3 entry `cube-member-sql-expression-retired`
(`migrations/entries/semantic/`) and its step-18 rationale fragment were
written after merging a `main` that contains #20953, which landed as
`c6b3a01d5d`. `registry.ts` was regenerated by `gen:migration-registry`,
never edited by hand. There is no D2 conversion, because an expression
has no mechanical rewrite into a dataset. There is no
`RETIRED_KEYS_BY_MAJOR` row, because no key left the shape.
**Liveness.** The `analytics_cube` rows `measures.sql` and
`dimensions.sql` stay `live`, re-verified 2026-09-30, with the narrowing
recorded and the field gate's `fieldsOfColumnSql` added to the evidence.
**Generated.** Only `content/docs/references/data/analytics.mdx` moved.
The `authorable-surface`, `api-surface` and `json-schema.manifest`
ratchets are byte-identical, as expected for a value narrowing.
`spec-changes.json` and the upgrade guide stay at protocol 17, so
major-18 entries do not project yet, and both checks are green.
**Showcase.** The `showcase_delivery` cube loses `done_rate`. The
`showcase_task_metrics` dataset gains `done_count` (`aggregate:
'count'`, `filter: { status: 'done' }`) and `done_rate` (`derived: { op:
'ratio', of: ['done_count', 'task_count'] }`, `format: '0.0%'`), in the
same shape as the existing `paid_rate`. No dashboard read the cube's
`done_rate`, so there was nothing to re-pin there.
`test/gap-fill.test.ts` is re-pinned: the cube's measure list, the parse
of the shipped cube, and the dataset's filtered-count-over-count form
plus its `DatasetSchema` parse.
**`@objectstack/service-analytics`.** One test and the README, nothing
else:
- `cube-authored-format-granularity.test.ts` built its custom-SQL
fixture with `CubeSchema.parse`, which now refuses it. The fixture is
built unparsed instead, plus one assertion that the parse refuses it.
The engine-path refusal it pins is unchanged.
- `README.md` no longer tells a reader to fold a per-metric condition
into the metric's own `sql` expression. That text ships in the package,
so it gets a `patch` line in the changeset. This file is outside the
claim's declared surface; see the Acceptance notes.
The gate's stand-down branch in `analytics-service.ts` is not touched.
#20965 remains open for it.
## The fork clause
The fork clause did not fire. The one authored expression member in the
tree is `done_rate`, and its structural equivalent exists and is
measured (next section). A shape-level reading for the seat: a dimension
CASE bucket has no dataset equivalent, because a dataset dimension names
a field and its only bucketing is `dateGranularity`. No authored cube in
the tree carries one. Two `service-analytics` gate fixtures do
(`dimension-source-field-gate.test.ts`,
`where-source-field-gate.test.ts`), but they are built without the parse
to pin the runtime branch #20965 owns. Out-of-repo cubes are NOT
MEASURED.
## The measure-level filter, verified before relying on it
- **Code path.** `compileDataset` reads `m.filter` into
`measureFilters`. `DatasetExecutor` splits filtered measures off with
`splitMeasuresByFilter` and runs one supplementary query per filtered
measure over the base filter combined with that measure's filter. It
then evaluates `derived` on the merged row, with `ratio` as `a / b` and
null on a zero denominator.
- **Unit pins.** The existing `dataset-executor.test.ts` cases cover a
supplementary query for a measure-scoped filter and a derived measure
over filtered and unfiltered dependencies, and
`dataset-compare-measure-filters.test.ts` covers the number a reader
sees. All are green in the `service-analytics` run below.
- **Live.** An ephemeral showcase boot on a private port (`pnpm dev --
--fresh`, torn down after) queried the task dataset through the
analytics dataset door as the seeded admin. `done_count` and `done_rate`
reconciled against the raw `showcase_task` rows in every priority bucket
and in the ungrouped total. `done_rate` equalled `done_count /
task_count`, and the column carried `format: '0.0%'` and `percentScale:
'fraction'`. The cube's meta listed its three remaining measures.
## Tests (final head `fa979c57ed` unless stated)
- `@objectstack/spec`:
- `vitest run --project local src/`: 544 files, 16268 passed, 1 todo.
- `--project local scripts/`: 41 files, 950 passed, 1 skipped (at
`a32fd122dc`; the second merge brought no `scripts/` change under
`packages/spec`).
- The relevant `--project repo` files: 10 files, 300 passed. These are
`step18-rationale-merge`, `conversions-major18-merge`,
`liveness/evidence`, `liveness/proof-registry`,
`cube-member-inner-name-retirement`, `cube-refresh-key-retirement`,
`retired-key-migrate-sentence`, `root-index`, `file-description` and
`category-title`.
- `typecheck` exit 0.
- `@objectstack/service-analytics`: `test` gave 149 files and 3449
passed; `typecheck` exit 0, and the edited test is in its program
(`--listFiles`).
- `@objectstack/example-showcase`: `vitest run` gave 29 files and 387
passed, after rebuilding its closure (60 tasks); `typecheck` exit 0, and
the three edited files are in its program.
- Gates:
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 116 families. All 116 were run, and
`--ran` reconciled them as "116 derived, 116 run, 0 NOT-MEASURED, 0
UNRUN", each with an exit code.
- 115 of them exited 0, including `check:generated`, `check:liveness`,
`check:adr-0087-registration` (reads the new marker),
`check:changeset-no-major`, `check:empty-changeset`,
`check:doc-authoring` and `check:nul-bytes`.
- The other one is `pnpm check:platform-checklist`, which exited 1 on an
anchor this diff does not touch. `areas/identity-auth.json` cites
`plugin-auth/src/auth-plugin.ts#twoFactor`, and that symbol is absent.
This branch's bytes for `packages/plugins/plugin-auth`,
`docs/qa/platform-checklist`, `scripts/check-platform-checklist.mjs` and
`scripts/symbol-anchors.mjs` are identical to the merge base
`05be352596`, so it is `main`'s state, not this diff's.
- Lint, a declared narrowing: `eslint --no-inline-config --format json`
over the 10 changed lintable files gave 10 files, 0 errors and 0
warnings.
- The population comes from `eslint.config.mjs`: the
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` block. The other changed files
are `.md`, `.mdx` and `.json`.
- The file count comes from the JSON output.
- Invariance: the config never enables type-aware linting (no
`parserOptions.project`, no typed rules), so this diff cannot move a
verdict on an untouched file. The whole-repo `pnpm lint` is CI's.
## Ablations (each from the committed state; disk-verified through
`scripts/ablation-replace.mjs`; restore proven by blob hash equal to
HEAD and an empty `git diff HEAD`)
| leg | mutation | result under mutation | restored |
|---|---|---|---|
| A1 | the member `sql` pattern admits anything | new spec pin: 9 failed
/ 6 passed (every refusal, door and pattern case red) | 15 / 15 green |
| A2 | the metric `filters` guidance offers the `sql` expression channel
again | 1 failed (the guidance case) | green |
| A3 | the D3 id renamed in the generated registry region | 1 failed
(the registration case) | green |
| A4 | the showcase `done_count` loses its filter | `gap-fill`: 1 failed
(the dataset-form case) | green |
| A5 | the pattern admits anything, then `@objectstack/spec` rebuilt |
`ablation-dist-preflight` marker present in 20 built files; the
service-analytics parse assertion failed (`expected true to be false`) |
rebuilt; marker absent from all 230 files; tree clean; 14 / 14 green |
The expected direction was "turns red" in all five, and that is what was
observed. No ablation file is left in the tree.
## Acceptance notes
- **Surface breach, declared.**
`packages/services/service-analytics/README.md` is outside the claim's
file surface. The dev contract's rule says published text this change
makes false is fixed in the same round, and the dev contract outranks
the dispatch words where they conflict. The edit is one paragraph, and
the changeset carries `'@objectstack/service-analytics': patch` for it.
The seat can drop both if it prefers #20965 to carry the fix.
- **Not fixed here, governed (Tier H).**
`skills/objectstack-ui/rules/dashboards.md` still escalates past a
dataset to "a hand-authored Cube (raw SQL / explicit joins)" (≈`:78`),
and lists "any custom-SQL metric" as a dataset gap (≈`:70`). Both are
now false: a cube member takes no SQL expression, and joins were already
derived. The natural carrier is the separate Tier H docs PR the ruling
names for ADR-0021's dated note. It is not mixed in here.
- **Not fixed here, internal.**
`docs/qa/platform-checklist/areas/dashboards.json` (the cube meta item,
≈`:976`, `:991`, `:1037`) names `done_rate` among "exactly its four
measures" and as a variant. The cube now declares three. No gate reads
that text. Carrier: the checklist's next revision.
- **Boundary, left as ruled.** `'*'` is admitted on a dimension and on a
non-count measure, because the execution parameters admit it on both
members. Neither was ever a working query. Separately, the `number` /
`string` / `boolean` measure types existed to carry an expression. With
an identifier `sql`, the raw-SQL path emits the column unaggregated and
the ObjectQL path refuses it, so retiring those three types is the
natural follow-up. That is a decision for the seat, not something this
PR does.
- **Not added: a tree-scoped text pin.** The narrowing is enforced at
every parse door. The one authored cube in the tree is parsed at import
(`defineCube`) and pinned by the showcase test. The tree's other
expression members are deliberate unparsed runtime fixtures in
`service-analytics`, owned by #20965's cleanup. A text scan would have
needed a directory exclusion and a new cross-package radius.
- `tsc` does not catch an expression member, because `sql` is still
`string`; the parse is the judge. Stored `analytics_cube` rows and built
artifacts that carry an expression are NOT MEASURED here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent ebb66aa commit 5d5e679
15 files changed
Lines changed: 753 additions & 57 deletions
File tree
- .changeset
- content/docs/references/data
- examples/app-showcase
- src
- data/analytics
- system/translations
- ui/datasets
- test
- packages
- services/service-analytics
- src/__tests__
- spec
- liveness
- src
- conversions
- data
- migrations
- entries/semantic
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
147 | 147 | | |
148 | 148 | | |
149 | 149 | | |
150 | | - | |
| 150 | + | |
151 | 151 | | |
152 | 152 | | |
153 | 153 | | |
| |||
158 | 158 | | |
159 | 159 | | |
160 | 160 | | |
161 | | - | |
| 161 | + | |
162 | 162 | | |
163 | 163 | | |
164 | 164 | | |
| |||
191 | 191 | | |
192 | 192 | | |
193 | 193 | | |
194 | | - | |
| 194 | + | |
195 | 195 | | |
196 | 196 | | |
197 | 197 | | |
| |||
220 | 220 | | |
221 | 221 | | |
222 | 222 | | |
223 | | - | |
| 223 | + | |
224 | 224 | | |
225 | 225 | | |
226 | 226 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
38 | | - | |
39 | | - | |
40 | | - | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
46 | 44 | | |
47 | 45 | | |
48 | 46 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
348 | 348 | | |
349 | 349 | | |
350 | 350 | | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
351 | 363 | | |
352 | 364 | | |
353 | 365 | | |
| |||
1018 | 1030 | | |
1019 | 1031 | | |
1020 | 1032 | | |
| 1033 | + | |
| 1034 | + | |
| 1035 | + | |
| 1036 | + | |
| 1037 | + | |
| 1038 | + | |
| 1039 | + | |
| 1040 | + | |
| 1041 | + | |
| 1042 | + | |
1021 | 1043 | | |
1022 | 1044 | | |
1023 | 1045 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
27 | 41 | | |
28 | 42 | | |
29 | 43 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
| 8 | + | |
7 | 9 | | |
8 | 10 | | |
9 | 11 | | |
| 12 | + | |
10 | 13 | | |
11 | 14 | | |
12 | 15 | | |
| |||
26 | 29 | | |
27 | 30 | | |
28 | 31 | | |
29 | | - | |
| 32 | + | |
30 | 33 | | |
31 | 34 | | |
32 | 35 | | |
| |||
63 | 66 | | |
64 | 67 | | |
65 | 68 | | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
66 | 99 | | |
67 | 100 | | |
68 | 101 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
104 | 104 | | |
105 | 105 | | |
106 | 106 | | |
107 | | - | |
108 | | - | |
109 | | - | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
110 | 111 | | |
111 | 112 | | |
112 | 113 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
274 | 274 | | |
275 | 275 | | |
276 | 276 | | |
277 | | - | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
278 | 283 | | |
279 | 284 | | |
280 | 285 | | |
281 | 286 | | |
282 | 287 | | |
283 | | - | |
| 288 | + | |
| 289 | + | |
284 | 290 | | |
285 | 291 | | |
286 | 292 | | |
| |||
0 commit comments