Skip to content

Commit 556c125

Browse files
committed
feat(spec): govern dataset + bind the analytics high-risk class (ADR-0054)
Unlocks the analytics binding that was previously blocked because its authorable surface wasn't governed. - Govern the `dataset` metadata type: new liveness/dataset.json classifies all 19 authorable props with file:line evidence from the analytics service consumer audit (26 live, 1 dead — measures.certified has no runtime consumer). `dataset` added to GOVERNED in check-liveness.mts. - Bind the analytics class: dataset.dimensions.dateGranularity carries the tz-bucketing proof (#1982/#2018) — the property whose org-timezone day-bucket behavior the proof asserts. proof-registry.mts flips analytics to bound. - Four high-risk classes now CI-enforced: field types, RLS, flow nodes, analytics. - README governed-types + high-risk tables updated; changeset description updated. Verified: liveness gate green (dataset 27 classified); 17 registry tests; the analytics proof runs green; full dogfood suite 59/59. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XVdnfUAx85amkerym26vdx
1 parent 1c1b1d0 commit 556c125

6 files changed

Lines changed: 131 additions & 16 deletions

File tree

.changeset/liveness-prove-it-runs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@objectstack/spec": patch
33
---
44

5-
Add the ADR-0054 "prove-it-runs" proof field + ratchet to the spec liveness gate. A `live` ledger entry may now carry a `proof` — a reference (`<file>#<proof-id>`) to a dogfood test that asserts the property's runtime behavior. For the high-risk classes bound this phase (field types → `field.type`; RLS → `permission.rowLevelSecurity.using`), a `live` classification must carry a valid proof, validated statically by the liveness gate (the file exists and declares the matching `@proof:` tag). The authoritative high-risk-class list lives in `scripts/liveness/proof-registry.mts`; see `liveness/README.md`.
5+
Add the ADR-0054 "prove-it-runs" proof field + ratchet to the spec liveness gate. A `live` ledger entry may now carry a `proof` — a reference (`<file>#<proof-id>`) to a dogfood test that asserts the property's runtime behavior. A bound high-risk `live` property must carry a valid proof, validated statically by the liveness gate (the file exists and declares the matching `@proof:` tag). Four high-risk classes are bound this phase: field types (`field.type`), RLS (`permission.rowLevelSecurity.using`), flow nodes (`flow.nodes.type`), and analytics (`dataset.dimensions.dateGranularity`). The `dataset` metadata type is now governed (new `liveness/dataset.json`). The authoritative high-risk-class list lives in `scripts/liveness/proof-registry.mts`; see `liveness/README.md`.

packages/spec/liveness/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ binding lands one class at a time (ADR-0054 §3), never as a big-bang backfill.
7878
| Field types | ✅ enforced | `field.type` | `field-zoo-roundtrip.dogfood.test.ts#field-type-roundtrip` |
7979
| RLS / sharing | ✅ enforced | `permission.rowLevelSecurity.using` | `rls-fixture.dogfood.test.ts#rls-by-id-write` |
8080
| Flow nodes | ✅ enforced | `flow.nodes.type` | `flow-node.dogfood.test.ts#flow-node-execution` |
81-
| Analytics dims/measures | ⛔ pending | | `analytics-timezone.dogfood.test.ts#analytics-tz-bucketing` (proof exists; surface `dataset`/`report` not yet governed) |
82-
| Form layout/section/widget | ⛔ pending || none yet (Phase 2) |
81+
| Analytics dims/measures | ✅ enforced | `dataset.dimensions.dateGranularity` | `analytics-timezone.dogfood.test.ts#analytics-tz-bucketing` |
82+
| Form layout/section/widget | ⛔ pending || none yet (form surface not yet governed) |
8383

8484
To bind a pending class: add its dogfood proof + `@proof:` tag, set `bound: true` and
8585
its `ledgerBindings` in `proof-registry.mts`, add the `proof` to the ledger entry, and
@@ -172,7 +172,8 @@ The governed set is `GOVERNED` at the top of `check-liveness.mts`. To add a type
172172
| agent | 18 | 4 | 5 | access/permissions/visibility dead (chat route hardcodes perms); autonomy experimental |
173173
| tool | 13 | 1 | 5 | write-only metadata; runtime uses a parallel AIToolDefinition |
174174
| skill | 15 || 2 | triggerPhrases dead (no matcher); permissions dead |
175+
| dataset | 26 || 1 | analytics semantic layer (compiled to a Cube); `measures.certified` dead; `dimensions.dateGranularity` carries the org-tz bucketing proof |
175176

176177
The `dead` set across types is the enforce-or-remove worklist (ADR-0049). Not yet governed
177-
(rollout): view, page, dashboard, app, report, dataset, job, datasource, translation,
178+
(rollout): view, page, dashboard, app, report, job, datasource, translation,
178179
email_template, doc, book, validation, seed.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
{
2+
"type": "dataset",
3+
"_note": "DatasetSchema (analytics semantic layer). Consumer: @objectstack/service-analytics — compileDataset() turns a dataset into a Cube, DatasetExecutor runs the query. Seeded from a dataset-property consumer audit (file:line evidence in service-analytics). ADR-0054: dimensions.dateGranularity carries the org-timezone date-bucketing proof (#1982/#2018). Framework provenance/lock fields auto-live.",
4+
"props": {
5+
"name": {
6+
"status": "live",
7+
"evidence": "packages/services/service-analytics/src/dataset-compiler.ts:178",
8+
"note": "registry key + compiled Cube name."
9+
},
10+
"label": {
11+
"status": "live",
12+
"evidence": "packages/services/service-analytics/src/dataset-compiler.ts:179",
13+
"note": "compiled into Cube title for presentations."
14+
},
15+
"description": {
16+
"status": "live",
17+
"note": "display/doc metadata (presentations); not read by analytics execution."
18+
},
19+
"object": {
20+
"status": "live",
21+
"evidence": "packages/services/service-analytics/src/dataset-compiler.ts:180",
22+
"note": "base table — Cube sql + join resolution + draft-rows resolver."
23+
},
24+
"include": {
25+
"status": "live",
26+
"evidence": "packages/services/service-analytics/src/dataset-compiler.ts:92",
27+
"note": "compiled into Cube joins + the NativeSQLStrategy join allowlist (ADR-0021)."
28+
},
29+
"filter": {
30+
"status": "live",
31+
"evidence": "packages/services/service-analytics/src/dataset-executor.ts:205",
32+
"note": "dataset-level WHERE, ANDed with runtime + measure-scoped filters."
33+
},
34+
"dimensions": {
35+
"children": {
36+
"name": {
37+
"status": "live",
38+
"evidence": "packages/services/service-analytics/src/dataset-compiler.ts:141",
39+
"note": "dimension identifier (Cube dimension key)."
40+
},
41+
"label": {
42+
"status": "live",
43+
"evidence": "packages/services/service-analytics/src/dataset-compiler.ts:142",
44+
"note": "compiled into Cube dimension for presentations."
45+
},
46+
"field": {
47+
"status": "live",
48+
"evidence": "packages/services/service-analytics/src/dataset-compiler.ts:144",
49+
"note": "emitted as the dimension's SQL column reference (relationship-validated)."
50+
},
51+
"type": {
52+
"status": "live",
53+
"evidence": "packages/services/service-analytics/src/dataset-compiler.ts:143",
54+
"note": "dimensionType() maps to Cube time/number/boolean/string; drives grouping."
55+
},
56+
"dateGranularity": {
57+
"status": "live",
58+
"evidence": "packages/services/service-analytics/src/dataset-compiler.ts:146",
59+
"proof": "packages/dogfood/test/analytics-timezone.dogfood.test.ts#analytics-tz-bucketing",
60+
"note": "ADR-0054 high-risk class (analytics): a time dimension's single granularity auto-buckets at query time (dataset-executor.ts:272-287); the proof asserts the day bucket SHIFTS with the org timezone (2024-03-01T03:00Z buckets to 2024-02-29 in America/Los_Angeles) — guarding the analytics-strategy ↔ in-memory count ↔ REST exec-context integration that #2018 fixed."
61+
}
62+
}
63+
},
64+
"measures": {
65+
"children": {
66+
"name": {
67+
"status": "live",
68+
"evidence": "packages/services/service-analytics/src/dataset-compiler.ts:166",
69+
"note": "measure identifier + derived-measure reference."
70+
},
71+
"label": {
72+
"status": "live",
73+
"evidence": "packages/services/service-analytics/src/analytics-service.ts:479",
74+
"note": "compiled into Cube metric + enriched onto result fields."
75+
},
76+
"aggregate": {
77+
"status": "live",
78+
"evidence": "packages/services/service-analytics/src/dataset-compiler.ts:60",
79+
"note": "aggregateToMetricType() → Cube metric type (sum/count/avg/…); unsupported aggregates throw."
80+
},
81+
"field": {
82+
"status": "live",
83+
"evidence": "packages/services/service-analytics/src/dataset-compiler.ts:170",
84+
"note": "SQL aggregate operand (count omits field → '*'); relationship-validated."
85+
},
86+
"filter": {
87+
"status": "live",
88+
"evidence": "packages/services/service-analytics/src/dataset-executor.ts:225",
89+
"note": "measure-scoped WHERE, applied via a supplementary per-measure query."
90+
},
91+
"format": {
92+
"status": "live",
93+
"evidence": "packages/services/service-analytics/src/analytics-service.ts:480",
94+
"note": "compiled into Cube metric + enriched onto result fields for the renderer."
95+
},
96+
"certified": {
97+
"status": "dead",
98+
"evidence": "no runtime consumer — analytics execution never reads it; not compiled into the Cube",
99+
"note": "measure trust/governance flag, declared but unenforced (ADR-0049 enforce-or-remove candidate). Not authorWarn'd: it may surface as an objectui badge, so it is not necessarily misleading."
100+
},
101+
"derived": {
102+
"status": "live",
103+
"evidence": "packages/services/service-analytics/src/dataset-executor.ts:247",
104+
"note": "post-aggregation arithmetic (ratio/sum/difference/product) over base measures; {op, of} both consumed."
105+
}
106+
}
107+
}
108+
}
109+
}

packages/spec/scripts/liveness/check-liveness.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const repoRoot = resolve(specRoot, '../..');
5353
const ledgerRoot = join(specRoot, 'liveness');
5454

5555
// Governed metadata types, rolled out highest-frequency / highest-risk first.
56-
const GOVERNED = ['object', 'field', 'flow', 'action', 'hook', 'permission', 'role', 'agent', 'tool', 'skill'];
56+
const GOVERNED = ['object', 'field', 'flow', 'action', 'hook', 'permission', 'role', 'agent', 'tool', 'skill', 'dataset'];
5757

5858
// ADR-0010 provenance/lock overlay fields — system-stamped, on every type; auto-live.
5959
const FRAMEWORK_FIELDS = new Set([

packages/spec/scripts/liveness/proof-registry.mts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,11 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [
8181
label: 'Analytics dimensions / measures',
8282
summary: 'date-dimension bucketing / aggregation under the org timezone (#1982/#2018).',
8383
proofId: 'analytics-tz-bucketing',
84-
// The proof EXISTS (golden tz-bucketing regression) and is tagged, so it is a
85-
// known proof — but it cannot be BOUND yet (see blockedReason).
8684
proofRef: 'packages/dogfood/test/analytics-timezone.dogfood.test.ts#analytics-tz-bucketing',
87-
bound: false,
88-
ledgerBindings: [],
89-
blockedReason:
90-
'the authorable surface (dataset/report dimensions+measures) is not yet a GOVERNED ledger type, ' +
91-
'so there is no entry to carry the proof. Bind once dataset/report are governed.',
85+
bound: true,
86+
// The org-timezone shift acts on a time dimension's bucketing granularity —
87+
// the property whose end-to-end correctness the tz proof guards.
88+
ledgerBindings: [{ type: 'dataset', path: 'dimensions.dateGranularity' }],
9289
},
9390
{
9491
id: 'flow-node',

packages/spec/scripts/liveness/proof-registry.test.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,12 @@ describe('registry invariants', () => {
102102

103103
it('BOUND_PROOF_PATHS maps the expected entries this phase', () => {
104104
expect([...BOUND_PROOF_PATHS.keys()].sort()).toEqual(
105-
['field/type', 'flow/nodes.type', 'permission/rowLevelSecurity.using'].sort(),
105+
[
106+
'field/type',
107+
'flow/nodes.type',
108+
'permission/rowLevelSecurity.using',
109+
'dataset/dimensions.dateGranularity',
110+
].sort(),
106111
);
107112
});
108113

@@ -120,6 +125,7 @@ describe('real proof wiring resolves', () => {
120125
field: 'packages/spec/liveness/field.json',
121126
permission: 'packages/spec/liveness/permission.json',
122127
flow: 'packages/spec/liveness/flow.json',
128+
dataset: 'packages/spec/liveness/dataset.json',
123129
};
124130

125131
function ledgerEntry(type: string, path: string): any {
@@ -144,8 +150,10 @@ describe('real proof wiring resolves', () => {
144150
}
145151
}
146152

147-
it('the analytics proof file exists and declares its tag (registered, pending bind)', () => {
148-
const analytics = HIGH_RISK_CLASSES.find((c) => c.id === 'analytics')!;
149-
expect(validateProofRef(analytics.proofRef, { repoRoot, fs, join })).toEqual({ ok: true });
153+
it('no bound class is left with an unresolved proof or a blockedReason', () => {
154+
for (const cls of HIGH_RISK_CLASSES.filter((c) => c.bound)) {
155+
expect(cls.blockedReason, `${cls.id} is bound but still records a blockedReason`).toBeUndefined();
156+
expect(validateProofRef(cls.proofRef, { repoRoot, fs, join })).toEqual({ ok: true });
157+
}
150158
});
151159
});

0 commit comments

Comments
 (0)