diff --git a/.changeset/18582-connector-analytics-cube-liveness-ledgers.md b/.changeset/18582-connector-analytics-cube-liveness-ledgers.md
new file mode 100644
index 00000000000..8422f3f3078
--- /dev/null
+++ b/.changeset/18582-connector-analytics-cube-liveness-ledgers.md
@@ -0,0 +1,15 @@
+---
+"@objectstack/spec": patch
+---
+
+`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).
+
+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".
+
+- **`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.
+- **`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.
+- **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.
+
+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.
+
+Clause-②: no
diff --git a/docs/qa/platform-checklist/coverage.json b/docs/qa/platform-checklist/coverage.json
index dc49f828c00..60b69e2957e 100644
--- a/docs/qa/platform-checklist/coverage.json
+++ b/docs/qa/platform-checklist/coverage.json
@@ -19,6 +19,11 @@
"ai.console-ai-surface-gating"
]
},
+ "analytics_cube": {
+ "items": [
+ "dashboards.cube-query"
+ ]
+ },
"api": {
"items": [
"api-backend.declarative-endpoint-execution",
@@ -50,6 +55,15 @@
"platform-core.docs-portal-render"
]
},
+ "connector": {
+ "items": [
+ "integration-system.connector-declarative-boot",
+ "integration-system.connector-auth-kind-application",
+ "integration-system.connector-descriptor-audit",
+ "integration-system.connector-degraded-recovery",
+ "automation.connector-dispatch-matrix"
+ ]
+ },
"crud_endpoints": {
"items": [
"api-backend.rest-crud-config-contract",
diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md
index f133b06265a..0a5e566480b 100644
--- a/packages/spec/liveness/README.md
+++ b/packages/spec/liveness/README.md
@@ -816,7 +816,7 @@ The governed set is `GOVERNED` at the top of `check-liveness.mts`. To add a type
RecordDetailView had been gating the History tab on it the whole time (#2707).
4. Add the type to `GOVERNED`; confirm the gate is green.
-## Current state — 37 governed types (complete registry coverage)
+## Current state — 39 governed types (complete registry coverage)
> **This heading is now checked** (#7257). `check:liveness` reconciles the table
> 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.
| 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) |
| 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 |
| 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. |
+| 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 |
+| 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 |
The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every
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
compares `GOVERNED` against the denominator in both directions (an ungoverned
authorable type fails; so does a stale pending row whose debt is already paid).
-⚠️ **The map is no longer empty, and that is #18133's finding rather than a
-regression.** Widening the denominator from the registered kinds to the authorable
+⚠️ **The map went from empty to three rows and back to empty, and neither move was
+a regression.** Widening the denominator from the registered kinds to the authorable
set (see the ⛔ note under *Source of truth* above) made three types visible that
had been in **neither** `GOVERNED` **nor** `PENDING_GOVERNANCE` — `connector`,
`sharing_rule` and `analytics_cube` — and therefore produced no row in any of the
-gate's lists while the report read complete. They are now declared debts with a
-reason and an issue number apiece, which is the state this ratchet exists to
-produce; the direction of travel is out of that map and into `GOVERNED`, exactly
-as it was for the nine. ⛔ Their presence is not a licence to leave them there.
-**One of the three is paid**: `sharing_rule` is governed as of #18582, and the map
-now holds `connector` and `analytics_cube`.
+gate's lists while the report read complete (#18133). Declaring them as debts with a
+reason and an issue number apiece is what a ratchet is for; paying them is the
+direction of travel, exactly as it was for the nine. **All three are now paid**:
+`sharing_rule` (PR #18587), then `connector` and `analytics_cube` together, each with
+a ledger beside this file — so `PENDING_GOVERNANCE` is empty again, this time with
+nothing hidden behind the emptiness. ⛔ The map stays in the gate: it is how the next
+authorable type that arrives ungoverned gets a declared debt instead of silence, and
+an entry added to it is a debt with an issue number, never a way to silence the gate
+on a type somebody has just registered.
diff --git a/packages/spec/liveness/analytics_cube.json b/packages/spec/liveness/analytics_cube.json
new file mode 100644
index 00000000000..72fc67b4aed
--- /dev/null
+++ b/packages/spec/liveness/analytics_cube.json
@@ -0,0 +1,152 @@
+{
+ "type": "analytics_cube",
+ "_note": "CubeSchema (packages/spec/src/data/analytics.zod.ts). Seeded 2026-09-17 (#18582): the LAST of the three PENDING_GOVERNANCE debts #18133 declared when PR #18581 widened the governance denominator from the registered kinds to `authorableTypes()`; `sharing_rule` was paid first (PR #18587) and `connector` is paid in the same diff as this file, which empties the map. NOT a registered metadata KIND — it is bound in `UNREGISTERED_KIND_SCHEMAS` (#10194) and reaches this walk through `getMetadataTypeSchema`'s unregistered-kind fallback, so the ledger governs it while `listMetadataTypeSchemaTypes()` still does not enumerate it. THE SHAPE FACT THAT DECIDES EVERY ROW BELOW: one Cube shape, THREE producers, one registry. `packages/services/service-analytics/src/cube-registry.ts` names them itself — (1) authored cubes (`defineStack({ analyticsCubes })` / `defineCube()`), threaded by the CLI into `AnalyticsServiceConfig.cubes` and registered by `registerAll`; (2) COMPILED DATASETS (ADR-0021), where `dataset-compiler.ts` MINTS a Cube from a `dataset` document; (3) ad-hoc query inference (`inferCubeFromQuery`). Only (1) is the authoring door this ledger governs, so a key whose only reader sits on path (2) is NOT live here however busy that reader is — that is the #4837 producer rule applied to a shape with three producers, and it is what decides `dimensions.granularities` and `measures.format` below. Every `live` row therefore carries a `producer` naming the CLI threading site: a consumer citation alone would be the `seed.env` shape, where the mechanism was right and nobody supplied the input. #10238 IS NOT PREJUDGED: the PENDING_GOVERNANCE row this file discharges said whether cube authoring is live end-to-end is its own measurement and 'this row does not prejudge it'. This ledger does not answer that question either — it answers the per-key one (who reads this key?), and the answers below are mixed: the query path (`sql`, `measures.sql`/`.type`, `dimensions.sql`/`.type`, `joins.name`) is genuinely consumed, while the caching, access-control and display-annotation keys are not. PREVIEW READ POINTS ENUMERATED (the #7131 mechanical rule, objectui @dda8f3815): `registerBuiltinPreviews()` in packages/app-shell/src/views/metadata-admin/previews/index.ts registers nineteen types and `analytics_cube` is NOT one of them — this type has no registered metadata-admin preview. Recorded rather than skipped, because 'the type has no registered preview' is the sentence a later sweep needs. What objectui DOES consume is the whole SHAPE: `clientValidation.ts` maps `analytics_cube` to `CubeSchema` itself, and unlike `sharing_rule` it is absent from `AUTHOR_SHAPE_ONLY_TYPES`, so both the CREATE and the EDIT door in metadata-admin refuse a cube this schema rejects. ADR-0054: no row here carries a `proof`, and none is owed — the `analytics` high-risk class binds `dataset/dimensions.dateGranularity` (the dataset door), not this type.",
+ "props": {
+ "name": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-analytics/src/cube-registry.ts#register — `this.cubes.set(cube.name, cube)`: the authored name IS the registry key, so it decides what `AnalyticsQuery.cube` can resolve at all; packages/services/service-analytics/src/analytics-service.ts#getMeta emits it as the cube's `name` and as the `${cube.name}.${key}` member id of every measure and dimension served by `GET /api/v1/analytics/meta`; packages/services/service-analytics/src/strategies/native-sql-strategy.ts#resolveMeasureSql names it in the `INVALID_FIELD` refusal for an undeclared member.",
+ "producer": "packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS — the `analytics` entry declares `configKey: 'analyticsCubes'` and the capability resolver threads it into the plugin (`const cubes = (config as any).analyticsCubes ?? (config as any).cubes ?? []; arg = { cubes }`); packages/services/service-analytics/src/analytics-service.ts#registerAll (`if (config.cubes) this.cubeRegistry.registerAll(config.cubes)`) is where the authored array becomes the registry every consumer below resolves through. Without this thread an authored cube reaches no reader at all — the `seed.env` shape (#4837).",
+ "note": "Identity, not display — `title` is the display half. A second cube registered under the same name silently replaces the first (`register` is a `Map.set`)."
+ },
+ "title": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-analytics/src/analytics-service.ts#getMeta — `title: cube.title` is the cube's display title on `GET /api/v1/analytics/meta`, the discovery surface a dashboard/report builder picks a cube from; packages/drivers/driver-memory/src/memory-analytics.ts#getMeta answers the identical projection for the in-memory driver, so the value reaches the wire on both execution paths.",
+ "producer": "packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS — the `analytics` entry declares `configKey: 'analyticsCubes'` and the capability resolver threads it into the plugin (`const cubes = (config as any).analyticsCubes ?? (config as any).cubes ?? []; arg = { cubes }`); packages/services/service-analytics/src/analytics-service.ts#registerAll (`if (config.cubes) this.cubeRegistry.registerAll(config.cubes)`) is where the authored array becomes the registry every consumer below resolves through. Without this thread an authored cube reaches no reader at all — the `seed.env` shape (#4837).",
+ "note": "Display-shaped, so the #7131 split settles it: being shown to a human IS the whole of the claimed effect, and there is no second layer where a 'real' consumer would live. The read point is the platform's own analytics discovery endpoint, NOT a metadata-admin preview — this type has none (enumerated in the file note above). `label` is the metric/dimension spelling of the same idea and is declared as an ALIAS of `title` on the cube (`strictObject` aliases), so a mis-spelled `label:` is refused with the corrective name rather than dropped."
+ },
+ "description": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Parsed, stored, and read by NOTHING. The `CubeMeta` projection both `getMeta` implementations build (`analytics-service.ts#getMeta`, `memory-analytics.ts#getMeta`) carries `name`, `title`, `measures` and `dimensions` — `description` is not in it, so unlike `title` it never reaches the discovery wire, and no strategy, driver or lint rule reads it. Census: zero reads of `cube.description` across packages/services, packages/drivers, packages/rest and packages/objectql, with `cube.title` as the lit control in the same scan (two hits, both cited on the `title` row above). ⛔ NOT an ADR-0049 retirement candidate on this reading alone: it is an administrative note on an authoring surface whose sibling annotations (`measures.description`, `dimensions.description`) are dead for the same reason and would go with it, and the `position.description` precedent keeps a documentation-shaped key that only documents. What it is NOT is enforced — an author who expects it on a dashboard picker is wrong today."
+ },
+ "sql": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-analytics/src/strategies/native-sql-strategy.ts#extractObjectName — `return cube.sql.trim()` is the FROM table of every generated statement and the object whose RLS/tenant read scope is injected (`applyReadScope(this.extractObjectName(cube), …)`); packages/services/service-analytics/src/strategies/objectql-strategy.ts#extractObjectName resolves the same value for the `engine.aggregate` path; packages/services/service-analytics/src/analytics-service.ts#cubeObjects adds `cube.sql.trim()` to the object set the tenant scope is computed over; packages/drivers/driver-memory/src/memory-analytics.ts#extractTableName reads it as the in-memory collection name.",
+ "producer": "packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS — the `analytics` entry declares `configKey: 'analyticsCubes'` and the capability resolver threads it into the plugin (`const cubes = (config as any).analyticsCubes ?? (config as any).cubes ?? []; arg = { cubes }`); packages/services/service-analytics/src/analytics-service.ts#registerAll (`if (config.cubes) this.cubeRegistry.registerAll(config.cubes)`) is where the authored array becomes the registry every consumer below resolves through. Without this thread an authored cube reaches no reader at all — the `seed.env` shape (#4837).",
+ "note": "'Base SQL statement or Table Name' in the describe, and the platform's own aliases (`table`, `sqlTable`) say which half is load-bearing: every consumer treats it as an OBJECT/TABLE name. Authoring it wrong is not silent — an unknown object fails the query."
+ },
+ "measures": {
+ "children": {
+ "name": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "The RECORD KEY is the identity, not this field. `measures` is `z.record(z.string(), MetricSchema)`, and every consumer resolves a measure by its key: `analytics-service.ts#getMeta` publishes `${cube.name}.${key}`, `native-sql-strategy.ts#lookupMember` looks the member up in the `cube.measures` bag by key, `memory-analytics.ts` indexes `cube.measures[fieldName]`. Nothing reads the inner `name`. The dataset compiler WRITES it (`dataset-compiler.ts`: `const metric: Metric = { name: m.name, … }; measures[m.name] = metric`) — writing it equal to the key it files the metric under, which is exactly why no reader ever needed it. Authoring a `name` that DISAGREES with the key is the trap: the key wins everywhere and the authored value is inert. ⛔ Not an ADR-0049 deletion candidate as it stands: it is `z.string().regex(…)` and REQUIRED, so removing it breaks every authored cube to delete nothing; the honest repairs are to derive it from the key or to make the disagreement an authoring refusal, and both are decisions rather than sweeps."
+ },
+ "label": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-analytics/src/analytics-service.ts#getMeta — `title: measure.label` is the human title of each measure on `GET /api/v1/analytics/meta`; packages/drivers/driver-memory/src/memory-analytics.ts#getMeta emits the identical projection.",
+ "producer": "packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS — the `analytics` entry declares `configKey: 'analyticsCubes'` and the capability resolver threads it into the plugin (`const cubes = (config as any).analyticsCubes ?? (config as any).cubes ?? []; arg = { cubes }`); packages/services/service-analytics/src/analytics-service.ts#registerAll (`if (config.cubes) this.cubeRegistry.registerAll(config.cubes)`) is where the authored array becomes the registry every consumer below resolves through. Without this thread an authored cube reaches no reader at all — the `seed.env` shape (#4837).",
+ "note": "Display-shaped and settled by the #7131 split, same as the cube's `title`. `CubeRegistry`'s own header names this read point ('maps every registered cube's measure/dimension `label` onto the `CubeMeta` titles served by GET /api/v1/analytics/meta'). REQUIRED on the schema, so there is no empty state to classify."
+ },
+ "description": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Not in the `CubeMeta` measure projection (`{ name, type, title }`) and read nowhere else — the same census, and the same reasoning, as the cube-level `description` row above."
+ },
+ "type": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-analytics/src/strategies/native-sql-strategy.ts#resolveMeasureSql — `const wrap = AGGREGATE_SQL[measure.type]` picks the aggregate the column is wrapped in (SUM / COUNT / AVG / …), `CONDITIONAL_AGGREGATE_SQL[measure.type]` picks its conditional form for a scoped measure filter, and `EXPRESSION_METRIC_TYPES.has(measure.type)` is what emits a custom expression UNWRAPPED; an unrecognised type throws rather than substituting `COUNT(*)` (#4157). packages/services/service-analytics/src/analytics-service.ts#getMeta also publishes it as the measure's `type` on `GET /api/v1/analytics/meta`.",
+ "producer": "packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS — the `analytics` entry declares `configKey: 'analyticsCubes'` and the capability resolver threads it into the plugin (`const cubes = (config as any).analyticsCubes ?? (config as any).cubes ?? []; arg = { cubes }`); packages/services/service-analytics/src/analytics-service.ts#registerAll (`if (config.cubes) this.cubeRegistry.registerAll(config.cubes)`) is where the authored array becomes the registry every consumer below resolves through. Without this thread an authored cube reaches no reader at all — the `seed.env` shape (#4837).",
+ "note": "The arithmetic itself: this key decides what number the query returns. `metric-type-coverage.test.ts` pins `AGGREGATE_SQL` ∪ `EXPRESSION_METRIC_TYPES` as an exact partition of the `AggregationMetricType` enum, so no spec-valid value can fall through."
+ },
+ "sql": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-analytics/src/strategies/native-sql-strategy.ts#resolveMeasureSql — `measure.sql` is the column (or expression) the aggregate is applied to, and `qualifyAndRegisterJoin(measure.sql, …)` is what lowers a dotted reference into a LEFT JOIN chain; packages/services/service-analytics/src/strategies/objectql-strategy.ts#resolveFieldName reads `measure.sql.replace(/^\\$/, '')` as the aggregate field for the `engine.aggregate` path.",
+ "producer": "packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS — the `analytics` entry declares `configKey: 'analyticsCubes'` and the capability resolver threads it into the plugin (`const cubes = (config as any).analyticsCubes ?? (config as any).cubes ?? []; arg = { cubes }`); packages/services/service-analytics/src/analytics-service.ts#registerAll (`if (config.cubes) this.cubeRegistry.registerAll(config.cubes)`) is where the authored array becomes the registry every consumer below resolves through. Without this thread an authored cube reaches no reader at all — the `seed.env` shape (#4837).",
+ "note": "REQUIRED, and the one place a cube author writes physical SQL. `'*'` is special-cased to a bare `*` (the COUNT(*) form); anything with a dot is either a relationship path or a SQL expression, and `IDENTIFIER_PATH` is what tells them apart (#4157)."
+ },
+ "format": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Written by the DATASET compiler, read by nobody — the three-producer fact in the file note decides this row. `dataset-compiler.ts` copies a dataset measure's own `format` onto the minted cube metric (`if (typeof m.format === 'string') metric.format = m.format`), and the value a caller actually receives is threaded from the DATASET measure, not from the cube: `analytics-service.ts` enriches the result's `fields[]` with `if (f.format == null && m.format) f.format = m.format`, where `m` is `dataset.measures[…]`. So the live key is `dataset.measures[].format` (governed in dataset.json), and the cube metric's own `format` is a carbon copy nothing reads back. On the AUTHORING door governed here there is no dataset to copy from, so a hand-written `format:` is inert. ⛔ Retirement is NOT the remedy: the key is the dataset compiler's own output slot on a shared shape, so deleting it from `MetricSchema` would break a live internal write; what is dead is authoring it by hand."
+ }
+ }
+ },
+ "dimensions": {
+ "children": {
+ "name": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Same shape and same verdict as `measures.name`: `dimensions` is a `z.record`, every consumer resolves by the record KEY (`getMeta` publishes `${cube.name}.${key}`, `lookupMember` indexes the bag, `memory-analytics.ts` reads `cube.dimensions[fieldName]`), and the dataset compiler writes the inner `name` equal to the key it files the dimension under (`dimensions[d.name] = dim`). A `name` that disagrees with its key is inert. Not a deletion candidate for the reason given on `measures.name`."
+ },
+ "label": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-analytics/src/analytics-service.ts#getMeta — `title: dimension.label` is the human title of each dimension on `GET /api/v1/analytics/meta`; packages/drivers/driver-memory/src/memory-analytics.ts#getMeta emits the identical projection.",
+ "producer": "packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS — the `analytics` entry declares `configKey: 'analyticsCubes'` and the capability resolver threads it into the plugin (`const cubes = (config as any).analyticsCubes ?? (config as any).cubes ?? []; arg = { cubes }`); packages/services/service-analytics/src/analytics-service.ts#registerAll (`if (config.cubes) this.cubeRegistry.registerAll(config.cubes)`) is where the authored array becomes the registry every consumer below resolves through. Without this thread an authored cube reaches no reader at all — the `seed.env` shape (#4837).",
+ "note": "Display-shaped, #7131 split, same as `measures.label`. The dataset side of the same field is where #6761 was found (an inline locale map was dropped and the machine name published as a title); a hand-authored cube declares a plain string, so that resolver is not in this path."
+ },
+ "description": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Not in the `CubeMeta` dimension projection (`{ name, type, title }`) and read nowhere else — same census as the two `description` rows above."
+ },
+ "type": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-analytics/src/strategies/native-sql-strategy.ts#carriesUninterpretableTemporalComparand — `this.lookupMember(cube, member, 'dimension')?.type === 'time' ? 'datetime' : null` decides how a filter comparand is shaped before it is bound as a parameter; packages/services/service-analytics/src/analytics-service.ts#getMeta publishes it as the dimension's `type` on `GET /api/v1/analytics/meta`; packages/services/service-analytics/src/dataset-executor.ts#timeDimensionsOf selects the bucketable dimensions with `compiled.cube.dimensions[d]?.type === 'time'`.",
+ "producer": "packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS — the `analytics` entry declares `configKey: 'analyticsCubes'` and the capability resolver threads it into the plugin (`const cubes = (config as any).analyticsCubes ?? (config as any).cubes ?? []; arg = { cubes }`); packages/services/service-analytics/src/analytics-service.ts#registerAll (`if (config.cubes) this.cubeRegistry.registerAll(config.cubes)`) is where the authored array becomes the registry every consumer below resolves through. Without this thread an authored cube reaches no reader at all — the `seed.env` shape (#4837).",
+ "note": "`time` is the member that carries behaviour (comparand shaping, bucketability); the other members are published and otherwise inert. The dataset-executor citation is on the compiled-dataset path — it is listed because it reads the same key off the same shape, and the authoring-door consumer is the comparand shaping in native-sql-strategy."
+ },
+ "sql": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-analytics/src/strategies/native-sql-strategy.ts#resolveDimensionSql — `dim.sql` is the GROUP BY / SELECT column, and `qualifyAndRegisterJoin(dim.sql, …)` lowers a dotted path into the LEFT JOIN chain; packages/services/service-analytics/src/strategies/objectql-strategy.ts#resolveFieldName reads `dim.sql.replace(/^\\$/, '')` as the group-by field for the `engine.aggregate` path.",
+ "producer": "packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS — the `analytics` entry declares `configKey: 'analyticsCubes'` and the capability resolver threads it into the plugin (`const cubes = (config as any).analyticsCubes ?? (config as any).cubes ?? []; arg = { cubes }`); packages/services/service-analytics/src/analytics-service.ts#registerAll (`if (config.cubes) this.cubeRegistry.registerAll(config.cubes)`) is where the authored array becomes the registry every consumer below resolves through. Without this thread an authored cube reaches no reader at all — the `seed.env` shape (#4837).",
+ "note": "REQUIRED. Same dotted-path / expression split as `measures.sql`."
+ },
+ "granularities": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "There IS a reader and an AUTHORED cube cannot reach it — the distinction the file note's three-producer fact exists to make. The one read is `packages/services/service-analytics/src/dataset-executor.ts#granularityOf` (`const cd = compiled.cube.dimensions[name]; … cd.granularities?.length === 1 ? String(cd.granularities[0]) : undefined`), whose argument is a `CompiledDataset` — a shape only `compileDataset()` produces, from a `dataset` document. The writers are the same two non-authoring producers: `dataset-compiler.ts` (`dim.granularities = d.dateGranularity ? [d.dateGranularity] : ['day','week','month','quarter','year']`), `cube-registry.ts#inferFromObject` and `analytics-service.ts#inferCubeFromQuery` (the 5-entry 'all granularities' list). A cube registered through `AnalyticsServiceConfig.cubes` never becomes a `CompiledDataset`, so a hand-authored `granularities:` is read by nothing. The AUTHORABLE key that really drives bucketing is `dataset.dimensions[].dateGranularity` — governed in dataset.json and the ADR-0054 `analytics` high-risk class's bound property. ⛔ ADR-0049 RETIREMENT IS NOT THE REMEDY, and this row says so explicitly so the enforce-or-remove channel does not act on the word `dead`: the key is the dataset compiler's own output channel on a shared shape, so deleting it from `DimensionSchema` would break a live internal path. What is dead is AUTHORING it on a hand-written cube."
+ }
+ }
+ },
+ "joins": {
+ "children": {
+ "name": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-analytics/src/strategies/native-sql-strategy.ts#qualifyAndRegisterJoin — `const joinTable = cube?.joins?.[alias]?.name ?? alias` is the TABLE the LEFT JOIN is emitted against, which is what makes an alias differ from its object (`account` -> `crm_account`); packages/services/service-analytics/src/strategies/native-sql-strategy.ts#crossFieldComparisonIn resolves `cube.joins?.[alias]?.name ?? alias` for every joined object when it walks the RLS read scopes; packages/services/service-analytics/src/analytics-service.ts#cubeObjects adds every `cube.joins[*].name` to the tenant-scoped object set; packages/services/service-analytics/src/strategies/objectql-strategy.ts#isCrossObjectField resolves the same value for the aggregate path.",
+ "producer": "packages/cli/src/commands/serve.ts#CAPABILITY_PROVIDERS — the `analytics` entry declares `configKey: 'analyticsCubes'` and the capability resolver threads it into the plugin (`const cubes = (config as any).analyticsCubes ?? (config as any).cubes ?? []; arg = { cubes }`); packages/services/service-analytics/src/analytics-service.ts#registerAll (`if (config.cubes) this.cubeRegistry.registerAll(config.cubes)`) is where the authored array becomes the registry every consumer below resolves through. Without this thread an authored cube reaches no reader at all — the `seed.env` shape (#4837).",
+ "note": "Security-relevant, not cosmetic: the joined object resolved here is what the read scope is computed for, so a wrong name would scope the wrong table. Note the JOIN is keyed by the record key (the alias) — `name` is its TARGET."
+ },
+ "relationship": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Declared with a `.default('many_to_one')` and dispatched on by nothing. The join SQL is SYNTHESISED, not read: `native-sql-strategy.ts#qualifyAndRegisterJoin` emits `LEFT JOIN
ON \"\".\"\" = \"\".\"id\"` for every hop, a foreign-key equality that is the same statement whichever cardinality the author declared. Census: `relationship` is WRITTEN by `dataset-compiler.ts` (`relationship: 'many_to_one'`, a constant) and read in neither strategy, neither driver, nor the REST layer; the lit control in the same scan is `cube.joins?.[alias]?.name`, which returns the hits cited on the `name` row above. The authoring trap is real — `one_to_many` parses, changes no SQL, and the aggregate silently keeps the many-to-one arithmetic."
+ },
+ "sql": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "The most expensive dead key on this type: it is REQUIRED, documented as 'Join condition (ON clause)', and the ON clause is synthesised without ever consulting it. `native-sql-strategy.ts#qualifyAndRegisterJoin` builds `ON \"\".\"\" = \"\".\"id\"` from the dotted member path alone; `objectql-strategy.ts` resolves joins through `cube.joins?.[alias]?.name` and lowers them to relationship traversals, with no ON clause at all. So an author who writes a non-FK join condition gets a DIFFERENT query than the one they declared, with a 200 and a plausible number — the `#10298` shape. ⛔ Not a mechanical ADR-0049 deletion: the key is required, so removing it edits every authored cube, and the honest fix is a decision (refuse a `sql` the engine will not honour, or teach the strategies to emit it). Recorded here so the decision has a measurement behind it."
+ }
+ }
+ },
+ "refreshKey": {
+ "children": {
+ "every": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Pre-aggregation / cache refresh cadence with no engine behind it. Census over the whole repository: `refreshKey` appears in `analytics.zod.ts` (the declaration), in `examples/app-showcase/src/data/analytics/showcase.cube.ts` (an AUTHOR writing it), in the generated authorable-surface artifacts, and in the protocol-18 strictness migration note — and in no consumer. There is no refresh scheduler, no pre-aggregation table and no cache keyed on it anywhere in packages/services, packages/drivers or packages/rest. The showcase authoring it is the evidence that the trap is live rather than theoretical."
+ },
+ "sql": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "The 'SQL to check for data changes' probe of the same unimplemented refresh mechanism — same census as `refreshKey.every`. The block's own `strictObject` history note ('a typo'd `sql` probe left the cube refreshing on nothing') describes a cadence that does not exist either way."
+ }
+ }
+ },
+ "public": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "An ACCESS-CONTROL key that gates nothing, which is the worst class this ledger records (a security no-op / false compliance, the reason the gate exists). Declared `z.boolean().default(false)` under the comment 'Access Control'. Census: three sites WRITE it — `analytics-service.ts#inferCubeFromQuery`, `cube-registry.ts#inferFromObject` and `dataset-compiler.ts`, each a literal `public: false` — and the showcase example authors `public: false`; nothing anywhere READS `cube.public`. Access to `/api/v1/analytics/query` is decided by the REST layer's own auth plus the per-object RLS/tenant scope `applyReadScope` injects from `cube.sql` and `cube.joins[].name`, never by this flag, so `public: true` grants nothing and `public: false` withholds nothing. ⛔ Do NOT read the dead verdict as 'the cube is exposed': it is not a hole that was opened, it is a knob that was never wired. ADR-0049 wants a DECISION here rather than a sweep — either enforce it at the query door or remove it — and the removal half is not free, because a `.default(false)` key is present on every parsed cube."
+ }
+ }
+}
diff --git a/packages/spec/liveness/connector.json b/packages/spec/liveness/connector.json
new file mode 100644
index 00000000000..c042fd735c8
--- /dev/null
+++ b/packages/spec/liveness/connector.json
@@ -0,0 +1,405 @@
+{
+ "type": "connector",
+ "_note": "DeclarativeConnectorEntrySchema (packages/spec/src/integration/connector.zod.ts). Seeded 2026-09-17 (#18582) together with `analytics_cube`: the last two of the three PENDING_GOVERNANCE debts #18133 declared when PR #18581 widened the governance denominator to `authorableTypes()` (`sharing_rule` was paid first, PR #18587). Their landing empties that map. NOT a registered metadata KIND — bound in `UNREGISTERED_KIND_SCHEMAS` (#6245) and reached through `getMetadataTypeSchema`'s unregistered-kind fallback. WHAT THE WALKER ACTUALLY RESOLVES, measured rather than assumed: 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 `shapeOf()` returns `ConnectorSchema`'s shape unchanged and the key set walked here is byte-identical to the base's — the ADR-0097 cross-field rules add no key and remove none. The gate therefore cannot tell the two schemas apart; what the binding buys is REFUSALS, which are invisible to the walk and visible only in the `authentication` / `actions` / `triggers` rows below, where they are the whole verdict. THE SHAPE FACT THAT DECIDES EVERY ROW: one schema, TWO doors. This ledger's denominator entry exists because of the AUTHORING doors (`defineStack({ connectors })` and `PUT /api/v1/meta/connector/:name`); the same `ConnectorSchema` is ALSO 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, and every row below says WHICH door its consumer is fed from. The keys an authored entry can reach are exactly the `ConnectorProviderContext` fields plus `name` and `enabled`; everything else in an authored entry is stored, served back by `/meta/connector`, and read by no runtime. That asymmetry is the trap this type carries, and it is recorded per key rather than asserted once. PRIOR MEASUREMENTS RE-VERIFIED, not inherited: the ADR-0087 conversion registry's `connector-field-mapping-transform-removed` entry recorded 'Execution: none — `fieldMappings` is spelled only inside packages/spec' (2026-08-06), the `syncConfig.schedule` retirement recorded '`syncConfig` has no reader outside `packages/spec`' (#16320, 2026-09-10), and `ConnectorTriggerSchema`'s own docblock says 'NOT YET ENFORCED — declared but never read by the runtime (#3197)'. All three were re-run on this checkout and all three still hold; the counts are in the rows. One prior claim FAILED re-verification and is corrected here: a comment in packages/spec/src/conversions/registry.ts asserts that `retryConfig` 'and the timeouts beside it are untouched — they are live'. They are not read anywhere; see those three rows. PREVIEW READ POINTS ENUMERATED (the #7131 mechanical rule, objectui @dda8f3815): `registerBuiltinPreviews()` registers nineteen types and `connector` is NOT one of them — this type has no registered metadata-admin preview. What objectui DOES consume is (a) the whole SHAPE, via `clientValidation.ts`, which maps `connector` to `DeclarativeConnectorEntrySchema` on BOTH the create and the edit door (it is not strict, so it may judge a stored body), and (b) the RUNTIME registry projection `GET /api/v1/automation/connectors`, from which `connectorsToOptions` reads `name`/`label`/`origin`, `connectorActionsToOptions` reads `actions[].key`/`.label`, and `connectorActionInputSchema` reads `actions[].inputSchema`. Those three are the cross-repo citations below. ADR-0054: no row carries a `proof` and none is owed — no high-risk class binds a `connector/*` path.",
+ "props": {
+ "name": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — the authored name keys the desired-set map, is the identity the §4 origin-conflict rule is checked against (`engine.getConnectorOrigin(name) === 'plugin'`), and is FORCED onto the provider's def (`const def = { ...materialization.def, name }`) so discovery and dispatch agree with the metadata; packages/services/service-automation/src/engine.ts#getConnectorDescriptors publishes it as the connector id on `GET /api/v1/automation/connectors`, which is what a `connector_action` flow node names in `connectorConfig.connectorId`.",
+ "producer": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — the ADR-0097 reconcile: it builds the desired set from the declared `connectors:` items, resolves the provider factory and calls `engine.registerConnector(def, handlers, 'declarative')`. This is the ONLY seam by which an authored entry reaches the connector registry, so it is the producer every `live` row below depends on (#4837): without it a declared connector is a stored document and nothing else.",
+ "note": "Identity, not display. A duplicate declared name is refused by name (`duplicate declarative connector instance name`), so this key also decides whether a stack boots."
+ },
+ "label": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidenceScope": "cross-repo",
+ "evidence": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — `label: entry.label ?? name` puts the authored label on `ConnectorProviderContext`; packages/connectors/connector-openapi/src/openapi-provider.ts, packages/connectors/connector-rest/src/rest-provider.ts and packages/connectors/connector-mcp/src/mcp-provider.ts each copy `label: ctx.label` onto the def they return; packages/services/service-automation/src/engine.ts#getConnectorDescriptors then publishes `label: def.label`; objectui @dda8f3815 packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.tsx#connectorsToOptions renders it as the connector picker's option text (`${label} (${name})`) in the flow designer.",
+ "producer": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — the ADR-0097 reconcile: it builds the desired set from the declared `connectors:` items, resolves the provider factory and calls `engine.registerConnector(def, handlers, 'declarative')`. This is the ONLY seam by which an authored entry reaches the connector registry, so it is the producer every `live` row below depends on (#4837): without it a declared connector is a stored document and nothing else.",
+ "note": "Display-shaped, so the #7131 split settles it: being shown to a human IS the whole claimed effect. All three shipped provider factories carry it, so this is the one authored display key that survives materialization intact — contrast `icon` and `type` below, which reach the same context object and are dropped."
+ },
+ "type": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-automation/src/engine.ts#getConnectorDescriptors — `type: def.type` is published on `GET /api/v1/automation/connectors`; packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors threads the authored value onto the provider context (`type: typeof entry.type === 'string' ? entry.type : 'api'`).",
+ "producer": "packages/connectors/connector-openapi/src/openapi-connector.ts (`type: 'api'` in the def literal), packages/connectors/connector-rest/src/rest-connector.ts and packages/connectors/connector-mcp/src/mcp-connector.ts — the def's `type` is supplied by the PLUGIN / provider factory, never carried over from the authored entry.",
+ "note": "⚠️ THE PRODUCER IS NOT THE AUTHOR, and that is the finding on this row. The authored `type` reaches `ConnectorProviderContext.type` and is then dropped: not one of the three shipped provider factories reads `ctx.type`, and `openapi-connector.ts` hard-codes `type: 'api'` into the def it returns. So on the PLUGIN door the key is live (a plugin writes it, `/automation/connectors` publishes it); on the DECLARATIVE door an authored `type: 'database'` on a provider-bound instance is published as whatever the factory chose. `type` is REQUIRED, so every author writes one. ⛔ Not an ADR-0049 candidate — it is enforced on one of its two doors; what is owed is either a factory that honours `ctx.type` or a declaration that it is provider-owned."
+ },
+ "description": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — `description: entry.description` on the provider context; packages/connectors/connector-openapi/src/openapi-provider.ts and packages/connectors/connector-mcp/src/mcp-provider.ts copy `description: ctx.description` onto the def; packages/services/service-automation/src/engine.ts#getConnectorDescriptors publishes `description: def.description`.",
+ "producer": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — the ADR-0097 reconcile: it builds the desired set from the declared `connectors:` items, resolves the provider factory and calls `engine.registerConnector(def, handlers, 'declarative')`. This is the ONLY seam by which an authored entry reaches the connector registry, so it is the producer every `live` row below depends on (#4837): without it a declared connector is a stored document and nothing else.",
+ "note": "Display-shaped (#7131). Two of the three shipped factories carry it — `connector-rest`'s does not, and writes its own fixed sentence instead — so an authored description survives on the openapi and mcp providers and is replaced on rest. The ADR-0097 §3 refusal message also points an author here (\"describe the auth scheme in `description`\"), which makes it the documented home for what `authentication` may not carry."
+ },
+ "icon": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-automation/src/engine.ts#getConnectorDescriptors — `icon: def.icon` is published on `GET /api/v1/automation/connectors`; packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors threads the authored value onto the provider context (`icon: entry.icon`).",
+ "producer": "packages/connectors/connector-openapi/src/openapi-connector.ts — `icon: config.icon ?? 'globe'`, where `config` is the factory's OWN argument object; the def's icon is supplied by the plugin, not by the authored entry.",
+ "note": "⚠️ Same split as `type`, and measured the same way: `ConnectorProviderContext.icon` is SET by the materializer and read by none of the three shipped provider factories (census: zero `ctx.icon` reads across packages/connectors, with `ctx.label` — four hits — as the lit control in the same scan). So an authored `icon:` on a declarative instance never reaches `/automation/connectors`, while a plugin-registered def's icon does. Optional, unlike `type`, so the trap is quieter."
+ },
+ "authentication": {
+ "status": "planned",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/spec/src/integration/connector.zod.ts#DeclarativeConnectorEntrySchema — `if (entry.authentication && entry.authentication.type !== 'none')` adds a custom issue on every authored entry, descriptor and provider-bound instance alike (#7990): the parse FAILS and the message names `auth: { type, credentialRef }` as the mechanism to use instead. Both authoring doors run it — `stack.zod.ts` binds this schema element-wise to `connectors:`, and `/meta/connector/:name` resolves it through `UNREGISTERED_KIND_SCHEMAS`.",
+ "producer": "packages/connectors/connector-openapi/src/openapi-connector.ts and packages/connectors/connector-rest/src/rest-connector.ts — `authentication: auth` on the def a provider factory returns, built from the RESOLVED credential, never from an authored value.",
+ "note": "PLANNED, deliberately not `dead` and deliberately not `live`, on the `api.inputMapping.transform` precedent: the only value an author may write is `{ type: 'none' }`, and anything else is LOUDLY REFUSED rather than silently dropped — an author who inlines a token is told so and told what to do instead, because a published connector row lands whole in `sys_metadata` where it would be cleartext at rest. Not `live`: the accepted value does nothing, and `.authentication` is read by NO consumer anywhere outside packages/spec (census across packages/connectors, packages/services, packages/rest and packages/runtime: every hit is a WRITE — `authentication: auth` or `authentication: { type: 'none' }` — or a TYPE reference `Connector['authentication']`). Not `dead`: the refusal is real and is the whole point of the key's presence on this door. ⛔ Not an ADR-0049 enforce-or-remove candidate: it is already enforced, by refusal."
+ },
+ "provider": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — presence of `provider` is what flips an entry from an inert catalog descriptor to an instance declaration (`if (typeof entry.provider !== 'string' || entry.provider.length === 0) continue`), and its value selects the factory (`engine.getConnectorProvider(provider)`); an unknown provider is a hard, named boot failure listing the installed ones. packages/spec/src/integration/connector.zod.ts#DeclarativeConnectorEntrySchema also branches every cross-field rule on it (`const isInstance = typeof entry.provider === 'string' && …`).",
+ "producer": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — the ADR-0097 reconcile: it builds the desired set from the declared `connectors:` items, resolves the provider factory and calls `engine.registerConnector(def, handlers, 'declarative')`. This is the ONLY seam by which an authored entry reaches the connector registry, so it is the producer every `live` row below depends on (#4837): without it a declared connector is a stored document and nothing else.",
+ "note": "The load-bearing key of ADR-0097 and the only one on this type whose absence changes the MEANING of every other key: without it `providerConfig` and `auth` are refused, `actions`/`triggers` become legal, and the entry is audited as a descriptor (#2612) instead of materialized."
+ },
+ "providerConfig": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — `providerConfig: entry.providerConfig ?? {}` on the provider context; packages/connectors/connector-openapi/src/openapi-provider.ts reads `ctx.providerConfig` for `spec` and `baseUrl` (and refuses a non-string `baseUrl` by name), packages/connectors/connector-rest/src/rest-provider.ts requires `providerConfig.baseUrl` and validates `defaultHeaders`, packages/connectors/connector-mcp/src/mcp-provider.ts reads `cfg.transport`; packages/spec/src/integration/connector.zod.ts#DeclarativeConnectorEntrySchema refuses it outright on an entry with no `provider`.",
+ "producer": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — the ADR-0097 reconcile: it builds the desired set from the declared `connectors:` items, resolves the provider factory and calls `engine.registerConnector(def, handlers, 'declarative')`. This is the ONLY seam by which an authored entry reaches the connector registry, so it is the producer every `live` row below depends on (#4837): without it a declared connector is a stored document and nothing else.",
+ "note": "Deliberately `z.record(z.string(), z.unknown())` — validated by the provider factory, not by this schema (ADR-0023). Not a container as far as this walk is concerned: a record of `unknown` has no child shape, so there is no subtree to drill and nothing is riding on a blanket verdict."
+ },
+ "auth": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — `auth = await this.resolveInstanceAuth(entry.auth, resolver, name, provider)` resolves the declared `credentialRef` through the secrets/env layer and puts the RESOLVED credential on the provider context; a failure there degrades or fails the instance by name (ADR-0097 §3). packages/connectors/connector-rest/src/rest-provider.ts, packages/connectors/connector-openapi/src/openapi-provider.ts and packages/connectors/connector-mcp/src/mcp-provider.ts each read `ctx.auth` and use it to sign the upstream call. packages/spec/src/integration/connector.zod.ts#DeclarativeConnectorEntrySchema refuses it on an entry with no `provider`.",
+ "producer": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — the ADR-0097 reconcile: it builds the desired set from the declared `connectors:` items, resolves the provider factory and calls `engine.registerConnector(def, handlers, 'declarative')`. This is the ONLY seam by which an authored entry reaches the connector registry, so it is the producer every `live` row below depends on (#4837): without it a declared connector is a stored document and nothing else.",
+ "note": "The live half of the pair whose other half (`authentication`) is refused: this key is how an authored connector carries a credential at all, and it carries a REFERENCE, never the secret."
+ },
+ "actions": {
+ "children": {
+ "key": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidenceScope": "cross-repo",
+ "evidence": "packages/services/service-automation/src/engine.ts#registerConnector — `if (typeof handlers[action.key] !== 'function') throw` refuses a def whose declared action has no handler, so the key is the contract between declaration and implementation; packages/services/service-automation/src/engine.ts#resolveConnectorActionEffect looks the action up by `a.key === actionId`, and `getConnectorActionHandler` dispatches `handlers[actionId]`; packages/services/service-automation/src/engine.ts#getConnectorDescriptors publishes it; objectui @dda8f3815 packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.tsx (`connectorActionsToOptions`) and .../connector-input-fields.ts#connectorActionInputSchema both resolve an action by `a.key === actionKey`.",
+ "producer": "packages/connectors/connector-mcp/src/mcp-connector.ts — `actions: selected.map((tool) => ({ key: tool.name, … }))` derives the action set from the upstream `tools/list`; packages/connectors/connector-openapi/src/openapi-connector.ts derives one action per OpenAPI operation. ⚠️ THE METADATA AUTHOR IS NOT A PRODUCER HERE: packages/spec/src/integration/connector.zod.ts#DeclarativeConnectorEntrySchema REFUSES `actions` on a provider-bound instance (ADR-0097 §5 — the provider derives them), and on a descriptor (no `provider`) the entry never reaches the registry at all.",
+ "note": "Live on the PLUGIN/provider door, inert-and-audited on the authoring door — the two-door fact in the file note, at its sharpest. What an authored `actions[]` on a descriptor DOES do is trigger a boot warning: packages/services/service-automation/src/plugin.ts#findInertDeclaredConnectors returns every declared connector that has at least one action, no runtime registration and no `enabled: false` opt-out, and `auditDeclaredConnectors` warns once per boot naming them and the remedy. That warning is the #2612 contract made audible; it is not dispatch."
+ },
+ "label": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidenceScope": "cross-repo",
+ "evidence": "packages/services/service-automation/src/engine.ts#getConnectorDescriptors — `label: a.label` on each projected action; objectui @dda8f3815 packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.tsx (`connectorActionsToOptions`, fed by `useConnectorActionOptions` off `GET /api/v1/automation/connectors`) renders it as the action picker's option text in the flow designer.",
+ "producer": "packages/connectors/connector-mcp/src/mcp-connector.ts — `label: titleize(slugify(tool.name))`; packages/connectors/connector-openapi/src/openapi-connector.ts derives one per operation. Same two-door caveat as `actions.key`.",
+ "note": "Display-shaped and settled by the #7131 split — the designer's action picker IS the claimed effect. REQUIRED, so there is no empty state."
+ },
+ "description": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Projected onto the wire and read by nobody. `engine.ts#getConnectorDescriptors` copies `description: a.description` into the `GET /api/v1/automation/connectors` payload, and no consumer in either repo reads it back: objectui's three connector-descriptor consumers take `name`/`label`/`origin` (`connectorsToOptions`), `key`/`label` (`connectorActionsToOptions`) and `key`/`inputSchema` (`connectorActionInputSchema`), and nothing in this repo reads a projected action's description. The lit control for that scan is `inputSchema` in the same projection, which IS read (see that row). Being on a machine-readable surface is not a consumer — the `view.label` precedent."
+ },
+ "inputSchema": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidenceScope": "cross-repo",
+ "evidence": "packages/services/service-automation/src/engine.ts#getConnectorDescriptors — `inputSchema: a.inputSchema` on the projected action; objectui @dda8f3815 packages/app-shell/src/views/metadata-admin/inspectors/connector-input-fields.ts#connectorActionInputSchema finds the committed action in that payload and returns its `inputSchema`, and `FlowNodeInspector.tsx` types the connector node's whole Input section from it (`connectorInputFields(connectorActionInputSchema(...))`, objectui #4305) — an action that declares none falls back to the generic key/value repeater, which is the observable difference.",
+ "producer": "packages/connectors/connector-mcp/src/mcp-connector.ts — the MCP tool's own JSON Schema is passed straight through ('The MCP inputSchema is already JSON Schema'); packages/connectors/connector-openapi/src/openapi-connector.ts derives it from the operation's parameters. Same two-door caveat as `actions.key`.",
+ "note": "The one action key with a structural (not display) consumer, and it is cross-repo: the designer builds a typed form from it. Declared `z.record(z.string(), z.unknown())` — JSON Schema by convention, unvalidated here — so it has no child shape and nothing rides on a blanket verdict."
+ },
+ "outputSchema": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "The twin of `inputSchema`, projected the same way and consumed by nothing — which is exactly what makes this row falsifiable rather than a guess. `engine.ts#getConnectorDescriptors` publishes `outputSchema: a.outputSchema`; the census over both repos finds no reader, while the identically-projected `inputSchema` one line above returns objectui's `connectorActionInputSchema` in the same scan. A flow node's downstream references are typed from the RUN's actual output (`nodeOutputRefs`), not from this declaration. ⛔ Not an ADR-0049 sweep candidate on this reading: the honest repair is to type the node's output refs from it, which is a feature decision, not a deletion."
+ },
+ "effect": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-automation/src/builtin/connector-nodes.ts — `const effect = engine.resolveConnectorActionEffect(cfg.connectorId, cfg.actionId)` is resolved BEFORE dispatch and decides what the run reports: `write` → `{ acted: 1 }`, `read` → `{ acted: 0 }`, undeclared → `{ unmeasuredEffect: true }`, so the declaration is what lets a connector-driven flow contribute a signal to the broken-sweep alert at all (#4395); packages/services/service-automation/src/engine.ts#resolveConnectorActionEffect reads it off the stored def (`.def.actions?.find((a) => a.key === actionId)?.effect`); packages/services/service-automation/src/engine.ts#getConnectorDescriptors also carries it to the designer.",
+ "producer": "packages/spec/src/integration/connector.zod.ts#ConnectorActionSchema names the producer itself: 'This is the ONLY producer of the effect' — whoever writes the def writes it, and both the hand-registered path and the ADR-0097 materialization path read it from there. Same two-door caveat as `actions.key`.",
+ "note": "Optional on purpose: a connector written before the key keeps reporting `unmeasuredEffect`, so declaring it is a strict improvement rather than a migration. The empty state is therefore meaningful and benign, which is why it is not `authorWarn`ed."
+ }
+ }
+ },
+ "triggers": {
+ "children": {
+ "key": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "The whole trigger sub-surface is declared and unread, and the schema says so itself: `ConnectorTriggerSchema`'s docblock reads '⚠️ NOT YET ENFORCED — declared but never read by the runtime (#3197). AutomationEngine.registerConnector ignores a connector's triggers … No polling loop or webhook receiver is driven by these definitions.' Re-verified on this checkout: `registerConnector` walks `parsed.actions` only, and the `this.triggers` map in engine.ts is the FLOW trigger-type registry (`schedule`/`record`/…), a different vocabulary that no connector trigger ever enters — that near-miss is the reason this row spells the control out. The one runtime touch of the key is a REFUSAL: packages/spec/src/integration/connector.zod.ts#DeclarativeConnectorEntrySchema rejects `triggers` on a provider-bound instance (ADR-0097 §5). On a descriptor it parses and evaporates. ⛔ ADR-0049 is owed a decision here rather than a sweep — either a polling/receiver engine or a retirement of the whole `ConnectorTriggerSchema` shape; the sub-keys below are dead for this one reason and are not repeated per row."
+ },
+ "label": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `triggers.key` — nothing reads a connector trigger."
+ },
+ "description": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `triggers.key`."
+ },
+ "type": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `triggers.key`. `polling` and `webhook` name two engines that do not exist for connectors: no polling loop reads `intervalSeconds`, and the webhook dispatcher is driven by `sys_webhook` rows materialized from the TOP-LEVEL `webhooks:` collection (see the `webhooks` subtree below)."
+ },
+ "intervalSeconds": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `triggers.key`. Renamed from `interval` by the protocol-18 conversion (#15680/#14478) so the unit lives in the key name — a rename that made the declaration honest without making it enforced."
+ },
+ "interval": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "REMOVED (#15680, ruling B on #14478) — tombstoned at the schema with `retiredKey`, which carries the prescription and makes authoring it both a tsc error and a parse error; sources are renamed by the protocol-18 conversion. The row stays because `retiredKey` keeps the key in the walked shape (the `rls.priority` precedent). Use `intervalSeconds`; the value (seconds) is unchanged. The tombstone is packages/spec/src/integration/connector.zod.ts#ConnectorTriggerSchema."
+ }
+ }
+ },
+ "syncConfig": {
+ "children": {
+ "strategy": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "`syncConfig` has NO reader outside packages/spec, and this ledger re-measured it rather than inheriting the claim: the only non-spec occurrence of the word in packages/ or examples/ is a prose comment in packages/qa/dogfood/test/expression-conformance.ledger.ts. No engine performs a connector-attached sync — no strategy dispatch, no direction dispatch, no batching, no conflict resolution. The same finding is recorded at the schema by the #16320 retirement of `syncConfig.schedule` ('nothing ever evaluated it'), which deleted the cron slot outright and left the rest of the block standing. ⛔ ADR-0049 owes this subtree a DECISION, not a mechanical sweep: it is seven keys of a declared capability the docs advertise (packages/spec/docs/SYNC_ARCHITECTURE.md ticks 'Conflict Resolution: multiple strategies'), so retiring it is a product call. The sub-keys below are dead for this one reason."
+ },
+ "direction": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `syncConfig.strategy` — no connector-attached sync engine exists."
+ },
+ "realtimeSync": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `syncConfig.strategy`. The `webhooks` subtree below is the mechanism this flag claims to enable, and it is dead on this type too."
+ },
+ "timestampField": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `syncConfig.strategy` — nothing tracks a last-sync watermark for a connector."
+ },
+ "conflictResolution": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `syncConfig.strategy`. Its default (`latest_wins`) is the kind of value that reads as a configured policy and resolves nothing."
+ },
+ "batchSize": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `syncConfig.strategy` — no bulk path reads it."
+ },
+ "deleteMode": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `syncConfig.strategy`. `soft_delete` by default, applied by nothing."
+ },
+ "filters": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `syncConfig.strategy`. Declared `z.record(z.string(), z.unknown())`, so it is not a container for this walk and no subtree rides on this verdict."
+ }
+ }
+ },
+ "fieldMappings": {
+ "children": {
+ "source": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Re-verified, not inherited: the ADR-0087 conversion registry's `connector-field-mapping-transform-removed` entry measured (2026-08-06) that '`fieldMappings` is spelled only inside packages/spec — the four connector packages, the automation engine, REST and objectui never read it'. That still holds on this checkout: zero occurrences of `fieldMappings` anywhere in packages/ or examples/ outside packages/spec. A connector field mapping is parsed, stored, and moves no value. ⛔ Same disposition as `syncConfig`: a decision, not a sweep — the whole L3 sync layer it belongs to is declared and unbuilt. Sub-keys below are dead for this one reason."
+ },
+ "target": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `fieldMappings.source`."
+ },
+ "transform": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "REMOVED (#5552, ADR-0049) — tombstoned on the shared base `FieldMappingSchema` with `retiredKey`, which retires all three authorable spellings at once; the whole five-member `FieldMappingTransform` union went with it, because no runtime executed any of them. The row stays because `retiredKey` keeps the key in the walked shape (the `rls.priority` precedent). Value conversion belongs on a surface that runs it: the import mapping's own `transform` (`data/mapping.zod.ts`), applied row by row by the REST import path and recorded live in mapping.json."
+ },
+ "defaultValue": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `fieldMappings.source`."
+ },
+ "dataType": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `fieldMappings.source`. SYNC_ARCHITECTURE.md's corrected note says an L3 mapping 'declares `dataType` and `syncMode` and performs no value transformation' — true about the transform half, and this row records that the declaration half is not read either."
+ },
+ "required": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `fieldMappings.source`. A `.default(false)` that no validation consults."
+ },
+ "syncMode": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `fieldMappings.source`. `read_only` / `write_only` / `bidirectional` gate nothing — there is no sync to gate."
+ }
+ }
+ },
+ "webhooks": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "A connector's NESTED webhook array is not the collection the dispatcher reads, and nothing else reads it either. `bootstrapDeclaredWebhooks` (packages/plugins/plugin-webhooks/src/bootstrap-declared-webhooks.ts) materializes `sys_webhook` rows from `readDeclared(engine, metadataService, 'webhook')` — metadata ITEMS of type `webhook`, which the decomposition registers from the TOP-LEVEL `webhooks:` stack collection (`METADATA_ARRAY_KEYS` in packages/objectql/src/engine.ts, and `webhooks: 'webhook'` in packages/metadata/src/plugin.ts). `connectors: 'connector'` in that same map registers a connector entry WHOLE, so the array nested inside it never becomes a `webhook` item and never reaches the materializer. Census: the only `.webhooks` reads outside packages/spec are of `stack.webhooks` — packages/lint/src/validate-functional-completeness.ts (`for (const hook of entriesOf(stack.webhooks))`) and the bootstrap's own docblock — with no read of a connector's own array anywhere. ⚠️ That docblock says it materializes each 'stack/connector-authored webhook'; the phrase is not backed by its code on this checkout, and it is what makes this key look live. ⛔ ADR-0049 owes a decision rather than a sweep — either decompose a connector's webhooks into `webhook` items, or retire the key. WHY ONE VERDICT COVERS THE SUBTREE, declared rather than assumed: the coordinate is recorded in scripts/liveness/undrilled-containers.baseline.json. `WebhookConfigSchema` is `WebhookSchema.extend({ events, signatureAlgorithm })`, so a `deferred` row pointing at the governed `webhook` type would be refused by the gate's key-set EQUALITY check — correctly, because the extension adds two keys the target does not classify. Drilling would mean writing 21 child rows of which 8 are the ADR-0010 protection envelope the gate auto-classifies `live` everywhere else and 13 would repeat this one sentence: fabricated granularity over a container that is dead as a whole. The recorded row is the honest form, and it leaves the baseline as soon as the decision above is taken."
+ },
+ "rateLimitConfig": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "REMOVED in @objectstack/spec 17.0.0 (#4911, ADR-0049 D2) — the entire `ConnectorRateLimitConfig` / `RateLimitStrategy` shape went, not just this key, because no outbound rate-limiting engine ever existed: the platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests. Tombstoned with `retiredKey`, which is why the key is still in the walked shape (the `rls.priority` precedent) and why authoring it is both a tsc error and a parse error. ⛔ Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the provider or upstream gateway. The tombstone is packages/spec/src/integration/connector.zod.ts#ConnectorSchema."
+ },
+ "retryConfig": {
+ "children": {
+ "strategy": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "⚠️ THIS ROW CORRECTS A PRIOR IN-REPO CLAIM. A comment in packages/spec/src/conversions/registry.ts (the `connector-rate-limit-config-removed` entry) states '`retryConfig` and the timeouts beside it are untouched — they are live'. Measured on this checkout: the word `retryConfig` does not occur anywhere in packages/ or examples/ outside packages/spec — zero files. Nothing retries a connector call on a strategy, a backoff, a jitter or a status-code list; a provider factory's handler makes one fetch. The claim was about what the RATE-LIMIT retirement did not touch, and it was read as a liveness verdict it never measured. ⛔ ADR-0049 owes a decision, not a sweep: `retryConfig` is the documented answer for a rate-limited upstream (SYNC_ARCHITECTURE.md points an author here because its `retryableStatusCodes` default includes 429), so retiring it removes the only advice the docs give. Sub-keys below are dead for this one reason."
+ },
+ "maxAttempts": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `retryConfig.strategy` — no retry loop reads it."
+ },
+ "initialDelayMs": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `retryConfig.strategy`."
+ },
+ "maxDelayMs": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `retryConfig.strategy`."
+ },
+ "backoffMultiplier": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `retryConfig.strategy`."
+ },
+ "retryableStatusCodes": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `retryConfig.strategy`. Its `[408, 429, 500, 502, 503, 504]` default is the value SYNC_ARCHITECTURE.md cites when it tells an author this is the answer for a 429 — a default that is read by nothing."
+ },
+ "retryOnNetworkError": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `retryConfig.strategy`."
+ },
+ "jitter": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `retryConfig.strategy`."
+ }
+ }
+ },
+ "connectionTimeoutMs": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "⚠️ Write-only, and the same prior claim corrected on `retryConfig.strategy` covers it ('the timeouts beside it … are live'). Every occurrence outside packages/spec is a WRITE of the literal 30000 into a def so the object satisfies the post-parse `Connector` type — packages/connectors/connector-{mcp,openapi,rest,slack} and the degraded-husk literal in packages/services/service-automation/src/plugin.ts — and the comment above each says exactly that ('Defaulted by ConnectorSchema; set explicitly so the literal satisfies the (post-parse) Connector output type'). No fetch, transport or handler reads it, so an authored timeout does not bound any call. The descriptor-only contract (#2612) is the other half of why this is inert on the authoring door: a `connectors:` entry with no `provider` never reaches the registry at all, and a provider-bound one contributes only `name`/`label`/`description`/`icon`/`type`/`providerConfig`/`auth` through `ConnectorProviderContext` — the rest of the entry is not carried anywhere."
+ },
+ "requestTimeoutMs": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Write-only, identical census and identical five write sites as `connectionTimeoutMs` — see that row. The two are declared with the same bounds and the same 30000 default and are read by nothing; a connector call is unbounded whatever an author writes."
+ },
+ "status": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Declared `ConnectorStatusSchema` with a `.default('inactive')`, and dispatched on by nothing — no consumer reads `def.status` in this repo, and the runtime's answer to 'can this connector be dispatched?' is a DIFFERENT field: `RegisteredConnector.state` (`ready` / `degraded`, #3017), which `getConnectorDescriptors` publishes as `state` and which no authored value can set. The two names one letter apart on one wire payload are the trap: `status: 'active'` on an authored entry neither enables nor advertises anything, while `state` — the field a reader will actually see — is computed. The keys that DO decide participation are `enabled` (materialization + the #2612 audit opt-out) and `provider`. ⛔ ADR-0049 owes a decision: retire it, or make it the authored half of the dispatchability answer."
+ },
+ "enabled": {
+ "status": "live",
+ "verifiedAt": "2026-09-17",
+ "evidence": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — `if (bound.enabled === false) continue` keeps the instance out of the desired set, and an instance that WAS materialized and is then disabled is torn down (`dematerializeConnector`) and dropped from the degraded-retry map, so `false` withdraws a live connector rather than merely not creating one; packages/services/service-automation/src/plugin.ts#findInertDeclaredConnectors treats `enabled: false` as the explicit opt-out that marks a deliberate catalog-only descriptor and silences the #2612 boot warning.",
+ "producer": "packages/services/service-automation/src/plugin.ts#materializeDeclaredConnectors — the ADR-0097 reconcile: it builds the desired set from the declared `connectors:` items, resolves the provider factory and calls `engine.registerConnector(def, handlers, 'declarative')`. This is the ONLY seam by which an authored entry reaches the connector registry, so it is the producer every `live` row below depends on (#4837): without it a declared connector is a stored document and nothing else.",
+ "note": "One of exactly two authored keys (with `provider`) that decide whether an entry participates at all, and the only one whose `false` is load-bearing in TWO directions — it removes a materialized connector AND suppresses an audit warning. Its `.default(true)` is why an omitted value is the participating one."
+ },
+ "errorMapping": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "RETIRED (ADR-0049 enforce-or-remove) — eleven authorable keys (`ErrorMappingConfig` ×4, `ErrorMappingRule` ×7) that nothing in the tree ever read, one of them spelled `userMessage`, the name of the LIVE API-error channel, so writing a rule here validated, published and showed nobody anything. Tombstoned with `retiredKey` because `ConnectorSchema` is not `.strict()` and a plain delete would be a silent strip (ADR-0104); the tombstone is inherited by `DeclarativeConnectorEntrySchema`, so `stack.connectors[]` and `/meta/connector` refuse it too. The row stays because `retiredKey` keeps the key in the walked shape (the `rls.priority` precedent). Registered as `integration/Connector:errorMapping` and `integration/DeclarativeConnectorEntry:errorMapping` in `RETIRED_KEYS_BY_MAJOR[18]`; sources are rewritten by the D2 conversion `connector-error-mapping-removed`. The tombstone is packages/spec/src/integration/connector.zod.ts#ConnectorSchema."
+ },
+ "health": {
+ "children": {
+ "healthCheck": {
+ "children": {
+ "enabled": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "No connector health-check loop exists. Census: the only `healthCheck` occurrences outside packages/spec are the KERNEL's own plugin health contract (`packages/core/src/health-monitor.ts`, `PluginHealthCheck`) — a different shape on a different subject — and nothing in packages/connectors or packages/services/service-automation polls a connector endpoint, counts consecutive failures, or acts on a threshold. SYNC_ARCHITECTURE.md ticks '✅ Monitoring: Health checks, metrics, logging' at L3; that tick is not backed on this surface. ⛔ ADR-0049 owes a decision rather than a sweep. The sub-keys below are dead for this one reason."
+ },
+ "intervalMs": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `health.healthCheck.enabled` — nothing schedules the probe."
+ },
+ "timeoutMs": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `health.healthCheck.enabled`."
+ },
+ "endpoint": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `health.healthCheck.enabled` — no request is ever made to it."
+ },
+ "method": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `health.healthCheck.enabled`."
+ },
+ "expectedStatus": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `health.healthCheck.enabled`."
+ },
+ "unhealthyThreshold": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `health.healthCheck.enabled`."
+ },
+ "healthyThreshold": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `health.healthCheck.enabled`."
+ }
+ }
+ },
+ "circuitBreaker": {
+ "children": {
+ "enabled": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "No circuit breaker exists for connectors. Census: nothing outside packages/spec reads `circuitBreaker` except the protocol-18 rename conversion in packages/spec/src/conversions/registry.ts, which rewrites the key's NAME and is not a consumer. No state machine opens, half-opens or closes anything, and no call path consults a breaker before dispatching a connector action. The nearest real mechanism is the ADR-0097 DEGRADED instance (#3017) — a husk registered with `state: 'degraded'` and a backoff retry — which is driven by materialization failures and reads none of these keys. ⛔ ADR-0049 owes a decision rather than a sweep. Sub-keys below are dead for this one reason."
+ },
+ "failureThreshold": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `health.circuitBreaker.enabled`."
+ },
+ "resetTimeoutMs": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `health.circuitBreaker.enabled`."
+ },
+ "halfOpenMaxRequests": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `health.circuitBreaker.enabled`."
+ },
+ "monitoringWindowMs": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `health.circuitBreaker.enabled`. Renamed from `monitoringWindow` by the protocol-18 conversion (#15680/#14478) so the unit lives in the key name — an honesty fix to a declaration that is still unread."
+ },
+ "monitoringWindow": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "REMOVED (#15680, ruling B on #14478) — tombstoned at the schema with `retiredKey`, which carries the prescription and makes authoring it a tsc error and a parse error; sources are renamed by the protocol-18 conversion `connector-duration-unit-suffixes`. The row stays because `retiredKey` keeps the key in the walked shape (the `rls.priority` precedent). Use `monitoringWindowMs`; the value (milliseconds) and the 60000 default are unchanged. The tombstone is packages/spec/src/integration/connector.zod.ts#CircuitBreakerConfigSchema."
+ },
+ "fallbackStrategy": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "Dead for the one reason recorded on `health.circuitBreaker.enabled`. `cache` / `default_value` / `error` / `queue` name four behaviours none of which is implemented anywhere — the shape this ledger exists to find."
+ }
+ }
+ }
+ }
+ },
+ "metadata": {
+ "status": "dead",
+ "verifiedAt": "2026-09-17",
+ "note": "'Custom connector metadata' — an open `z.record(z.string(), z.unknown())` bag that is stored with the entry and read by nothing: it is not in the `getConnectorDescriptors` projection, so it does not reach `GET /api/v1/automation/connectors`, and no provider factory, handler or lint rule consults it. ⛔ NOT an ADR-0049 retirement candidate: an untyped extension bag is a legitimate escape hatch whose whole contract is that the platform does not interpret it, so 'no consumer' is its specification rather than a defect. Recorded so the next sweep does not have to re-decide that. Not a container for this walk (a record of `unknown` has no child shape)."
+ }
+ }
+}
diff --git a/packages/spec/liveness/state-counts.md b/packages/spec/liveness/state-counts.md
index 2657aa59118..e731f55b49d 100644
--- a/packages/spec/liveness/state-counts.md
+++ b/packages/spec/liveness/state-counts.md
@@ -64,4 +64,6 @@ for both corollaries.
| `route_generation` | 0 | 0 | 0 | 4 | 0 | 4 |
| `realtime_subscription` | 0 | 0 | 0 | 6 | 0 | 6 |
| `sharing_rule` | 16 | 0 | 0 | 0 | 1 | 17 |
-| **total** | **878** | **5** | **1** | **96** | **11** | **991** |
+| `connector` | 20 | 0 | 0 | 53 | 1 | 74 |
+| `analytics_cube` | 17 | 0 | 0 | 12 | 0 | 29 |
+| **total** | **915** | **5** | **1** | **161** | **12** | **1094** |
diff --git a/packages/spec/scripts/liveness/check-liveness.mts b/packages/spec/scripts/liveness/check-liveness.mts
index baa1f1b9a47..5e6cf3542a5 100644
--- a/packages/spec/scripts/liveness/check-liveness.mts
+++ b/packages/spec/scripts/liveness/check-liveness.mts
@@ -263,7 +263,7 @@ const ledgerRoot = ledgerRootArg
// Governed metadata types, rolled out highest-frequency / highest-risk first.
// (`query` is not a metadata type — see SPEC_ONLY_SCHEMAS below.)
-const GOVERNED = ['object', 'field', 'flow', 'action', 'hook', 'permission', 'position', 'agent', 'tool', 'skill', 'dataset', 'page', 'view', 'report', 'dashboard', 'webhook', 'query', 'datasource', 'app', 'book', 'doc', 'email_template', 'job', 'mapping', 'seed', 'translation', 'validation', 'api', 'capability', 'qa', 'manifest', 'crud_endpoints', 'metadata_endpoints', 'batch_endpoints', 'route_generation', 'realtime_subscription', 'sharing_rule'];
+const GOVERNED = ['object', 'field', 'flow', 'action', 'hook', 'permission', 'position', 'agent', 'tool', 'skill', 'dataset', 'page', 'view', 'report', 'dashboard', 'webhook', 'query', 'datasource', 'app', 'book', 'doc', 'email_template', 'job', 'mapping', 'seed', 'translation', 'validation', 'api', 'capability', 'qa', 'manifest', 'crud_endpoints', 'metadata_endpoints', 'batch_endpoints', 'route_generation', 'realtime_subscription', 'sharing_rule', 'connector', 'analytics_cube'];
// Authorable metadata types that are NOT yet governed — the coverage ratchet.
//
@@ -301,25 +301,20 @@ const GOVERNED = ['object', 'field', 'flow', 'action', 'hook', 'permission', 'po
// ⛔ These rows are NOT a licence to leave them: the direction of travel is out
// of this map, exactly as it was for the nine.
//
-// [#18582] One of the three is PAID: `sharing_rule` moved into GOVERNED with
-// packages/spec/liveness/sharing_rule.json. The two left are `connector` and
-// `analytics_cube`, and the card that owes them is still open — the sentence
-// above is the whole reason this note updates the count instead of leaving a
-// stale "three" standing over a map of two.
-const PENDING_GOVERNANCE: Record = {
- connector:
- 'Authored via `stack.connectors[]` and `PUT /api/v1/meta/connector/:name` '
- + '(#6245 bound DeclarativeConnectorEntrySchema for exactly those two doors), and '
- + 'enrolled into this gate\'s denominator by #18133 — before which it was invisible '
- + 'to the ratchet rather than covered by it. No ledger walked it yet: seed '
- + 'packages/spec/liveness/connector.json (#18133).',
- analytics_cube:
- 'Authored via `stack.analyticsCubes[]` and `PUT /api/v1/meta/analytics_cube/:name` '
- + '(#10194). Whether that authoring is live end-to-end is its own measurement '
- + '(#10238) and this row does not prejudge it — the debt recorded here is only that '
- + 'no ledger asks who reads the cube\'s properties. Enrolled by #18133; seed '
- + 'packages/spec/liveness/analytics_cube.json.',
-};
+// [#18582] ALL THREE ARE PAID and this map is now EMPTY — its intended steady
+// state, reached in two rounds: `sharing_rule` first (PR #18587), then
+// `connector` and `analytics_cube` together, each with a ledger under
+// packages/spec/liveness/. Kept rather than deleted, for the reason the ratchet
+// paragraph above gives: the map is how the NEXT authorable type that arrives
+// ungoverned gets a declared debt instead of silence, and `report.stalePending`
+// already fails a row that outlives its debt, so an empty map costs nothing and
+// removing it would take the channel with it. ⛔ An entry added here is a debt
+// with an issue number and a direction of travel, never a way to silence the
+// gate on a type somebody just registered — read the #18133 note above before
+// writing one, and note that "the map is empty" is a fact about the ratchet, not
+// a claim that every governed type is fully `live`: what each ledger measured is
+// in the ledger.
+const PENDING_GOVERNANCE: Record = {};
// Spec-only override: governed types whose canonical schema is NOT (yet) in the
// metadata-type registry, so they can't be resolved via getMetadataTypeSchema.
diff --git a/packages/spec/scripts/liveness/undrilled-containers.baseline.json b/packages/spec/scripts/liveness/undrilled-containers.baseline.json
index cb581b1e331..bf9903c99ac 100644
--- a/packages/spec/scripts/liveness/undrilled-containers.baseline.json
+++ b/packages/spec/scripts/liveness/undrilled-containers.baseline.json
@@ -1,6 +1,6 @@
{
"_note": "SHRINK-ONLY RATCHET (#4956). A liveness-ledger entry on a CONTAINER property carries ONE blanket verdict for the whole subtree beneath it. That is a legal granularity — inventing per-key verdicts without evidence would be worse — but it must be DECLARED, because silence is indistinguishable from having looked. `dashboard.widgets` asserted in prose that its widget keys were 'classified in the DashboardWidgetSchema subtree', no such subtree had ever existed, and the gate had no way to disagree — so `widgets[].responsive` rode straight through the #3896 inert-key sweep that removed both its sibling `widgets[].performance` and its literal namesake `view.responsive` (#4956; retired four days late in #4876 / PR #4995).",
- "_containers": "`containers`: the child keys under these coordinates are classified NOWHERE — not in this ledger, not in another file, nowhere. Each row is a recorded, countable gap and a candidate for drilling. The gate prints the total on every run; `check:liveness --undrilled` prints the worklist. A row leaves by being DRILLED (add `children` with a status + evidence per key, the way `view.list` / `view.form` are drilled), never by being deleted for convenience — a row whose container has since been drilled FAILS the gate, so the debt cannot be overstated either. Adding a row is legitimate only when you have no per-key evidence to record, and it is deliberately a visible edit to a file named for the debt, because the alternative it replaced (a reassuring sentence in a `note`) cost nothing to write and could not be checked. ⚠️ DEPTH-TWO MIGRATION (#17424): 51 rows arrived at once when the walk learned to recurse. They are not new debt — every one of them was already riding on a blanket verdict, below a DRILLED container, where the one-level walk could not see it and therefore never counted it. Reading the jump as a regression is the wrong reading: the instrument's denominator grew to match the surface it always covered. Each of these rows is a `/.` coordinate, and each leaves the same way any other does — by being drilled, deferred, or by its property going away.",
+ "_containers": "`containers`: the child keys under these coordinates are classified NOWHERE — not in this ledger, not in another file, nowhere. Each row is a recorded, countable gap and a candidate for drilling. The gate prints the total on every run; `check:liveness --undrilled` prints the worklist. A row leaves by being DRILLED (add `children` with a status + evidence per key, the way `view.list` / `view.form` are drilled), never by being deleted for convenience — a row whose container has since been drilled FAILS the gate, so the debt cannot be overstated either. Adding a row is legitimate only when you have no per-key evidence to record, and it is deliberately a visible edit to a file named for the debt, because the alternative it replaced (a reassuring sentence in a `note`) cost nothing to write and could not be checked. ⚠️ DEPTH-TWO MIGRATION (#17424): 51 rows arrived at once when the walk learned to recurse. They are not new debt — every one of them was already riding on a blanket verdict, below a DRILLED container, where the one-level walk could not see it and therefore never counted it. Reading the jump as a regression is the wrong reading: the instrument's denominator grew to match the surface it always covered. Each of these rows is a `/.` coordinate, and each leaves the same way any other does — by being drilled, deferred, or by its property going away. [#18582] One row arrived with the `connector` ledger: `connector/webhooks`. It is a container whose blanket verdict is `dead` — a connector's nested webhook array never becomes a `webhook` metadata item, so the `sys_webhook` materializer never sees it (the ledger row carries the census). It is RECORDED rather than deferred because `WebhookConfigSchema` is `WebhookSchema.extend({ events, signatureAlgorithm })`, so the gate's key-set EQUALITY check would correctly refuse a `deferred` row pointing at the governed `webhook` type; and rather than drilled because 8 of its 21 child keys are the ADR-0010 protection envelope this gate auto-classifies `live` everywhere else, and the other 13 would each repeat one sentence — fabricated granularity over a container that is dead as a whole.",
"_deferred": "`deferred`: containers whose subtree IS classified elsewhere — and the reference is RESOLVED, not believed. This is the same claim that caused #4956, which is exactly why it may only be made as DATA: the gate requires the target to exist (a governed type root, or a drilled `type/prop` coordinate) and its classified key set to EQUAL the container's child key set. A dangling target fails; so does a drifted one — equality rather than subset, because a container that grows a key its target never classifies is #4956 reappearing one level down. Without this list the file's own header would have been a false claim: these six cover 248 child keys, nearly half the population. A `to` target may itself be a dotted coordinate (`dashboard/widgets.chartConfig`) since the walk recurses; the resolution rule is unchanged.",
"_excluded": "ADR-0010 framework overlay fields (`protection`, `_lock*`, `_provenance` — auto-classified live, they never consult the ledger) and container properties with NO ledger row at all (already reported UNCLASSIFIED by the forward pass).",
"_issue": "https://github.com/objectstack-ai/objectstack/issues/4956",
@@ -70,6 +70,7 @@
"app/branding",
"app/contextSelectors.optionsSource",
"book/groups.pages",
+ "connector/webhooks",
"dashboard/dateRange",
"dashboard/globalFilters",
"dashboard/header",