Skip to content

Commit f287435

Browse files
os-zhuangclaude
andauthored
feat(spec): analytics authorable strictness + the data/ mixed(p) verdicts (#4001 data batch D) (#8695)
* wip: batch D verdicts + analytics tightening (pre reverse-verify) * wip: batch D pin test + migration entry (pre-registry-regen) * feat(spec): analytics authorable strictness + data/ mixed(p) verdicts (#4001 batch D) - data/analytics.zod.ts: all 8 sites strictObject (cube family + query family); the live behaviour change is the nested timeDimensions[] item that used to ride through the already-strict /analytics/query wrapper - seed-loader.zod.ts: re-verdicted wire (producer enumeration — all framework code; the authored half of seeding is SeedSchema, already strict) - field-value.zod.ts: re-verdicted open (ADR-0104 record-data value contracts) - ledger prose + regenerated counts (data/ unresolved 22 -> 0, strip 132 -> 124) - semantic migration entry under protocol-18 + changeset (minor + BREAKING + ADR-0087 disposition, the #8666 precedent) - batchd pin test: doors, per-carrier closures, alias claims, wrapper composition, deliberate non-closures Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5tUwGM3LQoqErTfkvRW7W --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3508678 commit f287435

7 files changed

Lines changed: 609 additions & 91 deletions
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): refuse undeclared keys on the analytics authoring surface (#4001 data batch D)
6+
7+
**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
8+
launch-window convention ships it as `minor`; the migration prescription is
9+
registered under protocol major 18, where `os migrate meta` users will look).
10+
11+
All 8 `data/analytics.zod.ts` sites are strict: the cube family (`CubeSchema` +
12+
its `refreshKey` block, `MetricSchema` + its `filters[]` items,
13+
`DimensionSchema`, `CubeJoinSchema`) and the query family
14+
(`AnalyticsQuerySchema` + its `timeDimensions[]` items). Before this change an
15+
undeclared key on any of them was silently dropped: a join authored with a
16+
typo'd `relationship` registered with the `many_to_one` default — a different
17+
join shape than the author declared — and a cube's misspelled key vanished
18+
under a successful parse.
19+
20+
The subtle half is the query: `/analytics/query`'s TOP level has been strict
21+
since #3878 (`AnalyticsQueryRequestSchema`), but top-level strictness does not
22+
recurse — measured on `main`, `timeDimensions: [{ dimension, granuarity:
23+
'day' }]` rode through the strict wrapper with the typo silently stripped, so
24+
the query bucketed the whole range as one group under an ordinary 200. The
25+
nested item is now strict, and the base schema's own strictness makes the
26+
posture hold at every door instead of only at the wrapper that re-applied it.
27+
28+
**What is refused:** any key the shape does not declare, with a prescriptive
29+
message — the surface, the offending key, and a rename (`title``label` on a
30+
metric/dimension, `label``title` on the cube, `table`/`sqlTable``sql`,
31+
`granularity``granularities` on a dimension and the reverse on a query time
32+
dimension, `orderBy``order`; `filters` on a query gets the `where`
33+
prescription matching the dispatcher's #3878 hint).
34+
35+
**What stays accepted:** every declared key byte-identically, including the
36+
`#3878` tombstones on the request wrapper (`query`/`format` still answer their
37+
migration text).
38+
39+
## FROM → TO
40+
41+
```ts
42+
// before — parsed green; the join fell back to many_to_one silently
43+
defineCube({
44+
name: 'orders', sql: 'orders',
45+
measures: { revenue: { name: 'revenue', label: 'Revenue', type: 'sum', sql: 'amount' } },
46+
dimensions: {},
47+
joins: { customers: { name: 'customers', sql: 'a.id = b.a_id', relationshipp: 'one_to_many' } },
48+
})
49+
50+
// after — rejected with `relationshipp` → `relationship`; write the declared key
51+
defineCube({
52+
name: 'orders', sql: 'orders',
53+
measures: { revenue: { name: 'revenue', label: 'Revenue', type: 'sum', sql: 'amount' } },
54+
dimensions: {},
55+
joins: { customers: { name: 'customers', sql: 'a.id = b.a_id', relationship: 'one_to_many' } },
56+
})
57+
```
58+
59+
There is deliberately no automatic rewrite: an undeclared key is either a
60+
spelling of a declared one (the rejection names the rename) or names a
61+
capability the analytics layer does not deliver, and blessing it would be
62+
declared-but-unenforced surface (ADR-0078). `os migrate meta` surfaces the
63+
change as a structured TODO (semantic entry
64+
`analytics-authorable-unknown-keys-refused`, protocol major 18 — this refusal
65+
is not part of the v17.0.0 cut).
66+
67+
<!-- adr-0087: registered analytics-authorable-unknown-keys-refused -->

docs/audits/2026-07-unknown-key-strictness-ledger.counts.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@ regenerate.
2222
|---|---|
2323
| Triaged directories | 5 |
2424
| Object sites in them | 433 |
25-
| Still-open (strip) sites | 132 |
26-
| Files carrying at least one | 24 |
25+
| Still-open (strip) sites | 124 |
26+
| Files carrying at least one | 23 |
2727

2828
Remaining strip sites by class:
2929

3030
| Bucket | Sites |
3131
|---|---|
3232
| authorable — the ruling's forced scope | 2 |
33-
| unresolved — needs a per-schema verdict | 22 |
34-
| wire / open — out of forced scope | 104 |
33+
| unresolved — needs a per-schema verdict | 0 |
34+
| wire / open — out of forced scope | 118 |
3535
| no door — no carrier, ADR-0049 territory | 3 |
3636
| no gate — carrier live, no parse | 0 |
3737
| covered — no carrier, no parse, guarded at every consumer | 1 |
@@ -45,11 +45,11 @@ The `strict` column is the one the campaign schedules against; it counts both th
4545
| Dir | Sites | strict | passthrough | catchall | strip |
4646
|---|---|---|---|---|---|
4747
| `ui/` | 168 | 157 | 5 | 0 | 6 |
48-
| `data/` | 153 | 62 | 1 | 0 | 90 |
48+
| `data/` | 153 | 70 | 1 | 0 | 82 |
4949
| `automation/` | 65 | 42 | 0 | 0 | 23 |
5050
| `security/` | 20 | 7 | 0 | 0 | 13 |
5151
| `studio/` | 27 | 27 | 0 | 0 | 0 |
52-
| **total** | **433** | **295** | **6** | **0** | **132** |
52+
| **total** | **433** | **303** | **6** | **0** | **124** |
5353

5454
## File-level triage — site counts
5555

@@ -177,11 +177,10 @@ over it is here.
177177

178178
### `data/` — open
179179

180-
**90 strip of 153**, in 14 file(s).
180+
**82 strip of 153**, in 13 file(s).
181181

182182
| File | Strip | Sites |
183183
|---|---|---|
184-
| `analytics.zod.ts` | 8 | 8 |
185184
| `data-engine.zod.ts` | 13 | 13 |
186185
| `document.zod.ts` | 8 | 8 |
187186
| `driver-nosql.zod.ts` | 10 | 10 |
@@ -195,13 +194,13 @@ over it is here.
195194
| `object.zod.ts` | 1 | 20 |
196195
| `query.zod.ts` | 4 | 5 |
197196
| `seed-loader.zod.ts` | 12 | 12 |
198-
| **total** | **90** | **153** |
197+
| **total** | **82** | **153** |
199198

200199
| Bucket | Sites |
201200
|---|---|
202201
| authorable — the ruling's forced scope | 1 |
203-
| unresolved — needs a per-schema verdict | 22 |
204-
| wire / open — out of forced scope | 65 |
202+
| unresolved — needs a per-schema verdict | 0 |
203+
| wire / open — out of forced scope | 79 |
205204
| no door — no carrier, ADR-0049 territory | 2 |
206205
| no gate — carrier live, no parse | 0 |
207206
| covered — no carrier, no parse, guarded at every consumer | 0 |

0 commit comments

Comments
 (0)