diff --git a/.changeset/20287-connector-actions-and-app-areas-live.md b/.changeset/20287-connector-actions-and-app-areas-live.md new file mode 100644 index 00000000000..089b6c246ab --- /dev/null +++ b/.changeset/20287-connector-actions-and-app-areas-live.md @@ -0,0 +1,13 @@ +--- +"@objectstack/spec": patch +--- + +Liveness ledger: `connector.actions.description`, `connector.actions.outputSchema` and `app.areas.description` are now `live`, not `dead`. Studio reads each of them at the `.objectui-sha` pin, and each row cites that reader and its producer. Ledger data, two README Notes cells and the regenerated count shards only. ⛔ No schema, parse, `.describe()` or accept-set change. + +The ledgers ship inside this package (`files[]` includes `liveness`), and `@objectstack/lint` reads them to decide which authored keys draw an advisory warning. None of the three rows sets `authorWarn`, so the set of warnings does not change. + +- `connector.actions.description`: the flow designer's Action picker on a `connector_action` node shows each action's description beside its label. +- `connector.actions.outputSchema`: the flow designer offers a `connector_action` node's downstream references from the top-level `properties` of its action's `outputSchema`. +- `app.areas.description`: the Studio app preview lists each area, with its description beneath it when one is authored. +- Both connector rows are fed from the plugin and provider door, as their sibling `actions.*` rows are: the `actions` an author writes on a metadata connector entry never reach the registry the designer reads. +- The regenerated count shards: `connector` has 31 live and 23 dead (was 29 and 25), and `app` has 50 live and 8 dead (was 49 and 9). diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 28406e1b10f..661ad73fe0c 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -924,7 +924,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | query | **not a metadata type** — the REQUEST surface (`QuerySchema`: client SDK QueryBuilder output; the `POST /data/:object/query` body), governed via `SPEC_ONLY_SCHEMAS` (#4286). The gate resolves 1 experimental at the depth this ledger drills; the 7 marker-experimental search affordances sit one level deeper, below what this ledger declares (the walk recurses since #17424, but only where a `children` map is written, and none is written here) — resolved from `[EXPERIMENTAL — not enforced]` describe markers, not ledger entries (search `fuzzy`/`operator`/`boost`/`minScore`/`language`/`highlight` + `aggregations[].filter` — declared engine affordances no executor receives). The #4286 sweep closed out same-release: `having` ENFORCED 2026-07-31 (engine-side post-aggregation filter, both paths; was finding 1); dead 4 = the tombstoned removals `joins`/`windowFunctions`/`cursor`/`distinct` — REMOVED 2026-07-31 (retiredKey keeps each in the walked shape so the rows stay; protocol-17 semantic migrations; the JoinNode + WindowFunctionNode clusters and the `QueryBuilder.cursor()`/`.distinct()` producers deleted with their keys; `distinct`'s mis-wired REST count suppression deleted too — finding 2). **#6815** adds the 5th dead: `aggregations[].distinct` REMOVED 2026-08-09 (live → dead, `-1` live). It is the one member of this ledger the #4286 sweep could not have caught with the question it asked — that sweep looked for keys NO executor reads, and this one had a reader: the objectql in-memory fallback deduplicated before applying the function while all five other faces (driver-sql, driver-turso, driver-mongodb, driver-memory, service-analytics' `AGGREGATE_SQL`) ignored it, so one query answered two plausible NUMBERS depending on which backend served it. The lesson for the next audit is the question, not the key: a per-key `live` verdict is only as good as the count of faces it was measured across, and this row's 2026-07-31 evidence (`in-memory-aggregation.ts:167,204-206`) was TRUE and still the wrong verdict. `count_distinct` is the surviving spelling (enforce leg, #6409) | | datasource | seeded 2026-08-01 (#4487) — the **highest dead ratio of any governed type** (20 of 43), and it was ungoverned until now, which is not a coincidence: #4410/#4465/#4481 found six inert keys here by hand, two security-shaped (`schemaMode` left an external DB constructible as `managed` with DDL ungated; `ssl` configured nothing while looking configured). Dead set = `capabilities.*` (all 11 — the engine gates pushdown on the runtime driver's `supports.*` object, a non-overlapping vocabulary), `healthCheck.*` (3 — nothing schedules a datasource probe; the 20 `healthCheck` hits in the repo all belong to the PLUGIN health monitor and other surfaces), `retryPolicy.*` (4 — `retryPolicy` IS enforced on `hook` and `job`, which is what makes this one read alive; the shapes differ), `external.label`, `external.requirePermission`. **`capabilities.readOnly` is the one to know**: it reads as a safety switch, gates nothing, and two shipped prescriptions pointed authors at it until #4487 — `external.allowWrites: false` is the enforced write gate. `config` is a `z.record`, so its per-driver keys sit outside the walk (recorded in the entry's note, not silently skipped) **批 A CLOSED 2026-08-02 (#4583)**: the `capabilities` block — 11 flags, every one dead and authorWarn'd — was REMOVED rather than bridged; pushdown comes from the runtime driver's own `supports.*`, so there was nothing to connect it to. Its rows are deleted (strict-removal route), which is why dead falls 20 → 9. `readOnly` was the reason the audit was worth doing: it read as a safety switch, gated nothing, and had already been MOVED twice toward somewhere it might be enforced (#4410, #4465) — the shipped CRM example called a datasource a read replica on the strength of it while the datasource took writes. Removing it does NOT hand the author a working alternative: `external.allowWrites` only gates FEDERATED datasources, so a managed one has no read-only gate at all (#4584). Remaining 9 = healthCheck ×3 + retryPolicy ×4 + external ×2, batches B/C/D of #4583 **BATCHES B/C/D CLOSED 2026-08-02 — datasource now has ZERO dead properties**, down from the 20 it was seeded with (the highest dead ratio of any governed type). `retryPolicy` ×4 and `healthCheck` ×3 went as whole blocks, `external.label` / `external.requirePermission` as keys. None was bridgeable: each already had a different LIVE mechanism doing the job — the boot policy, the driver handle's on-demand `ping()`/`checkHealth()`, the top-level `label`, and ordinary permission sets + RLS. The `retryPolicy` rejection deliberately refuses to offer a rename: `hook`/`job` retryPolicy ARE enforced but spell the delay `backoffMs`, and that inconsistency is itself the evidence nothing read the datasource one (#4488's sharpest trap) | | webhook | **not a registered metadata type** — governed via the gate's spec-only schema override (`SPEC_ONLY_SCHEMAS`), not `getMetadataTypeSchema`; folding it onto the registry is the #3490 reassessment. This row once read 0/1/16 ("the ENTIRE authoring surface is dead", #3461) and both halves of that were CLOSED same-quarter: #3489 built the materializer bridge (authored `webhooks:` entries now land as `sys_webhook` dispatcher rows) and #3494 pruned the aspirational props outright — so the surviving surface is fully live. Kept in the table as the worked example that a dead verdict is a worklist entry, not a tombstone: enforce-or-remove resolved this one by ENFORCING | -| app | seeded 2026-08-01 (#4488). Dead 9 = the seven #4142 `retiredKey` tombstones (version/aria/objects/apis/sharing/embed/mobileNavigation — rows stay while the tombstones hold the keys in the walked shape) + `homePageId` (#4667 tombstone — the landing IS the first nav item; root landing follows `isDefault` routing) + `areas.description` (benign, docs-shaped, kept and not warned). RETIRED 17.0.0 (#4509, rows deleted — the selector schema is strict): selector `includeAll` (deliberately DISOBEYED, not merely unread — selectors are mandatory-scope and an "All" row would clear the scope, leaking system metadata through Studio's package filter; STUDIO_APP authored it against a renderer that ignored it) and `placement` (no renderer read it; "topbar" placed nothing). Nav walk covers the union's `object` variant; other variants hand-verified live, and the `actionDef` dispatch gap closed in #4509 **#4651**: the **fail-open area gates** `areas.visible` / `areas.requiredPermissions` — this ledger's most important app finding — are REMOVED, rows DELETED (strict removal; retained rows would report ORPHAN). They were not merely unread: `filterAppForUser` never reads `item.areas` at all and the shell renders every area, so a "hidden" or permission-gated area was served to everyone, while the identically named per-ITEM and per-APP keys ARE enforced. Route B (remove) over route A (enforce): enforcing needs semantics decided first (does filtering an area remove its items everywhere? does the server bind `user` for area CEL?), which the 17.0.0 window could not hold. Boundary unchanged and still recorded on `areas.navigation`: per-item gating inside an area is shell-side only. **#4667**: `homePageId` TOMBSTONED (row stays — retiredKey keeps it in the walked shape) and `areas.order` row DELETED (strict removal); `areas.order` read alive because the per-ITEM `order` really is sorted (NavigationRenderer.tsx:1154) while no renderer ever sorted areas. **The dead nine above still reconcile exactly** — nothing has left or joined that set since #4667; what moved after it are two additions on the other side of the ledger. **#4829** (PR #6942) declares `_unpublished`, `live`: a MACHINE-MANAGED publish gate, never authored, written by the AI additive-materialization path and cleared by `POST /packages/:id/publish-drafts`, and server-enforced in `filterAppForUser`. It is declared on AppSchema rather than omitted because the write path validates against that schema, so the flip itself would otherwise be unwritable — a `live` row for a key no author may set, which is the inverse of every dead row here. **#4848** (PR #7253) gave this type its **first `planned`**: `navigation.runAction`, the contract-first half of the SDUI deep-link promotion. `planned` and not `dead` was load-bearing, the same distinction `api`'s two mapping transforms draw — the reference IS validated at authoring (framework `validateCrossReferences`, the lint's nav `runAction` arm), and it carried `authorWarn` saying the shell did not read the declared slot yet, while auto-run fired only via the transitional `?runAction=` query param. **#10068 FLIPS it `live` 2026-08-20** (objectui#5216 via objectui PR #5354, absorbed by the `.objectui-sha` pin `9a3daf8`, which postdates that merge — both pointers were read AT THE PIN this repo builds against). It is this table's clearest worked example of **why one pointer is not a call graph**: the work-list anticipated a single `NavigationRenderer` pointer, and that would have overstated one half and understated the other. The renderer is the **`producer`** (`NAV_RUN_ACTION_PARAM`, the wire name's one definition, and `withRunAction` applied in `resolveHref`'s object branch — list landings only, never the `recordId` branch): it WRITES the deep link and runs nothing. What makes authoring the key change runtime behaviour is the **`evidence`** side, `app-shell`'s `useNavRunAction` read-once/consume-once hook, wired at every object list plus the entitlement-gated environment toolbar — so a renderer-only pointer would have said the slot is live because something *emits* it. Read it beside `seed.env` in the `producer` section above: same shape, opposite outcome, because here the second input really is supplied. ⚠️ It also records an **enforcement ≠ consumption** measurement worth carrying: the published `@objectstack/spec@17.0.0` does not enforce the `runAction` × `recordId` exclusivity (the `objectNavTargetExclusivity` superRefine is on this repo's `main` but outside the GA build) and accepts `runAction: ''` — the merged-but-unpublished window, not a defect — which makes objectui's list-surface-only precedence load-bearing rather than defensive. **merged upstream ≠ published ≠ pinned downstream**, and unlike a missing key a missing *refinement* fails silent (objectui#5328). | +| app | seeded 2026-08-01 (#4488). Dead 8 = the seven #4142 `retiredKey` tombstones (version/aria/objects/apis/sharing/embed/mobileNavigation — rows stay while the tombstones hold the keys in the walked shape) + `homePageId` (#4667 tombstone — the landing IS the first nav item; root landing follows `isDefault` routing). `areas.description` left that set on 2026-09-30 (#20299): the Studio app preview draws it (objectui#11027). RETIRED 17.0.0 (#4509, rows deleted — the selector schema is strict): selector `includeAll` (deliberately DISOBEYED, not merely unread — selectors are mandatory-scope and an "All" row would clear the scope, leaking system metadata through Studio's package filter; STUDIO_APP authored it against a renderer that ignored it) and `placement` (no renderer read it; "topbar" placed nothing). Nav walk covers the union's `object` variant; other variants hand-verified live, and the `actionDef` dispatch gap closed in #4509 **#4651**: the **fail-open area gates** `areas.visible` / `areas.requiredPermissions` — this ledger's most important app finding — are REMOVED, rows DELETED (strict removal; retained rows would report ORPHAN). They were not merely unread: `filterAppForUser` never reads `item.areas` at all and the shell renders every area, so a "hidden" or permission-gated area was served to everyone, while the identically named per-ITEM and per-APP keys ARE enforced. Route B (remove) over route A (enforce): enforcing needs semantics decided first (does filtering an area remove its items everywhere? does the server bind `user` for area CEL?), which the 17.0.0 window could not hold. Boundary unchanged and still recorded on `areas.navigation`: per-item gating inside an area is shell-side only. **#4667**: `homePageId` TOMBSTONED (row stays — retiredKey keeps it in the walked shape) and `areas.order` row DELETED (strict removal); `areas.order` read alive because the per-ITEM `order` really is sorted (NavigationRenderer.tsx:1154) while no renderer ever sorted areas. **The dead eight above reconcile exactly** — only `areas.description` has left that set since #4667, and nothing has joined it; what moved after it are two additions on the other side of the ledger. **#4829** (PR #6942) declares `_unpublished`, `live`: a MACHINE-MANAGED publish gate, never authored, written by the AI additive-materialization path and cleared by `POST /packages/:id/publish-drafts`, and server-enforced in `filterAppForUser`. It is declared on AppSchema rather than omitted because the write path validates against that schema, so the flip itself would otherwise be unwritable — a `live` row for a key no author may set, which is the inverse of every dead row here. **#4848** (PR #7253) gave this type its **first `planned`**: `navigation.runAction`, the contract-first half of the SDUI deep-link promotion. `planned` and not `dead` was load-bearing, the same distinction `api`'s two mapping transforms draw — the reference IS validated at authoring (framework `validateCrossReferences`, the lint's nav `runAction` arm), and it carried `authorWarn` saying the shell did not read the declared slot yet, while auto-run fired only via the transitional `?runAction=` query param. **#10068 FLIPS it `live` 2026-08-20** (objectui#5216 via objectui PR #5354, absorbed by the `.objectui-sha` pin `9a3daf8`, which postdates that merge — both pointers were read AT THE PIN this repo builds against). It is this table's clearest worked example of **why one pointer is not a call graph**: the work-list anticipated a single `NavigationRenderer` pointer, and that would have overstated one half and understated the other. The renderer is the **`producer`** (`NAV_RUN_ACTION_PARAM`, the wire name's one definition, and `withRunAction` applied in `resolveHref`'s object branch — list landings only, never the `recordId` branch): it WRITES the deep link and runs nothing. What makes authoring the key change runtime behaviour is the **`evidence`** side, `app-shell`'s `useNavRunAction` read-once/consume-once hook, wired at every object list plus the entitlement-gated environment toolbar — so a renderer-only pointer would have said the slot is live because something *emits* it. Read it beside `seed.env` in the `producer` section above: same shape, opposite outcome, because here the second input really is supplied. ⚠️ It also records an **enforcement ≠ consumption** measurement worth carrying: the published `@objectstack/spec@17.0.0` does not enforce the `runAction` × `recordId` exclusivity (the `objectNavTargetExclusivity` superRefine is on this repo's `main` but outside the GA build) and accepts `runAction: ''` — the merged-but-unpublished window, not a defect — which makes objectui's list-surface-only precedence load-bearing rather than defensive. **merged upstream ≠ published ≠ pinned downstream**, and unlike a missing key a missing *refinement* fails silent (objectui#5328). | | book | seeded 2026-08-01 (#4488). ADR-0046 §6 spine; `audience` is ENFORCED and fail-closed (tree 401/403 + per-doc effective-audience union on both list and tree). Dead 2 = BOTH inline `translations` maps (book-level and per-group): no resolver reads them and the bundle translator doesn't cover `book` — the trap is that `doc.translations` two files over works on every read path. Also recorded: the `include: { tag }` rule variant is live — `tags` was DECLARED on DocSchema in 17.0.0 (#4509, ADR-0049); see the `doc` row below for the fix's full history. **#4667**: both inline translation maps retired — book-level row DELETED (BookSchema is strictObject), group-level row KEPT as a tombstone (BookGroupSchema is a plain z.object with no .strict(), so a bare delete would have zod silently strip it). No resolver read either; the trap was proximity to `doc.translations`, which is live on every doc render path. | | doc | seeded 2026-08-01 (#4488). Fully live: the kernel stores `content` unparsed, but the REST read layer localizes (resolveDocLocale), audience-gates, list-strips `content`, and the book resolver consumes name/label/description/order/group — plus the objectui console portal renders it all. The schema's own "docs are inert data" header describes the kernel, not the type. **`tags` DECLARED in 17.0.0 (#4509)** — the enforce half of enforce-or-remove: the book resolver's `include: { tag }` matcher, the REST transport and `ResolverDoc.tags` all already existed, but DocSchema is strict and had no `tags` key, so authoring one was a parse error and the variant could never match. Live on arrival | | email_template | this row read 8/–/13/– for one day (seeded 2026-08-01, #4488: "every authorable property is dead", the webhook shape on AUTH mail) and #4509 CLOSED it by ENFORCING — the second worked example, after `webhook`, that a dead verdict is a worklist entry rather than a tombstone. `bootstrapDeclaredEmailTemplates` materializes declared items into the `sys_email_template` rows `sendTemplate` reads, sharing `mapTemplateToRow` with the built-in seeder so the two doors cannot drift, and re-materializes on live metadata writes (`email_template` is `allowRuntimeCreate: true`, so boot-only would have left Studio saves inert). Three breaks had to close, not one: the engine never registered `emailTemplates:` into the registry, built-in seeds masqueraded as `managed_by: admin` and outranked declared templates, and nothing materialized. ADR-0054 proof bound on `subject` (`email-template-materialization`) | @@ -944,7 +944,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | rest_api | seeded 2026-09-21 (#14640) — the FIFTH `RestServerConfig` sub-object, enrolled a round after the four above and deliberately so. #14369 left `RestApiConfigSchema` out because the `api` block's consumption seam was then still VALIDATE-ONLY (#11637 ran the declared contract and discarded its output), so a census would have recorded a half that was about to move; the gate source and four rows of this table said as much. That fence was re-tested before a line of this ledger was written and it has EXPIRED: `RestServer.normalizeConfig` now BUILDS the `api` block from `parseDeclaredApiConfig`'s output — “the asymmetry is gone and all five now build from their parsed output” — and the change is RELEASED, not in flight, with `packages/rest/CHANGELOG.md` re-stating the same zero this file records. ⛔ **The ledger is `rest_api.json`, NOT `api.json`**: that name was already taken by `ApiEndpointSchema`, the registered `api` metadata type with real consumers in the matcher, executor, policy chain and mapping layer — one spelling, two unrelated meanings inside `packages/spec`, and filing here would have published one file's measurement under the other's name. Live 20 = `version` / `basePath` / `apiPath`, which `getApiBasePath` splices into the prefix of EVERY mounted route (read through a whole-block destructure, which is why the dead-key census below had to sweep destructuring shapes and not a property-access pattern alone), the eight `enable*` switches, each gating a mount and most of them also the discovery document's capability block, and `projectResolution` — plus, since #20294 (ENFORCED 2026-09-28, ruling B on #20359), the eight identity members of `documentation` (`title`, `description`, `termsOfService`, `contact.name` / `url` / `email`, `license.name` / `url`), which `RestServer.overlayDocumentationInfo` lays over the served OpenAPI `info` on both `/openapi.json` doors (`contact` / `license` replaced whole; nothing authored serves the artifact's `info` unchanged). Dead 4 = the `requireAuth` tombstone (#3963, still `.omit()`ed by this seam because #3963 chose warn-and-ignore and converting that to a boot failure is that decision's to make), the `responseFormat` and `documentation.enabled` tombstones (RETIRED 2026-09-27, #20295, ADR-0049 enforce-or-remove — refused at `RestServer` construction with their prescription; `responseFormat` retired whole, so its three child rows collapsed into one), and the `documentation.version` tombstone (RETIRED 2026-09-28, #20294 — the served `info.version` is the protocol version, #11646; an app's own release number goes into `description`). Until #20294 the nine non-`enabled` members of `documentation` (drilled, including its nested `contact` / `license`) were all dead too — normalized into `this.config.api` and read back by nothing, so `documentation.title` retitled no served document. Every zero carries a lit control on the same instrument (twelve sibling keys on the same block return 1-2 reads), each of the three shapes a spelling sweep is blind to was swept with its own control, and the backstop is structural rather than textual: `NormalizedRestServerConfig` is module-local with no `export` and `RestServer.config` is `private`, so the normalized block cannot be reached from outside that one class. ⛔ **The two dead containers do NOT share one verdict**: `documentation`'s members are OpenAPI `info` fields whose enforce route collides with a recorded ownership decision (`info` is written by `build-openapi.ts` and was passed through untouched by #11646 — settled by ruling B on #20359, which split the block by field owner: identity overlaid, `version` retired), while `responseFormat`'s enforce route means making the response envelope configurable — a larger claim. This file records status; the enforce-or-remove call per key is a follow-up on the human floor — made for `responseFormat` and `documentation.enabled` (retired, #20295), and for `documentation`'s other members (#20294: the identity members enforced, `version` retired). `evidenceScope` stays `in-repo`: objectui was measured clean at the pinned sha and at head against a lit control, but the closed cloud runtime was not reachable from the measuring container, so #14796's structural reading is cited as a standing reading rather than re-claimed as a sweep | | 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`), as measured at `5f5511f0` before #20288 repointed the enum at the emitted `DataEventType` + `BulkDataEventType` names, 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`. ⚠️ The MECHANISM changed with the `connectionTimeoutMs` retirement and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def, and the key-set conclusion used to rest on that attachment. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child, and what preserves the walked shape is the pipe's read-through `shape`, NOT a `superRefine` attachment. The CONCLUSION is unchanged and re-measured on the built entry rather than inherited: both carriers expose 30 keys and the key sets are byte-identical, with no entry-only and no base-only key. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the two rows where they are the whole verdict (`authentication` and `actions`; `triggers` was the third until its retirement made it a tombstone both carriers refuse). **ONE SCHEMA, TWO DOORS** is the shape fact behind the 29/1/25 split (live/planned/dead; counts read from the generated `state-counts/connector.md` shard, never hand-kept here): 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 author-supplied `ConnectorProviderContext` fields plus `provider` and `enabled` — `name` is itself one of those fields (the former "plus `name`" tail double-counted it), `loadPackageFile` is host-injected rather than authored, and `provider` selects the factory without ever reaching the context; `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`, and ⛔ NOT "refused outright" — the former tail here said exactly that and all three instruments contradict it, including the one it cites: the KEY is ACCEPTED (`connector.zod.ts` declares `authentication: ConnectorAuthConfigSchema.optional().default({ type: 'none' })`, and the accepted value does nothing); what #7990 refuses is a non-`none` VALUE (`if (entry.authentication && entry.authentication.type !== 'none')`, whose own message prescribes "drop `authentication` (or set `{ type: 'none' }`)"); and ADR-0097 §3, titled "Credentials are references", rejects **inline secrets** in stack metadata, not the key. Accepted-and-ignored, plus a loud refusal of every value but `{ type: 'none' }`, is exactly the basis of the `planned` verdict — which the row itself already stated ("the accepted value does nothing"), so the summary, not the row, was the wrong half. The 25 `dead`, re-measured at this head and partitioned so every row is counted exactly once: two declared subsystems with no engine — `syncConfig` (8), `fieldMappings` (7) — plus `metadata`, `actions.description`/`.outputSchema`, and the seven top-level `retiredKey` tombstones `rateLimitConfig`, `errorMapping`, `connectionTimeoutMs`, `health`, `status`, `webhooks` and `triggers`. That sums to 25, the dead count the generated `state-counts/connector.md` shard carries. ⚠️ It was 30 until the connector `triggers` array was retired (ADR-0049; ADR-0041 unchanged): `triggers` counted 6 drilled rows (`key`, `label`, `description`, `type`, `intervalSeconds` and the `interval` rename tombstone — dead because nothing read a connector trigger, which the schema's own docblock said: #3197) and is now ONE leaf tombstone row, by the same gate rule as `health` below. ⚠️ It was 44 until the connector resilience family was retired (ADR-0049): `health` counted 15 drilled rows (both sub-blocks plus the `monitoringWindow` tombstone) and is now ONE leaf tombstone row — the gate refuses `children` under a property that is no longer a container — and `webhooks` left the undrilled baseline for the same reason; `status` and `webhooks` stayed one row each and changed only from dead-awaiting-a-decision to dead-and-tombstoned. ⚠️ `retryConfig` IS NO LONGER IN THIS LIST: all eight of its sub-keys went `live` when #18975 made the declared policy execute at the one platform fetch site, which is the same measurement the falsification note at the end of this row records — so a reader who still finds "`retryConfig` (8)" among the dead is reading a stale copy. ⚠️ Nor is it "the two timeouts" any more: `requestTimeoutMs` is `live` (it becomes `resilientFetch`'s per-attempt deadline) and `connectionTimeoutMs` is the retired tombstone named above. ⭐ EIGHT rows in this ledger are `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent) — `rateLimitConfig`, `errorMapping`, `connectionTimeoutMs`, `health`, `status`, `webhooks`, `triggers` and `fieldMappings.transform` — but ⛔ that eight is NOT a separate addend: the first seven ARE the top-level tombstones counted above and the last one is already inside the `fieldMappings` count, which is exactly the double-count that made the previous "and four `retiredKey` tombstones" tail drift. (`triggers.interval` was one of the eight until its array left whole with `triggers`, whose own leaf row took its place — the count held at eight by a swap, not by standing still.) (`health.circuitBreaker.monitoringWindow` was the ninth until its block left whole with `health`.) Count them by name, never by adding the tail. **A prior in-repo claim is recorded here with its DIRECTION measured rather than remembered, because this row's job is the history of how the type got here**: the conversion registry's note inside `connector-rate-limit-config-removed`'s fixture reads "`retryConfig` and the timeouts beside it are untouched by THIS conversion — a statement about its scope, not a liveness verdict. They are not live: declared, defaulted and documented, and read by nothing." ⚠️ It asserts they are NOT live, and it scopes "untouched" to that one conversion. The former tail here quoted it as asserting the OPPOSITE ("they are live") and called it false when seeded — an inversion that turned this whole passage upside down, and it is corrected rather than carried. Measured direction: the note was TRUE when this ledger was seeded (2026-09-17) and is STALE now, #18975 having made the declared policy execute at the one platform fetch site (`connectorFetchOptions` → `resilientFetch`), so `retryConfig`'s eight sub-keys are `live` on their own rows and `requestTimeoutMs` is `live` beside them; only `connectionTimeoutMs` still answers to it, as the retired tombstone. ⛔ The stale comment is not rewritten from here — it is #19729's, as a dated note beside it — and it is not a line this PR's diff touches. ⚠️ The seeding note's supporting census — "the word does not occur outside `packages/spec` at all" — is FALSE at this head and is corrected rather than carried: `git grep -n retryConfig 14fdebd766 -- . ':!packages/spec'` returns 67 **matching lines** over 15 files — `git grep -o` on the same tree and pathspec returns 77 **occurrences**, and a line is not an occurrence, which is the trap a re-measurer falls into next (26 matching lines in the materializer `packages/services/service-automation/src/plugin.ts` and its materialization test, 22 across `connector-rest` and `connector-openapi` — providers, connectors and their tests — 13 in five `.changeset` fragments, and 6 on two `content/docs` pages). ⛔ Re-read that as the standing lesson of this row: a census is a count plus the tree it was taken against, and a bare "does not occur" with no commit behind it is the shape that rots first. The timeouts half is settled on its own rows: `requestTimeoutMs` is `live`, `connectionTimeoutMs` is retired | +| 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`. ⚠️ The MECHANISM changed with the `connectionTimeoutMs` retirement and the prior sentence here is corrected rather than carried: that schema USED TO BE `ConnectorSchema.superRefine(...)`, a Zod 4 check attached to the same object def, and the key-set conclusion used to rest on that attachment. It is now a `z.preprocess` PIPE — both published carriers wrap one shared private `ConnectorBaseSchema` in the ADR-0049 retired-default residue stage, the entry schema adding the ADR-0097 cross-field rules on the base before wrapping, so the two are SIBLINGS rather than parent and child, and what preserves the walked shape is the pipe's read-through `shape`, NOT a `superRefine` attachment. The CONCLUSION is unchanged and re-measured on the built entry rather than inherited: both carriers expose 30 keys and the key sets are byte-identical, with no entry-only and no base-only key. The gate cannot tell the two schemas apart; what the entry schema buys is REFUSALS, invisible to the walk and visible only in the two rows where they are the whole verdict (`authentication` and `actions`; `triggers` was the third until its retirement made it a tombstone both carriers refuse). **ONE SCHEMA, TWO DOORS** is the shape fact behind the 31/1/23 split (live/planned/dead; counts read from the generated `state-counts/connector.md` shard, never hand-kept here): 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 author-supplied `ConnectorProviderContext` fields plus `provider` and `enabled` — `name` is itself one of those fields (the former "plus `name`" tail double-counted it), `loadPackageFile` is host-injected rather than authored, and `provider` selects the factory without ever reaching the context; `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`, and ⛔ NOT "refused outright" — the former tail here said exactly that and all three instruments contradict it, including the one it cites: the KEY is ACCEPTED (`connector.zod.ts` declares `authentication: ConnectorAuthConfigSchema.optional().default({ type: 'none' })`, and the accepted value does nothing); what #7990 refuses is a non-`none` VALUE (`if (entry.authentication && entry.authentication.type !== 'none')`, whose own message prescribes "drop `authentication` (or set `{ type: 'none' }`)"); and ADR-0097 §3, titled "Credentials are references", rejects **inline secrets** in stack metadata, not the key. Accepted-and-ignored, plus a loud refusal of every value but `{ type: 'none' }`, is exactly the basis of the `planned` verdict — which the row itself already stated ("the accepted value does nothing"), so the summary, not the row, was the wrong half. The 23 `dead`, re-measured at this head and partitioned so every row is counted exactly once: two declared subsystems with no engine — `syncConfig` (8), `fieldMappings` (7) — plus `metadata` and the seven top-level `retiredKey` tombstones `rateLimitConfig`, `errorMapping`, `connectionTimeoutMs`, `health`, `status`, `webhooks` and `triggers`. That sums to 23, the dead count the generated `state-counts/connector.md` shard carries. ⚠️ It was 25 until `actions.description` and `actions.outputSchema` went `live` (#20287: the flow designer reads both, objectui#11028). ⚠️ It was 30 until the connector `triggers` array was retired (ADR-0049; ADR-0041 unchanged): `triggers` counted 6 drilled rows (`key`, `label`, `description`, `type`, `intervalSeconds` and the `interval` rename tombstone — dead because nothing read a connector trigger, which the schema's own docblock said: #3197) and is now ONE leaf tombstone row, by the same gate rule as `health` below. ⚠️ It was 44 until the connector resilience family was retired (ADR-0049): `health` counted 15 drilled rows (both sub-blocks plus the `monitoringWindow` tombstone) and is now ONE leaf tombstone row — the gate refuses `children` under a property that is no longer a container — and `webhooks` left the undrilled baseline for the same reason; `status` and `webhooks` stayed one row each and changed only from dead-awaiting-a-decision to dead-and-tombstoned. ⚠️ `retryConfig` IS NO LONGER IN THIS LIST: all eight of its sub-keys went `live` when #18975 made the declared policy execute at the one platform fetch site, which is the same measurement the falsification note at the end of this row records — so a reader who still finds "`retryConfig` (8)" among the dead is reading a stale copy. ⚠️ Nor is it "the two timeouts" any more: `requestTimeoutMs` is `live` (it becomes `resilientFetch`'s per-attempt deadline) and `connectionTimeoutMs` is the retired tombstone named above. ⭐ EIGHT rows in this ledger are `retiredKey` tombstones that keep their rows because the key stays in the walked shape (the `rls.priority` precedent) — `rateLimitConfig`, `errorMapping`, `connectionTimeoutMs`, `health`, `status`, `webhooks`, `triggers` and `fieldMappings.transform` — but ⛔ that eight is NOT a separate addend: the first seven ARE the top-level tombstones counted above and the last one is already inside the `fieldMappings` count, which is exactly the double-count that made the previous "and four `retiredKey` tombstones" tail drift. (`triggers.interval` was one of the eight until its array left whole with `triggers`, whose own leaf row took its place — the count held at eight by a swap, not by standing still.) (`health.circuitBreaker.monitoringWindow` was the ninth until its block left whole with `health`.) Count them by name, never by adding the tail. **A prior in-repo claim is recorded here with its DIRECTION measured rather than remembered, because this row's job is the history of how the type got here**: the conversion registry's note inside `connector-rate-limit-config-removed`'s fixture reads "`retryConfig` and the timeouts beside it are untouched by THIS conversion — a statement about its scope, not a liveness verdict. They are not live: declared, defaulted and documented, and read by nothing." ⚠️ It asserts they are NOT live, and it scopes "untouched" to that one conversion. The former tail here quoted it as asserting the OPPOSITE ("they are live") and called it false when seeded — an inversion that turned this whole passage upside down, and it is corrected rather than carried. Measured direction: the note was TRUE when this ledger was seeded (2026-09-17) and is STALE now, #18975 having made the declared policy execute at the one platform fetch site (`connectorFetchOptions` → `resilientFetch`), so `retryConfig`'s eight sub-keys are `live` on their own rows and `requestTimeoutMs` is `live` beside them; only `connectionTimeoutMs` still answers to it, as the retired tombstone. ⛔ The stale comment is not rewritten from here — it is #19729's, as a dated note beside it — and it is not a line this PR's diff touches. ⚠️ The seeding note's supporting census — "the word does not occur outside `packages/spec` at all" — is FALSE at this head and is corrected rather than carried: `git grep -n retryConfig 14fdebd766 -- . ':!packages/spec'` returns 67 **matching lines** over 15 files — `git grep -o` on the same tree and pathspec returns 77 **occurrences**, and a line is not an occurrence, which is the trap a re-measurer falls into next (26 matching lines in the materializer `packages/services/service-automation/src/plugin.ts` and its materialization test, 22 across `connector-rest` and `connector-openapi` — providers, connectors and their tests — 13 in five `.changeset` fragments, and 6 on two `content/docs` pages). ⛔ Re-read that as the standing lesson of this row: a census is a count plus the tree it was taken against, and a bare "does not occur" with no commit behind it is the shape that rots first. The timeouts half is settled on its own rows: `requestTimeoutMs` is `live`, `connectionTimeoutMs` is retired | | 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 kept `dimensions.granularities` (read only 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) `dead` until **#20282**'s second stage (2026-09-29) read both on the query doors off whichever cube answers the name: `analytics-service#withDeclaredMeasureFormats` describes each measure column's `fields[].format`, and `#withDeclaredGranularityDefaults` buckets a grouped time dimension at the default `dataset-executor#declaredDefaultGranularity` reads — the one reading (a single-entry list) the dataset path's `granularityOf` now shares. 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 3 `dead` are the `refreshKey` tombstone and the inner `name` on each of `measures`/`dimensions`, where the record KEY is the identity — RETIRED by #20300 (ADR-0049 enforce-or-remove) as `retiredKey()` tombstones on the member `strictObject`s, so those two rows STAY `dead` (the tombstone keeps the key in the walked shape) and the count does not move. It was 6 until **#20282**'s third stage (2026-09-29) published the three `description`s on discovery: `analytics-service#getMeta` copies each onto its `CubeMeta` entry, the read point the `title`/`label` rows already cite (display-shaped, the #7131 split). It was 7 until #20637 RETIRED `refreshKey` whole (ADR-0049 enforce-or-remove, maintainer letter C): the caching block's `every` and `sql` were two drilled `dead` rows — no refresh scheduler, pre-aggregation or analytics result cache exists anywhere, re-measured 2026-09-29 — and the `retiredKey()` tombstone that replaced the block is ONE leaf row, because the gate refuses `children` on a property that is no longer a container (the connector `health` precedent). **#20282** flips the tenth, the visibility flag `public`, `dead` → `live` 2026-09-27: seeded as a knob that was never wired (three internal mints wrote `false`, nothing read it), it is now read by `service-analytics`' `cube-visibility.ts#isCubePublic` — `getMeta` omits a hidden cube and `query()` / `generateSql()` refuse it — in the same change that moved its default from `false` to the Cube.dev `true`, since enforcing the old default would have hidden every authored cube. It was 12 until #18612 RETIRED `joins[].relationship` and the REQUIRED `joins[].sql` (ADR-0049 enforce-or-remove, maintainer-ruled batch #154): the ON clause is SYNTHESISED as an FK equality and the authored one was never consulted, so a declared join condition came back REPLACED under a 200. `CubeJoinSchema` is a `strictObject`, so the route was strict deletion plus a `guidance` prescription and the two rows left this ledger with the keys — not the `retiredKey()` route, which keeps the row. **#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 diff --git a/packages/spec/liveness/app.json b/packages/spec/liveness/app.json index d3fba17449c..083df78cb02 100644 --- a/packages/spec/liveness/app.json +++ b/packages/spec/liveness/app.json @@ -220,9 +220,12 @@ "note": "2026-09-27 (#20146): REPOINTED off objectui's `AppSidebar.tsx` — deprecated, never mounted (the console renders `UnifiedSidebar`), and removed from objectui main by objectui PR #10617 — to the mounted reader(s) above, each re-read at the `.objectui-sha` pin f8a9d0fb and unchanged at objectui main fb91ac9b0." }, "description": { - "status": "dead", - "verifiedAt": "2026-08-01", - "note": "display annotation no surface renders. Benign — docs-shaped, kept, not warned (hook.label precedent). VERDICT RE-TESTED AND UPHELD 2026-08-10 (#7427) under the previews ruling (#7131; README, 'Designer previews count as consumers'): 'no surface renders' is exactly the claim that ruling put back on the table for display keys, so it was measured instead of trusted. At objectui @e9ab52f9 AppPreview IS registered (previews/index.ts:40) and reachable (ResourceEditPage.tsx:949), and it contains ZERO occurrences of `areas` — it reads the app label at AppPreview.tsx:193 and walks `navigation` items, never the area collection. The area-level description reaches no human there." + "status": "live", + "verifiedAt": "2026-09-30", + "evidenceScope": "cross-repo", + "evidence": "objectui @db11afd4967: packages/app-shell/src/views/metadata-admin/previews/AppPreview.tsx#readAreas (resolves each area's `description` through `resolveI18nLabel` in the designer locale; unauthored stays undefined and draws nothing); objectui @db11afd4967: packages/app-shell/src/views/metadata-admin/previews/AppPreview.tsx#AppPreview (draws it under the area's label in the preview's Areas list, in read and design mode)", + "producer": "objectui @db11afd4967: packages/app-shell/src/views/metadata-admin/previews/index.ts#registerBuiltinPreviews (registers AppPreview for `app`); objectui @db11afd4967: packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx#MetadataResourceEditPageImpl (`app` registers no custom EditPage, so the generic edit route resolves `getMetadataPreview('app')` and hands it the draft); framework: packages/rest/src/meta-item-read-gate.ts#filterAppForUserWithReason (the served app keeps every key of each area it serves)", + "note": "RE-GRADED dead → live 2026-09-30 (#20299) under the #7131 previews ruling (README, 'Designer previews count as consumers'): for a display key, being shown to a human is the whole claimed effect. objectui#11027, read at the `.objectui-sha` pin db11afd4967, added the Areas list; the superseded note measured AppPreview at @e9ab52f9 with zero `areas` reads. UNCHANGED: still docs-shaped, deliberately KEPT (ADR-0033) and not authorWarn'd." }, "navigation": { "status": "live", @@ -232,7 +235,7 @@ "note": "the active area's tree replaces the top-level navigation. Since #4722 area trees ARE server-side gated: filterAppForUser (packages/rest/src/rest-server.ts:1870) runs the SAME filterNav over every `areas[].navigation`, so an item's `requiredPermissions` / `requiresService` is enforced identically in both trees and a gated entry (with its objectName/pageName/componentRef target) never reaches the browser. An area emptied BY the gate is dropped, mirroring the top-level group collapse; an area authored empty is passed through. Still client-only at both levels: `visible` (CEL — needs a bound user context the read layer lacks) and `requiresObject`. The area-LEVEL keys stay retired (#4651) — this enforces the items inside, not a revived area gate. 2026-09-27 (#20146): REPOINTED off objectui's `AppSidebar.tsx` — deprecated, never mounted (the console renders `UnifiedSidebar`), and removed from objectui main by objectui PR #10617 — to the mounted reader(s) above, each re-read at the `.objectui-sha` pin f8a9d0fb and unchanged at objectui main fb91ac9b0. The `AppSchemaRenderer` leg is unchanged by this re-point." } }, - "note": "Drilled because the gating keys diverged sharply from the live identity/tree keys — and they are gone: `visible` and `requiredPermissions` were RETIRED in 17.0.0 (#4651), rows DELETED because NavigationAreaSchema is strict, so the keys left the walked shape and retained rows would report ORPHAN. Keep drilling: `description` is the surviving benign dead key, and the drill is what would catch a new gate being added here." + "note": "Drilled because the gating keys diverged sharply from the live identity/tree keys — and they are gone: `visible` and `requiredPermissions` were RETIRED in 17.0.0 (#4651), rows DELETED because NavigationAreaSchema is strict, so the keys left the walked shape and retained rows would report ORPHAN. Keep drilling: the drill is what would catch a new gate being added here." }, "contextSelectors": { "children": { diff --git a/packages/spec/liveness/connector.json b/packages/spec/liveness/connector.json index ca81696ee82..ce48036228c 100644 --- a/packages/spec/liveness/connector.json +++ b/packages/spec/liveness/connector.json @@ -85,9 +85,12 @@ "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." + "status": "live", + "verifiedAt": "2026-09-30", + "evidenceScope": "cross-repo", + "evidence": "packages/services/service-automation/src/engine.ts#getConnectorDescriptors — `description: a.description` on each projected action, the `GET /api/v1/automation/connectors` payload; objectui @db11afd4967: packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.tsx#connectorActionsToOptions (a non-blank string `description` becomes the action option's `hint`, fed by `useConnectorActionOptions` off that payload); objectui @db11afd4967: packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.tsx#ReferenceCombobox (draws each option as `label — hint` in the `connector_action` node's Action picker)", + "producer": "packages/connectors/connector-slack/src/slack-connector.ts#createSlackConnector — a `description` on each of its three actions; packages/connectors/connector-openapi/src/openapi-connector.ts#createOpenApiConnector — `description: op.description`; packages/connectors/connector-mcp/src/mcp-connector.ts#createMcpConnector — the server's tool description verbatim; objectui @db11afd4967: packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts#FLOW_NODE_CONFIG (the `connector_action` node's `actionId` field is a `connector-action` reference, which is what mounts that picker). Same two-door caveat as `actions.key`.", + "note": "RE-GRADED dead → live 2026-09-30 (#20287): objectui#11028, read at the `.objectui-sha` pin db11afd4967, shows it beside the action's label in the flow designer's Action picker. Display-shaped, so the picker IS the claimed effect (the #7131 split), as for `actions.label`. The superseded note — no consumer in either repo reads it back — was true until objectui#11028." }, "inputSchema": { "status": "live", @@ -95,12 +98,15 @@ "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." + "note": "A structural (not display) consumer, cross-repo like `outputSchema`'s: 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." + "status": "live", + "verifiedAt": "2026-09-30", + "evidenceScope": "cross-repo", + "evidence": "packages/services/service-automation/src/engine.ts#getConnectorDescriptors — `outputSchema: a.outputSchema` on each projected action, the `GET /api/v1/automation/connectors` payload; objectui @db11afd4967: packages/app-shell/src/views/metadata-admin/inspectors/flow-scope.ts#nodeOutputRefs (a committed `connector_action` node offers one `nodeId.key` reference per top-level `properties` key of its action's `outputSchema`, through `connectorActionOutputSchema` and `connectorActionOutputKeys`; no schema, no references); objectui @db11afd4967: packages/app-shell/src/views/metadata-admin/inspectors/flow-scope.ts#resolveFlowScope (hands those references to every downstream node's and edge's data picker)", + "producer": "objectui @db11afd4967: packages/app-shell/src/views/metadata-admin/inspectors/connector-input-fields.ts#useConnectorRegistry (reads that payload); objectui @db11afd4967: packages/app-shell/src/views/metadata-admin/inspectors/FlowNodeInspector.tsx#FlowNodeInspector and objectui @db11afd4967: packages/app-shell/src/views/metadata-admin/inspectors/FlowEdgeInspector.tsx#FlowEdgeInspector (each reads the registry when the flow holds a committed connector action and passes it to `useFlowScope` — the input the read depends on); framework: packages/connectors/connector-slack/src/slack-connector.ts#createSlackConnector — `outputSchema: slackOutputSchema()` on every action; packages/connectors/connector-openapi/src/openapi-connector.ts#buildOutputSchema; packages/connectors/connector-mcp/src/mcp-connector.ts#createMcpConnector — the tool's `outputSchema` when the server declares one. Same two-door caveat as `actions.key`.", + "note": "RE-GRADED dead → live 2026-09-30 (#20287): objectui#11028, read at the `.objectui-sha` pin db11afd4967, types a connector node's downstream references from it — the repair the superseded note named. The engine stores each top-level key of a node's `output` as `nodeId.key`, so the offered references are the ones a run writes. The designer offers them and stops flagging them as out of scope; nothing validates a reference against the schema." }, "effect": { "status": "live", diff --git a/packages/spec/liveness/permission.json b/packages/spec/liveness/permission.json index 969b43f678e..c88d80508f7 100644 --- a/packages/spec/liveness/permission.json +++ b/packages/spec/liveness/permission.json @@ -170,13 +170,13 @@ "status": "dead", "evidenceScope": "cross-repo", "verifiedAt": "2026-08-10", - "note": "CORRECTED 2026-07-30 (was live with no evidence): no consumer in either repo. VERDICT RE-TESTED AND UPHELD 2026-08-10 (#7427) against the maintainer ruling that a designer preview rendering a key to a human is a runtime consumer (2026-08-10, #7131; README, 'Designer previews count as consumers'). This row is the closest structural twin of the four rows that ruling re-graded — a display `label` marked dead — so it was measured rather than assumed, and it comes out the other way. THE MEASUREMENT, at objectui @e9ab52f9: PermissionPreview IS registered for `permission` (previews/index.ts:71) and IS reachable (ResourceEditPage.tsx:949), so the preview lookup runs; but PermissionPreview.tsx:111 reads `rowLevelSecurity` only as an ARRAY and PermissionPreview.tsx:164 renders `${rls.length} RLS rules` — a COUNT. It never indexes a policy, never reads `.label`, and no policy field reaches a human through it. The 2026-07-30 wording 'PermissionPreview counts them' was exact, and counting is not rendering: the ruling turns on the VALUE being shown to a person, which is precisely what a length does not do. The other measured surface is PermissionAdvancedFacets.tsx:192-193 (reads `draft.rowLevelSecurity`, strips retired keys) and :264 (writes it back) — an authoring FORM, the 'authoring surface echoing input' the 2026-07 correction rejected, and the new ruling names previews, not edit forms. So both halves of the original closure survive it. Benign display metadata — deliberately NOT authorWarn'd. To re-open this row, the thing to look for is a preview that renders the policy's label text, not another surface that counts policies." + "note": "No reader reaches it, measured at objectui @db11afd4967. PermissionPreview.tsx#readPolicies reads each policy's `label`, and `PermissionPreview` draws it in its Row-Level Security list, but no route mounts that preview for `permission`. ResourceEditPage.tsx#MetadataResourceEditPage hands every non-create `permission` item to the custom EditPage that services/builtinComponents.tsx registers, `PermissionMatrixEditPage`. That page renders no preview, and its RLS form `PermissionAdvancedFacets` reads no policy label. Create mode previews only object, report and dataset. The other `getMetadataPreview` callers (`EmbeddedItemEditor`, `StudioDesignSurface`, the dev-only preview gallery) never open a `permission`. A preview no route mounts is a read point that never runs (README, 'Designer previews count as consumers'). Benign display metadata, deliberately NOT authorWarn'd. To re-open: a mounted surface that draws the policy's label." }, "description": { "status": "dead", "evidenceScope": "cross-repo", "verifiedAt": "2026-08-10", - "note": "CORRECTED 2026-07-30 (was live with no evidence): same closure as label. RE-TESTED AND UPHELD 2026-08-10 (#7427) with `label`, same measurement at objectui @e9ab52f9 — the permission preview counts RLS policies (PermissionPreview.tsx:164) and renders no field of any individual policy. Benign — not authorWarn'd." + "note": "Same closure as `label`, at objectui @db11afd4967: `PermissionPreview` draws each policy's `description` beneath its row, but no route mounts it for `permission`. `PermissionMatrixEditPage` takes the item, and its RLS form reads no description. Benign, not authorWarn'd." }, "object": { "status": "live", diff --git a/packages/spec/liveness/state-counts/app.md b/packages/spec/liveness/state-counts/app.md index deb6ea3ab26..e782acfa559 100644 --- a/packages/spec/liveness/state-counts/app.md +++ b/packages/spec/liveness/state-counts/app.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `app` | 49 | 0 | 0 | 9 | 1 | 59 | +| `app` | 50 | 0 | 0 | 8 | 1 | 59 | diff --git a/packages/spec/liveness/state-counts/connector.md b/packages/spec/liveness/state-counts/connector.md index 84df8371bd3..b2dd149ac6f 100644 --- a/packages/spec/liveness/state-counts/connector.md +++ b/packages/spec/liveness/state-counts/connector.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `connector` | 29 | 0 | 0 | 25 | 1 | 55 | +| `connector` | 31 | 0 | 0 | 23 | 1 | 55 | diff --git a/packages/spec/liveness/view.json b/packages/spec/liveness/view.json index b65e0730e0b..bd9ae6c0202 100644 --- a/packages/spec/liveness/view.json +++ b/packages/spec/liveness/view.json @@ -12,7 +12,7 @@ "status": "dead", "evidenceScope": "cross-repo", "verifiedAt": "2026-08-10", - "note": "Display metadata on the container with no runtime consumer. VERDICT RE-TESTED AND UPHELD 2026-08-10 (#7427) under the maintainer ruling that a designer preview rendering a key to a human is a runtime consumer (2026-08-10, #7131; README, 'Designer previews count as consumers'). It is the twin of `translation.label`, which that ruling DID re-grade, so it was measured, and it splits on REACHABILITY — the half of the README rule that is easy to skip. THE MEASUREMENT, at objectui @e9ab52f9: ViewPreview is registered for `view` (previews/index.ts:37) and resolved by ResourceEditPage.tsx:949, and it genuinely READS this key — ViewPreview.tsx:115 injects `(body).label ?? (draft).label ?? name` as the label of the single named listView it hands to the `object-view` schema. The read is real; the RENDER is not reachable. That injected label has exactly two render paths in plugin-view's ObjectView, and this preview takes neither: ObjectView.tsx:1119 renders `{view.label || key}` as a named-view tab, but ObjectView.tsx:1112 returns null when the map holds one entry or fewer, and ViewPreview always injects exactly one; ObjectView.tsx:993 passes the label into the `renderListView` list-view schema (ListView appends it to export filenames), but `renderListView` is a prop and plugin-view/src/index.tsx:58-64 registers the bare `ObjectViewRenderer` for `object-view` — it renders ObjectView with schema and dataSource only, so the prop is undefined on this path. A read point whose render cannot fire is what PR #7425 meant by 'a preview no registry hands a draft to is a read point that never runs', one level in: here the registry does hand it a draft, and the draft's label still reaches no human. Not authorWarn'd: the README's rule is that pure display annotation (description, tags, icon) must not warn, and the job.label / translation.label precedent is this exact case — docs-shaped, deliberately kept, ADR-0033 exempt from enforce-or-remove. To re-open: a second listView entry, or `renderListView` wired on this path, would make it render — either change flips this row." + "note": "Display metadata on the `defineView` container. No reader reaches it, measured at objectui @db11afd4967. ViewPreview.tsx#ViewPreview draws `draft.label` as the preview heading (`ViewLabelHeading`), but the Studio draft is always a ViewItem, never the container. `getMetaItems` drops aggregated containers from every `view` list, and Studio's view `listFilter` (services/builtinComponents.tsx) drops them again. `expandViewContainerWithDiagnostics` gives each ViewItem the `label` of its list or form entry, never the container's. So no Studio list or quick-find hands the container to the preview, and its own label reaches no human there. The `name` row draws the same container/ViewItem line. Not authorWarn'd: docs-shaped, deliberately kept (ADR-0033). To re-open: a Studio surface that draws a container's own label." }, "object": { "status": "live",