Commit c8dd8dd
Part of #20282
Clause-②: yes (narrowing)
Stage 2 of #20282, under claim `5886559774`
(`session_014EJ1ED8X4MMrT18BhVx4tx`, `domain:spec` seat 2). An AUTHORED
analytics cube's `measures.format` and `dimensions.granularities` now
reach the readers a compiled dataset already reaches. `refreshKey` is
measured only, and nothing is built for it. The card stays open for
stage 3 (descriptions) and for the `refreshKey` decision.
## What changes
One Cube shape has three producers: authored cubes
(`AnalyticsServiceConfig.cubes`, which the CLI threads from
`analyticsCubes`), compiled datasets, and ad-hoc inference. Until now,
both keys were read only on the compiled-dataset path.
- **`measures.format`**
- `analytics-service.ts#withDeclaredMeasureFormats` runs in `queryIn`,
beside the SQL-echo gate.
- Every measure column a query names now carries its cube measure's
declared `format` as `fields[].format`. This holds for every strategy
(NativeSQL, ObjectQL, the delegated fallback) and for both member
spellings.
- A measure that declares no format gets no key. A value already on the
column is never replaced.
- On the dataset door, the value read is the compiler's copy of the
dataset measure's `format`, the same value `enrichResultColumns` writes
anyway.
- `GET /analytics/meta` is unchanged. See the premise checks below.
- **`dimensions.granularities`**
- `analytics-service.ts#withDeclaredGranularityDefaults` runs on
`query()` and on the `generateSql()` dry run, before the source-field
gates and strategy selection.
- It reads `dataset-executor.ts#declaredDefaultGranularity`, a new
function extracted from `granularityOf`, which now calls it too. Both
producers are therefore read by one rule.
- The rule, exactly the compiled-dataset path's:
- a single-entry list is the default bucket for a time dimension the
query groups by without stating a granularity;
- a stated granularity always wins;
- a granularity outside the list is not refused;
- a list of two or more states no default;
- a `timeDimensions` entry that carries only a `dateRange`, for a
dimension the query does not group by, stays a filter.
- **Spec** (`packages/spec/src/data/analytics.zod.ts`, after PR #20616
merged; `origin/main` merged first through
`scripts/pm/os-regen-merge.sh` as `d963f30e33`)
- `MetricSchema.format` and `DimensionSchema.granularities` gain
describes that state the enforcement.
- The metric's example values move from the names "currency" and
"percent" to numeral patterns, the vocabulary the `fields[].format` slot
documents.
- `content/docs/references/data/analytics.mdx` is regenerated with
`gen:docs`.
- **Ledger**
- Both rows in `packages/spec/liveness/analytics_cube.json` go `dead` →
`live`. Each cites its readers as `file#symbol` and the CLI threading
producer.
- The file note's two sentences that named both keys as dead are
rewritten.
- `state-counts/analytics_cube.md` is regenerated with
`gen:liveness-counts`: live/dead goes from 18/9 to 20/7.
- The `analytics_cube` Notes cell in `liveness/README.md`, which listed
both keys among the dead, is rewritten.
- **Changeset**: `@objectstack/spec` minor and
`@objectstack/service-analytics` minor, with a `**BREAKING**` sentence.
## Clause-② (measured arm): yes (narrowing)
- **Widening:** two authored keys take effect, and `fields[].format` is
populated for authored cubes. The contract already declares that member.
- **Narrowing, measured:**
- Method: a throwaway service-seam probe, run once on head and once with
the fill ablated through `scripts/ablation-replace.mjs` (round 1 at
`958251b6ac`; round 3 at `e1383be04d`, blob `9d77adcd798f` →
`94b1bc63165a`, restored to HEAD). The probe is not committed.
- Cube: an authored cube whose `placed_at` declares `granularities:
['month']`, whose `shipped_at` declares two intervals, and (round 3)
which declares `joins: { account: { name: 'crm_account' } }`.
| request (authored cube; `placed_at` declares `['month']`, `shipped_at`
two intervals; `joins: { account }` where named) | fill ablated (= base
behaviour) | head |
|:--|:--|:--|
| custom-SQL measure grouped by `placed_at` | 200 (raw SQL) | **400
`INVALID_FIELD`**, custom-SQL measure (`resolveMeasureAggregation`) |
| cross-object measure (`sum` of `account.balance`) grouped by
`placed_at` | 200 | **400 `INVALID_FIELD`**, cross-object measure
(`planCrossObject`) |
| `count` by `placed_at` with a `where` field over `account` | 200 |
**400 `INVALID_FIELD`**, cross-object filter, `param: where` |
| `count` grouped by a one-interval time dimension over `account`
(`account.created_at`) | 200 | **400 `INVALID_FIELD`**, cannot bucket a
cross-object time dimension |
| `count` by `placed_at` beside a multi-hop dimension
(`account.owner.region`) | 200 | **400 `INVALID_FIELD`**, single-hop
only |
| `avg` by `placed_at` beside a cross-object dimension
(`account.industry`) | 200 | **400 `INVALID_FIELD`**, non-recombinable
measure |
| `count_distinct` by `placed_at` beside a cross-object dimension | 200
| **400 `INVALID_FIELD`**, non-recombinable measure |
| host whose `queryCapabilities` offers raw SQL only (a hand override;
`AnalyticsServicePlugin` wires both): plain `count` grouped by
`placed_at` | 200 | **"No strategy can handle query"** (every newly
bucketed query) |
| control: `count` by `placed_at` beside a cross-object dimension
(recombinable) | 200 | 200 |
| control: `avg` by `placed_at`, no cross-object member | 200 | 200 |
| control: custom-SQL measure grouped by `shipped_at` (two intervals) |
200 | 200 |
- Every 400 is byte-identical (code, status, message, member, param,
cube) to what the same request with `granularity: 'month'` stated by
hand already got: the fill hands the engine path the very query a
hand-stated granularity does. The class is the engine aggregate path's
whole refusal set as the new bucketing reaches it, read from
`ObjectQLStrategy.planCrossObject` and `resolveMeasureAggregation`: a
custom-SQL measure; and, on a cube whose members resolve through
`joins`, a measure or `where` field over a joined object, a
`timeDimensions` entry over a joined object, a multi-hop dimension, and
an `avg` / `count_distinct` measure beside a dimension over a joined
object. `planCrossObject`'s two dataset-definition arms read only
compiled-dataset scope, so they cannot fire on an authored cube. Two
pins: `DECLARED NARROWING` (custom-SQL) and `DECLARED NARROWING, joined
cube` (cross-object measure).
- The changeset carries the `**BREAKING**` sentence (the class and its
remedy) and the disposition `registered
analytics-cube-single-granularity-default-enforced`, a new ADR-0087 D3
semantic entry (round 2, seat note `5889752648` Q4 = B).
- `check-adr-0087-registration`: `✓ 1 declared-breaking changeset(s),
each carrying an ADR-0087 disposition`.
- Round 2 registered it:
`18.analytics-cube-single-granularity-default-enforced.ts` tells authors
that a one-interval list is now a default bucket, including one the
protocol-18 conversion `cube-sub-day-granularities-removed` minted, and
(round 3) which queries grouped by it the engine path refuses: the whole
class above, plus every newly bucketed query on a raw-SQL-only host.
`registry.ts` is regenerated; `spec-changes.json` and the upgrade guide
were regenerated with no change, because neither projects major-18
entries yet.
## Premise checks, against `origin/main` `7510663c87`
- **`format` on `CubeMeta`: not done, because the premise does not
hold.**
- The dataset path surfaces `format` only through `fields[]`. A compiled
dataset's `getMeta` projection is `{ name, type, title }` too.
- `AnalyticsMetadataResponseSchema` records the narrowing for this
(`#6442`).
- `content/docs/api/data-api.mdx` already sends clients to `fields[]`
for `format`.
- The spec contract files (`contracts/analytics-service.ts`,
`api/analytics.zod.ts`) are outside this claim's surface.
- **`granularities` refusal: none invented.**
- The dataset path never compares a requested granularity against the
list, so there is no refusal to mirror.
- What a multi-entry list should mean ("Supported Granularities") is an
open fork in the report.
- **`refreshKey` census** (tree `958251b6ac`; `packages/services`,
`packages/drivers`, `packages/rest`, non-test):
- `refreshKey`: 0 hits. The repo-wide control finds 9 files.
- Pre-aggregation, materialized-view and rollup terms: 7 hits, all
unrelated (automation subflow rollups, and a driver-sql built-in column
flag).
- `ICacheService` consumers: plugin-auth rate-limit and secondary
storage, runtime inbound rate limit, dispatcher counter store, sms. None
is in analytics.
- service-analytics reads no job, cache or scheduler service. Its one
cache is the request-scoped label map in
`dimension-labels.ts#withLabelFetchCache` (the lit control, 1 hit).
- A scheduler exists (`service-job`'s `IJobService`: cron, interval and
db adapters), and nothing in analytics uses it.
- Nothing exists that could key on `refreshKey`, so building a cache is
a separate card.
## Verification
Final head `162e6c0f01` (round 3; merge base `f4ce10c89d`), unless a
line says otherwise. Builds and test runs went through
`os-verify-lock.sh`. The `check:*` gates and eslint ran outside it, as
the lock's scope prescribes. So did `gen:docs`, after two lock
acquisitions for `check:generated --fix` timed out in the queue (exit
99).
- **Round 3 (`162e6c0f01`):** service-analytics 136/3185 (includes the
new joined-cube pin), typecheck clean; runtime REST pin 1/3; spec
`src/migrations` 3/161; `migrate-meta-engine-guidance.test.ts` 3/3; spec
`--project repo` 43/761. Lit/dark: with the fill ablated, the pin file
goes 7 red (the 6 prior bucketing and narrowing cases plus the joined
pin), and the probe's E1 to E7 and R1 each answer 200; on head each is
refused. `dispatch-gates --ran`: 115 derived / 115 run / 0 NOT MEASURED,
114 exit 0, 1 exit 1 (`check:platform-checklist`, not a PR gate).
Regenerated artefacts vs `origin/main`: `registry.ts` +62/-0 (the entry
block only), `spec-changes.json` and the upgrade guide identical.
Driver-free merge-tree probe against `2473e26875`: clean, and the
migration and liveness checks are green on the merge tree.
- **Round 2 (`257ab1bc92`):** `migrate-meta-engine-guidance.test.ts`
3/3; spec `--project repo` 43/761; spec `src/migrations` 3/161;
`check:migration-registry`, `check:spec-changes`, `check:upgrade-guide`,
`check-adr-0087-registration` green; `dispatch-gates --ran` 115 derived
/ 115 run / 0 NOT MEASURED, 114 exit 0, 1 exit 1
(`check:platform-checklist`, inputs equal to merge base `3f45b6cc13`).
The lines below are round 1's, at `d665865d5b`.
- **Builds (①):**
- `pnpm --filter '@objectstack/service-analytics...' build`: exit 0.
- `turbo run build --filter='@objectstack/runtime^...'`: 29/29.
- `pnpm --filter @objectstack/spec build`, after the describe edit: exit
0.
- **Tests (②):**
| suite | files | tests | result |
|:--|--:|--:|:--|
| service-analytics, whole package | 135 | 3178 | pass |
| new service-door file | — | 13 | included above |
| spec `--project local` | 575 | 16917 (+1 todo) | pass |
| spec `--project repo` | 42 | 745 | pass |
| runtime: the new REST pin plus the 2 sibling harness files that
consume service-analytics | 3 | 24 | pass |
| rest: the 7 files that import service-analytics | 7 | 86 | pass (at
`958251b6ac`; service-analytics src is unchanged since) |
- Typecheck is clean for spec, service-analytics and runtime. Runtime's
includes `check:test-typecheck`, and the new file adds no debt.
- `tsc --listFiles` puts the new service test in service-analytics'
program.
- Two consumers were not run, and both are unaffected by construction
because their cubes declare no `format` and no time dimension:
`packages/client` `analytics-automation-json-erasure.test.ts`, and the
dogfood analytics files, which declare neither key.
- **Ablations (predicted before each run; every leg restored to the HEAD
blob with `git diff HEAD` empty):**
| mutation | suite | predicted | observed | at |
|:--|:--|:--|:--|:--|
| format early-return (`9d77adcd798f` → `0d48cfde9469`) | service door |
4 red | 4 red, 8 green | `9bf3b3b0b4` |
| format early-return | REST, through `dist/` | 1 red | 1 red, 2 green |
`9bf3b3b0b4` |
| granularity early-return (`9d77adcd798f` → `94b1bc63165a`) | service
door | 6 red | 6 red, 7 green | `958251b6ac` |
| granularity early-return | REST, through `dist/` | 2 red | 2 red, 1
green | `9bf3b3b0b4` |
- Both REST legs were rebuilt, then checked with
`ablation-dist-preflight` (marker present in 2 built files). Each
restore leg was rebuilt again and checked `--absent`, with the tree
clean.
- **Gates:**
- `dispatch-gates --commands`: 111 derived. `--ran` with exit codes: 111
run, 0 NOT MEASURED. 109 exit 0.
- Two exit 1, both pre-existing. Their inputs are byte-identical to the
merge base `1322cc72c`:
- `check:platform-checklist`: `areas/identity-auth.json` cites
`auth-plugin.ts#twoFactor`, which is absent;
- `check:docs-transcript-drift`: 4 CLI transcripts print "author-time
rules (47)", and the count derives to 46.
- `check:liveness`: `analytics_cube 27 classified (live 20, dead 7)`,
with the state-counts current.
- `check:generated`: all 15 artifacts up to date.
- **Lint, narrowed:**
- `eslint --no-inline-config --format json` on the 4 changed `.ts`
files: 4 files, 0 errors, 0 warnings.
- The other 4 changed paths (`.md` / `.json`) are reported by eslint
itself as "File ignored because no matching configuration".
- Invariance: `eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`), so this diff cannot move a verdict on an
untouched file.
## Acceptance notes
- **File-surface amendments to claim `5886559774`,** each forced by the
claim's own items:
- the `analytics_cube` Notes cell in `packages/spec/liveness/README.md`.
It is the prose half of the state table whose shard the claim names, and
it named both keys as dead.
-
`packages/runtime/src/analytics-authored-cube-format-granularity.test.ts`,
the REST pin the claim asks for "where the analytics harness reaches".
It is a new file, and the runtime harness drives the real dispatcher
route.
- the generated `content/docs/references/data/analytics.mdx`, which the
describes regenerate.
- the D3 entry, the regenerated `migrations/registry.ts` and the
changeset marker, per seat note `5889752648`.
- `packages/services/service-analytics/src/preview-evaluator.ts` still
open-codes the single-entry rule (`dim.granularities?.length === 1`) on
the draft-preview path. That makes it a third spelling beside
`declaredDefaultGranularity`. It is outside this surface; `carrier:`
承接者:无.
- `examples/app-showcase/src/data/analytics/showcase.cube.ts` authors
`done_rate: { format: 'percent' }`. That named style now reaches
`fields[].format` verbatim, and a numeral-pattern renderer does not read
it as a percentage. The spec describe now teaches the pattern
vocabulary. The example's value is outside this surface and is reported
to the seat.
- Carried from stage 1 and unchanged here: the open-core `os serve`
artifact-fallback boot threads no `analyticsCubes`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1a75e39 commit c8dd8dd
12 files changed
Lines changed: 908 additions & 28 deletions
File tree
- .changeset
- content/docs/references/data
- packages
- runtime/src
- services/service-analytics/src
- __tests__
- spec
- liveness
- state-counts
- src
- data
- migrations
- entries/semantic
Lines changed: 26 additions & 0 deletions
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
148 | 148 | | |
149 | 149 | | |
150 | 150 | | |
151 | | - | |
| 151 | + | |
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
| |||
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
162 | | - | |
| 162 | + | |
163 | 163 | | |
164 | 164 | | |
165 | 165 | | |
| |||
199 | 199 | | |
200 | 200 | | |
201 | 201 | | |
202 | | - | |
| 202 | + | |
203 | 203 | | |
204 | 204 | | |
205 | 205 | | |
| |||
228 | 228 | | |
229 | 229 | | |
230 | 230 | | |
231 | | - | |
| 231 | + | |
232 | 232 | | |
233 | 233 | | |
234 | 234 | | |
| |||
Lines changed: 154 additions & 0 deletions
| 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 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
0 commit comments