Skip to content

Commit 25fefde

Browse files
committed
feat(spec): govern connector + analytics_cube in the liveness ledger
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 840c655 commit 25fefde

5 files changed

Lines changed: 55 additions & 18 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`liveness/connector.json` and `liveness/analytics_cube.json` — the last two governance debts the liveness ratchet declared are paid, so `PENDING_GOVERNANCE` is empty and every authorable metadata type now has a ledger (#18582).
6+
7+
The ledgers ship inside this package, so these are the files an upgrading reader greps to learn whether a key they are about to author does anything. Both types are authored through real doors — `defineStack({ connectors })` / `defineStack({ analyticsCubes })` and `PUT /api/v1/meta/{connector,analytics_cube}/:name` — and neither had ever been walked: they were in neither `GOVERNED` nor `PENDING_GOVERNANCE` until #18133 widened the denominator, so their silence read as "nothing to report".
8+
9+
- **`connector` — 74 properties: 20 `live`, 1 `planned`, 53 `dead`.** One schema, two doors: the ledger's entry exists for the AUTHORING doors, while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a plugin or an ADR-0097 provider factory builds in code. The keys an authored entry can actually reach are the `ConnectorProviderContext` fields plus `name` and `enabled`; `type` and `icon` reach that context and are dropped by all three shipped provider factories. The 53 dead are four declared subsystems with no engine — `syncConfig`, `fieldMappings`, `retryConfig`, `health` — plus `triggers` (the schema's own docblock already said so, #3197), the connector's nested `webhooks`, `status`, both timeouts, and four `retiredKey` tombstones. `authentication` is `planned`: refused outright by ADR-0097 §3, never ignored.
10+
- **`analytics_cube` — 29 properties: 17 `live`, 12 `dead`.** The query path is genuinely consumed (`sql` is both the FROM table and the object whose RLS read scope is injected; `measures.type` picks the aggregate; `joins[].name` the joined table). What is not: the caching block (`refreshKey`), the `public` access flag that gates nothing, `joins[].relationship` and the REQUIRED `joins[].sql` — the ON clause is synthesised as a foreign-key equality and an authored one is never consulted — and the inner `name` on each of `measures`/`dimensions`, where the record key is the identity. #10238 (is cube authoring live end to end?) is a separate measurement and is not prejudged here.
11+
- **Two prior in-repo claims were falsified and are corrected in the ledgers.** A comment in `src/conversions/registry.ts` says `retryConfig` "and the timeouts beside it are untouched — they are live"; the word does not occur outside `packages/spec` at all. And `bootstrapDeclaredWebhooks` documents itself as materializing each "stack/connector-authored webhook", while its source is `readDeclared(…, 'webhook')` — metadata items the decomposition registers from the top-level `webhooks:` collection, which a connector's nested array never becomes.
12+
13+
No schema changed and no verdict moved on an existing ledger: `check:liveness` walks two more types and reports the same 583 repo-local evidence paths resolving, with 39 governed types indexed by the README table.
14+
15+
Clause-②: no

‎docs/qa/platform-checklist/coverage.json‎

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
"ai.console-ai-surface-gating"
2020
]
2121
},
22+
"analytics_cube": {
23+
"items": [
24+
"dashboards.cube-query"
25+
]
26+
},
2227
"api": {
2328
"items": [
2429
"api-backend.declarative-endpoint-execution",
@@ -50,6 +55,22 @@
5055
"platform-core.docs-portal-render"
5156
]
5257
},
58+
"capability": {
59+
"items": [
60+
"access-security.capability-declaration-lifecycle",
61+
"ai.console-ai-surface-gating",
62+
"platform-core.marketplace-install-local-lifecycle"
63+
]
64+
},
65+
"connector": {
66+
"items": [
67+
"integration-system.connector-declarative-boot",
68+
"integration-system.connector-auth-kind-application",
69+
"integration-system.connector-descriptor-audit",
70+
"integration-system.connector-degraded-recovery",
71+
"automation.connector-dispatch-matrix"
72+
]
73+
},
5374
"crud_endpoints": {
5475
"items": [
5576
"api-backend.rest-crud-config-contract",
@@ -304,13 +325,6 @@
304325
"items": [
305326
"integration-system.webhook-lifecycle"
306327
]
307-
},
308-
"capability": {
309-
"items": [
310-
"access-security.capability-declaration-lifecycle",
311-
"ai.console-ai-surface-gating",
312-
"platform-core.marketplace-install-local-lifecycle"
313-
]
314328
}
315329
}
316330
}

‎packages/spec/liveness/README.md‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,7 @@ The governed set is `GOVERNED` at the top of `check-liveness.mts`. To add a type
816816
RecordDetailView had been gating the History tab on it the whole time (#2707).
817817
4. Add the type to `GOVERNED`; confirm the gate is green.
818818

819-
## Current state — 37 governed types (complete registry coverage)
819+
## Current state — 39 governed types (complete registry coverage)
820820

821821
> **This heading is now checked** (#7257). `check:liveness` reconciles the table
822822
> against `GOVERNED` in both directions — a governed type with no row fails, a row
@@ -938,6 +938,8 @@ marker where the Notes cell goes, never a guess at what belongs there.
938938
| route_generation | seeded 2026-09-02 (#14369) — one of the FOUR `RestServerConfig` sub-objects, and the family that made the `SPEC_ONLY_SCHEMAS` boundary explicit: SERVER CONFIGURATION. An author writes `RestServerConfigSchema` (`packages/spec/src/api/rest-server.zod.ts`) as the REST server's construction argument — not a metadata item, not a request body, not a manifest — so no registry has ever held it and no ratchet rooted in one could ask who reads it. Rooted on the four sub-schemas rather than on the whole config on purpose: the walk drilled exactly ONE level when this was rooted (it recurses as of #17424; the rooting stands), so with `RestServerConfigSchema` as the root the sub-objects would BE the drilled level and `metadata.endpoints.schema` / `batch.operations.upsertMany` would have no row of their own, their container's blanket `live` silently covering a dead key — #4956's shape, in the ledger written to end it. `RestApiConfigSchema` (the fifth sub-object, `api`) is deliberately not enrolled: its consumption seam is still validate-only and is the subject of its own card, so a census of it would record a half that is about to move. **What #11984 settled and what it did not**: that PR made `RestServer.normalizeConfig` PARSE and CONSUME these four instead of casting them, so an out-of-enum or out-of-range value is now refused at construction — accept/reject. Executing a declared contract does not give a key a consumer, and this family is that distinction's worked example. Dead 6 = every key it has, and that is the finding: `routes` is parsed, defaulted and normalized into `this.config.routes`, and nothing ever reads it back. `excludeObjects: ['sys_log']` excludes nothing, `nameTransform: 'plural'` still mounts every route under the raw object name, and the per-object `overrides` record (drilled to `enabled` / `basePath` / `operations`) turns nothing on or off. ⚠️ The `overrides` hits in `packages/rest/src` are a REQUEST BODY and a test builder — different keys with the same name. This is the one member of the family with a customer-visible limb: `RestServerConfigSchema`'s own `@example` advertises `routes: { excludeObjects: ['system_log'] }`, so the published prose promises a capability the runtime does not deliver (Prime Directive #10). Fixing that example belongs to whichever enforce-or-remove limb the key lands on — `routes.*` reads as designed-but-never-wired, so enforcing it is real work in route generation that changes the mounted surface, and no dev agent decides that **#14691 RETIRED all six (2026-09-03, ADR-0049)**: every key is now a `retiredKey()` tombstone and the sub-object is tombstones-only; the rows stay `dead` with a REMOVED note (non-strict schema) and the three `overrides.*` child rows collapse into the one `overrides` row. Triage held `overrides` open as an ENFORCE candidate; the measurement closed it as REMOVE because the capability already exists at its proper seat — per-object exposure is the object's own `enable.apiEnabled` / `enable.apiMethods`, enforced by rest-server.ts#enforceApiAccess (404 / 405) — and `basePath` / `nameTransform` would contradict the one deployment-wide data base and Prime Directive #6 (the object name IS the REST path segment). The `@example` limb is corrected in the same change. `evidenceScope` widened to `cross-repo` (#14796) |
939939
| realtime_subscription | seeded 2026-09-04 (#14446) — a TRANSPORT-PROTOCOL surface, the fifth category the `SPEC_ONLY_SCHEMAS` override has had to reach. `SubscriptionSchema` (`packages/spec/src/api/realtime.zod.ts`) is what a client declares to open a realtime subscription: the item type of `RealtimeConfigSchema.subscriptions` and the `Subscription` the generated API reference publishes. Like `query` it is a request surface rather than stored metadata, and like `query` that is exactly why it went unasked — no registry holds it, `RealtimeConfigSchema` is `.passthrough()` so nothing downstream even refuses an unknown key, and the whole vocabulary sat outside the denominator while the reference kept publishing it. Rooted on `SubscriptionSchema` rather than on `RealtimeConfigSchema` for the reason the four `RestServerConfig` sub-objects document one row up: the walk drilled exactly ONE level when this was rooted (it recurses as of #17424; the rooting stands), so with the config as the root `events[].type` and `events[].filters` would inherit a container verdict instead of carrying rows of their own — #4956's shape. **Dead 6 = every key it has, and the CONTAINER is the finding**: nothing outside `packages/spec` imports `SubscriptionSchema`, `SubscriptionEventSchema` or `RealtimeConfigSchema` at all, so no key beneath them can be read (the `manifest.contributes` reasoning). The two keys the card measured are the sharp ones. `events[].type` accepts `RealtimeEventType`, whose four members (`record.created` / `record.updated` / `record.deleted` / `field.changed`) are DISJOINT from what the engine publishes (`DataEventType`'s `data.record.*`, live emitter in `service-knowledge`), so an author who writes the enum's own `record.created` gets a subscription that silently never fires — and the enum is what the API reference shows them. Its direction is settled by the 2026-09-02 triage and quoted verbatim in the row: enforce means REPOINTING THE ENUM, never changing what the runtime publishes. `field.changed` is the same spelling the sibling `DataEventType` REMOVED in 17.0.0 (#4673, PR #4685) for having no producer; it survives here only because this enum was never in a ratchet's denominator. `events[].filters` is `z.unknown().optional()` — the textbook ADR-0049 fourth state, no shape and no reader, failing in the permissive direction (a subscriber who filters receives every event). ⚠️ Three spellings of a realtime subscription exist and only the third is executed: this one, `websocket.zod.ts#EventSubscriptionSchema`, and the plain interface `contracts/realtime-service.ts#RealtimeSubscriptionOptions` that `in-memory-realtime-adapter.ts#matchesSubscription` actually reads. The file note names the same-name-different-shape traps so the next census does not mistake one for a consumer. Zero live |
940940
| sharing_rule | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, and the first one PAID (`connector` and `analytics_cube` are still owed on that card). Not a registered kind: it is bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reaches the walk through `getMetadataTypeSchema`'s unregistered-kind fallback, so this ledger governs a type `listMetadataTypeSchemaTypes()` still does not enumerate. One shape fact decides every row: the AUTHORING shape is not the ENFORCED shape. ADR-0057 D6 makes the `sys_sharing_rule` row canonical (`object_name` + `criteria_json` + `recipient_type`/`recipient_id` + `access_level`) and `bootstrapDeclaredSharingRules` translates each authored key into it at boot — nothing re-parses `SharingRuleSchema` at enforcement time — so every consumer cited reads a COLUMN and every row carries the `producer` (#4837) that populates it, which is the `seed.env` lesson applied to a whole type rather than to one key. Preview read points ENUMERATED per the #7131 rule and the answer recorded rather than skipped: `registerBuiltinPreviews()` (objectui @dda8f381) registers twenty types and `sharing_rule` is not one of them; what objectui does consume is the whole shape, on the CREATE door only (`AUTHOR_SHAPE_ONLY_TYPES` — the EDIT door is deliberately ungated because a served body carries the `_diagnostics` decoration this `.strict()` schema rejects). The single non-`live` row is `type`, the `SharingRuleType` discriminator: one member, `criteria`, whose only reader is a defensive `=== 'owner'` comparison that is unreachable for every value the schema admits. `planned` on the `action.operation` precedent (a one-member discriminator held `planned` until a runtime half dispatched on it, #15080), and deliberately NOT an enforce-or-remove candidate: the key is required, so removing it would break every authored rule to delete nothing. |
941+
| connector | seeded 2026-09-17 (#18582) — the second of the three `PENDING_GOVERNANCE` debts #18133 declared, paid in the same diff as `analytics_cube`, which empties that map. Not a registered kind: bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. **What the walk actually resolves, measured:** the binding names `DeclarativeConnectorEntrySchema`, and that schema is `ConnectorSchema.superRefine(...)` — in Zod 4 a `superRefine` attaches a check to the same object def rather than wrapping it, so the walked key set is byte-identical to the base's. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the three rows where they are the whole verdict. **ONE SCHEMA, TWO DOORS** is the shape fact behind the 20/1/53 split: the ledger's denominator entry exists for the AUTHORING doors (`defineStack({ connectors })`, `PUT /meta/connector/:name`), while the same `ConnectorSchema` is what `AutomationEngine.registerConnector` parses for a def a PLUGIN or an ADR-0097 provider factory builds in code — so a key can have a real consumer and still do nothing when a metadata author writes it. The keys an authored entry can reach are exactly the `ConnectorProviderContext` fields plus `name` and `enabled`; `type` and `icon` reach that context and are dropped by all three shipped factories, and each says so on its own row. `authentication` is the ledger's `planned`: refused outright by ADR-0097 §3 (#7990) rather than ignored. The 53 `dead` are four declared subsystems with no engine — `syncConfig` (7), `fieldMappings` (7), `retryConfig` (8), `health` (14, both sub-blocks) — plus `triggers` (6, and the schema's own docblock says so: #3197), the connector's nested `webhooks` (one blanket verdict, recorded in the undrilled baseline), `status`, `metadata`, the two timeouts, `actions.description`/`.outputSchema`, and four `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent). **A prior in-repo claim was falsified here**: the conversion registry's note that `retryConfig` "and the timeouts beside it are untouched — they are live" is corrected on those rows — the word does not occur outside `packages/spec` at all |
942+
| analytics_cube | seeded 2026-09-17 (#18582) — the third debt, paid in the same diff as `connector`. Not a registered kind either: bound in `UNREGISTERED_KIND_SCHEMAS` by #10194 and reached through the same unregistered-kind fallback. **ONE Cube shape, THREE producers, one registry** is what decides every row: `cube-registry.ts` names them itself — authored cubes (`analyticsCubes[]` / `defineCube()`, threaded by the CLI into `AnalyticsServiceConfig.cubes`), COMPILED DATASETS (ADR-0021, where `dataset-compiler` mints a Cube), and ad-hoc query inference. Only the first is the authoring door governed here, so a key whose only reader sits on the compiled-dataset path is not live for an authored cube however busy that reader is — the #4837 producer rule on a shape with three producers. That is `dimensions.granularities` (read by `dataset-executor#granularityOf`, whose argument is a `CompiledDataset` an authored cube never becomes) and `measures.format` (written by the compiler, threaded to the wire from the DATASET measure instead). The query path is genuinely live: `sql` is the FROM table AND the object whose RLS read scope is injected, `measures.type` picks the aggregate, `measures.sql`/`dimensions.sql` the column, `joins[].name` the joined table. The 12 `dead` are the caching block (`refreshKey.every`/`.sql` — no refresh scheduler exists anywhere), the access-control flag (`public` — three sites write `false`, nothing reads it: a knob that was never wired, not a hole that was opened), `joins[].relationship` and the REQUIRED `joins[].sql` (the ON clause is SYNTHESISED as an FK equality and the authored one is never consulted), the three `description`s, and the inner `name` on each of `measures`/`dimensions`, where the record KEY is the identity. **#10238 is not prejudged**: whether cube authoring is live end to end is still its own measurement — this ledger answers the per-key question only |
941943

942944
The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every
943945
misleading entry carries `authorWarn` so authors hear about it at compile time
@@ -953,14 +955,17 @@ precisely how the gap survived for a year: prose cannot fail a build. Now the ga
953955
compares `GOVERNED` against the denominator in both directions (an ungoverned
954956
authorable type fails; so does a stale pending row whose debt is already paid).
955957

956-
⚠️ **The map is no longer empty, and that is #18133's finding rather than a
957-
regression.** Widening the denominator from the registered kinds to the authorable
958+
⚠️ **The map went from empty to three rows and back to empty, and neither move was
959+
a regression.** Widening the denominator from the registered kinds to the authorable
958960
set (see the ⛔ note under *Source of truth* above) made three types visible that
959961
had been in **neither** `GOVERNED` **nor** `PENDING_GOVERNANCE` — `connector`,
960962
`sharing_rule` and `analytics_cube` — and therefore produced no row in any of the
961-
gate's lists while the report read complete. They are now declared debts with a
962-
reason and an issue number apiece, which is the state this ratchet exists to
963-
produce; the direction of travel is out of that map and into `GOVERNED`, exactly
964-
as it was for the nine. ⛔ Their presence is not a licence to leave them there.
965-
**One of the three is paid**: `sharing_rule` is governed as of #18582, and the map
966-
now holds `connector` and `analytics_cube`.
963+
gate's lists while the report read complete (#18133). Declaring them as debts with a
964+
reason and an issue number apiece is what a ratchet is for; paying them is the
965+
direction of travel, exactly as it was for the nine. **All three are now paid**:
966+
`sharing_rule` (PR #18587), then `connector` and `analytics_cube` together, each with
967+
a ledger beside this file — so `PENDING_GOVERNANCE` is empty again, this time with
968+
nothing hidden behind the emptiness. ⛔ The map stays in the gate: it is how the next
969+
authorable type that arrives ungoverned gets a declared debt instead of silence, and
970+
an entry added to it is a debt with an issue number, never a way to silence the gate
971+
on a type somebody has just registered.

‎packages/spec/liveness/state-counts.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,6 @@ for both corollaries.
6464
| `route_generation` | 0 | 0 | 0 | 4 | 0 | 4 |
6565
| `realtime_subscription` | 0 | 0 | 0 | 6 | 0 | 6 |
6666
| `sharing_rule` | 16 | 0 | 0 | 0 | 1 | 17 |
67-
| **total** | **878** | **5** | **1** | **96** | **11** | **991** |
67+
| `connector` | 20 | 0 | 0 | 53 | 1 | 74 |
68+
| `analytics_cube` | 17 | 0 | 0 | 12 | 0 | 29 |
69+
| **total** | **915** | **5** | **1** | **161** | **12** | **1094** |

0 commit comments

Comments
 (0)