Repository navigation
Commit 559041d
feat(spec): govern connector + analytics_cube in the liveness ledger — PENDING_GOVERNANCE reaches empty (#18609)
Fixes #18582
Clause-②: no
`PENDING_GOVERNANCE` reaches **empty**. `connector` and `analytics_cube`
— the two debts left on this card after `sharing_rule` was paid by PR
#18587 — move into `GOVERNED` with a ledger each, so every authorable
metadata type in the denominator #18133 widened now has one.
## What was measured
**`connector` — 74 properties: 20 `live`, 1 `planned`, 53 `dead`.**
- **Which schema the walker really resolves** (the seat's open question
1): `getMetadataTypeSchema('connector')` returns
`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 `shapeOf()`
returns `ConnectorSchema`'s shape unchanged: the walked key set is
byte-identical to the base's, tombstones included. **The gate cannot
tell the two schemas apart.** What the entry schema buys is refusals,
which are invisible to the walk and show up only on the three rows where
they are the whole verdict. That difference is recorded in the ledger's
`_note` and in the README row.
- **One schema, two doors** is the shape fact behind the 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 — every row says which door its consumer is
fed from, and every `live` row carries a `producer` (#4837).
- 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
provider factories (`ctx.icon` census: zero reads across
`packages/connectors`, with `ctx.label` — four hits — as the lit
control). `authentication` is the ledger's one `planned`: refused
outright by ADR-0097 §3 (#7990), never ignored.
- The 53 `dead` are four declared subsystems with no engine
(`syncConfig` 7, `fieldMappings` 7, `retryConfig` 8, `health` 14),
`triggers` (6 — the schema's own docblock already said so, #3197), the
connector's nested `webhooks`, `status`, `metadata`, both timeouts,
`actions.description` / `.outputSchema`, and four `retiredKey`
tombstones whose rows stay because the key stays in the walked shape
(the `rls.priority` precedent).
**`analytics_cube` — 29 properties: 17 `live`, 12 `dead`.**
- **An honest `dead` was the outcome on 12 rows** (the seat's open
question 2), and none was inflated to green the gate. `cube-registry.ts`
names three producers into one registry — authored cubes, compiled
datasets (ADR-0021) and ad-hoc query inference — and only the first is
the door this ledger governs, so a key whose only reader sits on the
compiled-dataset path is not live for an authored cube. That is
`dimensions.granularities` (read by `dataset-executor#granularityOf`,
whose argument is a `CompiledDataset` an authored cube never becomes)
and `measures.format`.
- **Whether ADR-0049 wants a retirement is answered per row, and mostly
the answer is no** — the ledger says so explicitly so the
enforce-or-remove channel does not act on the word `dead`.
`granularities` and `measures.format` are the dataset compiler's own
output channel on a shared shape: deleting them breaks a live internal
write. `joins[].sql` is REQUIRED and documented as the ON clause while
the strategies synthesise an FK equality and never consult it — a
decision, not a sweep. `public` is an access-control flag that gates
nothing (three sites write `false`, nothing reads it): a knob that was
never wired, not a hole that was opened. `refreshKey.every` / `.sql` are
the only rows where retirement is the obvious shape, and even there the
showcase example authors them.
- **#10238 is not prejudged.** Whether cube authoring is live end to end
remains its own measurement; this ledger answers the per-key question
only, and says so in the `_note`.
**Two prior in-repo claims were falsified by this measurement and are
corrected in the ledgers** (not in their source files — that is out of
scope here, and both are filed below):
1. `packages/spec/src/conversions/registry.ts` states `retryConfig` "and
the timeouts beside it are untouched — they are live". The word
`retryConfig` does not occur anywhere in `packages/` or `examples/`
outside `packages/spec`, and every `connectionTimeoutMs` /
`requestTimeoutMs` occurrence is a WRITE of the literal 30000 so a def
satisfies the post-parse type.
2. `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.
## The gate, red before and green after
Both ledgers in place and both types in `GOVERNED`, before the README /
counts caught up — `pnpm --filter @objectstack/spec check:liveness`,
**exit 1**:
```
✗ 2 governed type(s) with NO row in the README state table:
connector
analytics_cube
✗ 1 README state-table heading error(s):
heading says 37 governed types, GOVERNED has 39
✗ the generated count artifact is not current:
packages/spec/liveness/state-counts.md is STALE — it does not match what the gate measures right now.
first difference at line 67:
- | **total** | **878** | **5** | **1** | **96** | **11** | **991** |
+ | `connector` | 20 | 0 | 0 | 53 | 1 | 74 |
✗ 2 row(s) where README.md and state-counts.md disagree:
connector — counted in state-counts.md, no row in the README table
analytics_cube — counted in state-counts.md, no row in the README table
✗ 1 UNDECLARED container inheritance — a blanket verdict covers keys nothing classified:
connector/webhooks — one verdict covers 21 unclassified child key(s): …
```
That run is also the answer to the seat's warning about
`liveness/README.md`: `check-liveness.mts` declares `readmeMissingRows`
for exactly this, so the README rows, the heading count and
`state-counts.md` are not optional extras — the gate reverse-requires
them. After the README rows + heading (37 → 39), `gen:liveness-counts`,
and the `connector/webhooks` row in `undrilled-containers.baseline.json`
— **exit 0**:
```
governance denominator: 30 authorable type(s) — 26 registered kind(s) + 4 unregistered-kind stack
collection(s) (analytics_cube, connector, sharing_rule, webhook); 30 governed, 0 awaiting a ledger.
(+ 9 type(s) governed from OUTSIDE the denominator via SPEC_ONLY_SCHEMAS — 39 governed in total.)
✓ every governed-type property, at every depth the ledger drills, is classified, every authorable type
— registered kind or unregistered-kind stack collection — is governed or explicitly pending, …
and the README state table carries a row for each of the 39 governed type(s) it claims to index.
✓ packages/spec/liveness/state-counts.md is current — the same 39 row(s).
```
`connector/webhooks` is RECORDED in the undrilled baseline rather than
deferred or drilled, and the ledger row says why: `WebhookConfigSchema`
is `WebhookSchema.extend({ events, signatureAlgorithm })`, so a
`deferred` row to the governed `webhook` type would be refused by the
gate's key-set EQUALITY check — correctly — and drilling would mean
writing 21 child rows of which 8 are the ADR-0010 protection envelope
this gate auto-classifies `live` everywhere else.
## Verification
| Command | Result |
|---|---|
| `pnpm --filter @objectstack/spec check:liveness` | exit 0 —
`PENDING_GOVERNANCE` empty, 39 governed |
| `pnpm --filter @objectstack/spec check:generated` | exit 0 — all 15
generated artifacts up to date |
| `pnpm --filter @objectstack/spec check:authorable-surface` | exit 0 —
1536 schemas generated |
| `pnpm --filter @objectstack/spec check:api-surface` | exit 0 — public
API surface unchanged |
| `pnpm --filter @objectstack/spec check:docs` | exit 0 — 223 generated
files in sync |
| `pnpm --filter @objectstack/spec typecheck` +
`check:scripts-typecheck` | exit 0 |
| `pnpm --filter @objectstack/spec exec vitest run scripts/liveness/` |
11 files, 315 tests passed |
| `pnpm check:platform-checklist` | exit 0 — 38 kinds mapped, 1 waived |
| `pnpm check:nul-bytes`, `check:published-files`, `check:merge-driver`,
`check:doc-authoring`, the three changeset gates + their self-tests,
`check:keyed-text-bounds`, `check:comment-mask-*`,
`check:closing-keyword-parity`, `check:pm-*` | exit 0 (22 families) |
`packages/spec` has no `lint` script; the repo runs one root `eslint .
--no-inline-config`, so the ESLint reading here is a **declared
narrowing** with its three pieces of evidence: (1) the universe comes
from the config itself — the only config object with a `files` glob for
source is `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`, and the seven
non-`.mts` paths in this diff are `.json` / `.md`, confirmed by running
ESLint on `liveness/connector.json` and getting `File ignored because no
matching configuration was supplied`; (2) `--format json` on the one
file this diff adds to that universe reports **1 file, 0 errors, 0
warnings**; (3) `eslint.config.mjs`'s own header states this repo "never
enables type-aware linting (no `parserOptions.project`, no typed
`@typescript-eslint` rules) for ANY file", so nothing in this diff can
move the verdict on a file it does not touch. The whole-farm run is
CI's. Both figures are read at `a442b583fb`.
**Widening tells.** `node scripts/pm/check-widening-tells.mjs
--declaration no --diff DIFFPATH (this PR's diff)` exits **0 with no
tell**, matching PR #18587 — but its own output is the honest reading
and it is not "clean": `8 changed file(s) — 0 judged against a declared
surface (no widening tell), 8 NOT MEASURED`, because no declared surface
covers ledger JSON, a changeset, a checklist map or a gate script.
Reported as NOT MEASURED rather than as a pass.
## File surface
| Path | Why |
|---|---|
| `packages/spec/liveness/connector.json` | new ledger (23 top-level
rows, 7 drilled containers) |
| `packages/spec/liveness/analytics_cube.json` | new ledger (9 top-level
rows, 4 drilled containers) |
| `packages/spec/scripts/liveness/check-liveness.mts` | both types into
`GOVERNED`; `PENDING_GOVERNANCE` emptied; its `[#18582]` note rewritten
(it said "two left") |
| `packages/spec/liveness/README.md` | from the pre-declared OPEN set —
two state-table rows and the heading count 37 → 39, both
reverse-required by `readmeMissingRows` / `readmeHeadingErrors`; the
closing `PENDING_GOVERNANCE` paragraph rewritten |
| `packages/spec/liveness/state-counts.md` | OPEN set — regenerated with
`gen:liveness-counts`, never hand-edited |
| `packages/spec/scripts/liveness/undrilled-containers.baseline.json` |
one recorded row, `connector/webhooks`, reverse-required by the
container-coverage leg (see above) |
| `docs/qa/platform-checklist/coverage.json` | OPEN set — two entries;
the map is keyed by ledger name and `check:platform-checklist` reds on
an unmapped kind. Existing key order left as it was |
| `.changeset/18582-connector-analytics-cube-liveness-ledgers.md` | OPEN
set — `patch`, because `liveness` is in this package's published
`files[]`, so both ledgers ship in the tarball |
Neither `packages/spec/src/ui/view.zod.ts` (PR #18561) nor
`packages/spec/scripts/check-generated.ts` (#17735) is touched.
## Acceptance notes
Seen and deliberately not fixed here — three are findings this seat asks
the dispatching seat to file, the rest are noted only:
- **to file (contract violation; dedupe words: `retryConfig` live claim,
connector timeouts, conversions registry comment):**
`packages/spec/src/conversions/registry.ts`'s
`connector-rate-limit-config-removed` entry asserts `retryConfig` "and
the timeouts beside it are untouched — they are live". Measured false;
the comment is what a later reader will trust.
- **to file (contract violation; dedupe words:
`bootstrapDeclaredWebhooks` docblock, connector-authored webhook,
sys_webhook source):** the materializer's docblock claims it
materializes each "stack/connector-authored webhook"; its source is
`readDeclared(…, 'webhook')`, which a connector's nested array never
reaches.
- **to file (metadata-authoring trap; dedupe words: `analytics_cube`
joins sql ON clause, synthesised FK equality, cube join relationship):**
`Cube.joins[].sql` is REQUIRED and documented as the join's ON clause,
and both strategies synthesise `ON "parent"."seg" = "alias"."id"`
without reading it, so a non-FK join condition returns a 200 carrying
different arithmetic than the author declared. `joins[].relationship` is
the same shape one key over.
- **noted, not filed:** `packages/spec/docs/SYNC_ARCHITECTURE.md` still
ticks "✅ Monitoring: Health checks, metrics, logging" and "✅ Conflict
Resolution: Multiple strategies" at L3, both unbacked on this surface —
the `health` and `syncConfig` subtrees are dead. Carrier: the next PR
that acts on the `syncConfig` / `health` ADR-0049 decision; that file is
the one an author reads before writing either block.
- **noted, not filed:** `analytics_cube.public` is an access-control key
that gates nothing. Not filed separately because the ledger row IS the
record and the remedy is the ADR-0049 decision the `dead` verdict opens.
Carrier: the enforce-or-remove sweep that reads this ledger.
- **noted, not filed:** `Metric.name` / `Dimension.name` are required
inner fields shadowed by their record key, so a disagreement is silently
resolved in the key's favour. Carrier: none — no PR and no person is
near these files today; recorded here so a later sweep does not have to
re-derive it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 684146b commit 559041d
8 files changed
Lines changed: 620 additions & 31 deletions
File tree
- .changeset
- docs/qa/platform-checklist
- packages/spec
- liveness
- scripts/liveness
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
22 | 27 | | |
23 | 28 | | |
24 | 29 | | |
| |||
50 | 55 | | |
51 | 56 | | |
52 | 57 | | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
53 | 67 | | |
54 | 68 | | |
55 | 69 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
816 | 816 | | |
817 | 817 | | |
818 | 818 | | |
819 | | - | |
| 819 | + | |
820 | 820 | | |
821 | 821 | | |
822 | 822 | | |
| |||
938 | 938 | | |
939 | 939 | | |
940 | 940 | | |
| 941 | + | |
| 942 | + | |
941 | 943 | | |
942 | 944 | | |
943 | 945 | | |
| |||
953 | 955 | | |
954 | 956 | | |
955 | 957 | | |
956 | | - | |
957 | | - | |
| 958 | + | |
| 959 | + | |
958 | 960 | | |
959 | 961 | | |
960 | 962 | | |
961 | | - | |
962 | | - | |
963 | | - | |
964 | | - | |
965 | | - | |
966 | | - | |
| 963 | + | |
| 964 | + | |
| 965 | + | |
| 966 | + | |
| 967 | + | |
| 968 | + | |
| 969 | + | |
| 970 | + | |
| 971 | + | |
0 commit comments