diff --git a/.changeset/24900-webhook-label-description-live.md b/.changeset/24900-webhook-label-description-live.md new file mode 100644 index 00000000000..0a3873c13c0 --- /dev/null +++ b/.changeset/24900-webhook-label-description-live.md @@ -0,0 +1,9 @@ +--- +'@objectstack/spec': patch +--- + +fix(spec): `os lint` no longer tells authors to remove a declared webhook's `label` or `description` + +Clause-②: no (a liveness-ledger re-grade: no schema, accept set, served body or error code moves; only the advisory `liveness-dead-property` warning on these two keys stops) + +The shipped liveness ledger (`liveness/webhook.json`) graded a declared webhook's `label` and `description` `dead` once the boot seeder that copied them into a `sys_webhook` row retired, and the advisory lint warned "Remove it" on every webhook that authored them. They are read: the Studio metadata list and quick-find show both keys, read off `GET /api/v1/meta/webhook`. Both rows are now `live`, so a declared webhook may keep its `label` and `description` and no warning is printed for them. Nothing else changes: the dispatcher still delivers on the same props as before. diff --git a/docs/qa/platform-checklist/areas/integration-system.json b/docs/qa/platform-checklist/areas/integration-system.json index adbfb8c0488..075c4c6fddf 100644 --- a/docs/qa/platform-checklist/areas/integration-system.json +++ b/docs/qa/platform-checklist/areas/integration-system.json @@ -642,7 +642,7 @@ ], "source": [ "packages/spec/src/automation/webhook.zod.ts#WebhookTriggerType (WebhookTriggerType enum + why undelete/api are absent; materialization contract; strict shape #4001)", - "packages/spec/liveness/webhook.json (9 props live on the registry read; label/description dead since the boot seeder retired)", + "packages/spec/liveness/webhook.json (9 props live on the registry read; label/description live on the Studio metadata list and quick-find, which read the declared item over GET /api/v1/meta/webhook)", "packages/plugins/plugin-webhooks/src/declared-webhooks.ts + auto-enqueuer.ts (the registry read and its remaps; trigger→event mapping incl. the #4639 bulk pair; #3196 unknown-trigger warn; the activation-ledger switch; an 18.0 database's seeded rows read beside the declaration)", "packages/services/service-messaging/src/http-outbox.ts#HttpRedeliverError (delivery statuses, attempts; HttpRedeliverError + assertHttpRedeliverable/assertRedeliverAllowed — terminal-only, the attempts===0 parked-row refusal, the fail-closed guard) + packages/services/service-messaging/src/http-signature-at-rest.integration.test.ts (redelivery replays the stored signature byte-for-byte)", "packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts#session (POST /api/v1/webhooks/redeliver — session auth with NO capability gate by design ('every authenticated user counts'), #10740 activeOrganizationId threading, the 401/400/404/409/500 arms) + (the #8069 redeliver-guard install; its ABSENCE is an error-level log, not a silent pass)", diff --git a/examples/app-showcase/src/automation/webhooks/index.ts b/examples/app-showcase/src/automation/webhooks/index.ts index e04a91ea3c1..8fd77f3a412 100644 --- a/examples/app-showcase/src/automation/webhooks/index.ts +++ b/examples/app-showcase/src/automation/webhooks/index.ts @@ -4,8 +4,10 @@ import { defineWebhook } from '@objectstack/spec/automation'; /** * Outbound webhook — fans out task lifecycle events (create/update/delete) - * to an external endpoint. No `label` / `description`: a declared webhook has - * no Setup row to show them on, and the liveness ledger marks both dead. + * to an external endpoint. A declared webhook has no Setup row, but a `label` / + * `description` authored here is not inert: the liveness ledger grades both + * `live`, because the Studio metadata list and quick-find show them, read off + * `GET /api/v1/meta/webhook`. * Authored via `defineStack({ webhooks })`; the webhooks plugin's dispatcher * reads this declaration from the registry (ADR-0131 D2) — no `sys_webhook` * row is written for it. diff --git a/packages/lint/src/lint-liveness-properties.test.ts b/packages/lint/src/lint-liveness-properties.test.ts index 1c22df12954..ec3aeb9ecbe 100644 --- a/packages/lint/src/lint-liveness-properties.test.ts +++ b/packages/lint/src/lint-liveness-properties.test.ts @@ -251,12 +251,13 @@ describe('lintLivenessProperties', () => { // #3494 PRUNED the aspirational dead props from the schema and the #3489 // materializer bridge made every remaining prop live. ADR-0131 D2 then // retired that bridge: a declared webhook is read from the registry and has - // no Setup row, so the two display-only props, `label` and `description`, - // lost their only reader and the ledger marks them dead. Every delivery- - // bearing prop stays silent. Runs against the REAL webhook.json, so a ledger - // flip that is not carried to authors fails here. + // no Setup row. The two display-only props, `label` and `description`, stay + // live all the same: the Studio metadata list and quick-find draw them off + // `GET /api/v1/meta/webhook`, so the ledger grades them live and nothing + // warns. Runs against the REAL webhook.json, so a ledger flip that is not + // carried to authors fails here. - it('warns on a declared webhook\'s label and description only — every delivery-bearing prop is live', () => { + it('stays silent on a declared webhook that authors every prop, label and description included', () => { const findings = lintLivenessProperties({ webhooks: [{ name: 'showcase_task_changed', @@ -272,14 +273,7 @@ describe('lintLivenessProperties', () => { description: 'Sends task lifecycle events to an external system.', }], }); - expect(findings.map((f) => [f.rule, f.message.match(/`(\w+)`/)?.[1]])).toEqual([ - ['liveness-dead-property', 'label'], - ['liveness-dead-property', 'description'], - ]); - for (const f of findings) { - expect(f.where).toBe("webhook 'showcase_task_changed'"); - expect(f.hint).toContain('no Setup row'); - } + expect(findings).toEqual([]); }); it('stays silent on a declared webhook that authors only what is delivered on', () => { diff --git a/packages/spec/liveness/state-counts/webhook.md b/packages/spec/liveness/state-counts/webhook.md index 1cf9e8785ee..d052b4768eb 100644 --- a/packages/spec/liveness/state-counts/webhook.md +++ b/packages/spec/liveness/state-counts/webhook.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `webhook` | 17 | 0 | 0 | 2 | 0 | 19 | +| `webhook` | 19 | 0 | 0 | 0 | 0 | 19 | diff --git a/packages/spec/liveness/webhook.json b/packages/spec/liveness/webhook.json index 0ffb012b813..8e40abf689f 100644 --- a/packages/spec/liveness/webhook.json +++ b/packages/spec/liveness/webhook.json @@ -1,6 +1,6 @@ { "type": "webhook", - "_note": "WebhookSchema (outbound webhook — packages/spec/src/automation/webhook.zod.ts). Governed via a spec-only schema override in the gate (SPEC_ONLY_SCHEMAS): webhook is still NOT a registered metadata type (absent from kernel/metadata-type-schemas.ts) — registering it would turn on Studio webhook CRUD + saveMetaItem overlay + create-seeds; that reassessment is tracked in #3490 and deliberately deferred. TWO THINGS CLOSED THE OLD 'entire surface is dead' classification: (1) #3494 PRUNED the aspirational dead props outright — body / payloadFields / includeSession / retryPolicy / tags / authentication are gone from the schema; (2) the #3461 materializer bridge (PR #3489) makes every REMAINING prop live. `bootstrapDeclaredWebhooks` (plugins/plugin-webhooks/src/bootstrap-declared-webhooks.ts) materializes each stack/connector-authored webhook into a `sys_webhook` DATA row on boot — WebhookSchema.parse (:114) → mapWebhookToRow (:191): `object`→`object_name` (:195), `isActive`→`active` (:202), same-named `name`/`label`/`triggers`/`url`/`method`/`description`, `secret`→the encrypted `signing_secret` column (#7799), `headers`→the encrypted `headers_secret` column (#7986), and the REST of the envelope → `definition_json`. The dispatcher (AutoEnqueuer) reads those rows (auto-enqueuer.ts:175 `where:{active:true}`) and fans out on data.record.* events (plus the aggregate data.records.* a predicate write publishes, dispatched under the opt-in bulk_update/bulk_delete triggers — #4639), reading `object_name`/`name`/`url`/`method`/`triggers` off the row, `timeoutMs` back out of `definition_json`, and BOTH credentials through `engine.resolveSecretField()` against their encrypted columns — the signing key from `signing_secret` (#7799) and the custom headers from `headers_secret` (#7986). So all 11 remaining props are LIVE; there is no dead/experimental prop left, so nothing carries an authorWarn (the old per-webhook `url` heads-up is gone — authoring is no longer a no-op). Seed-not-clobber: an admin-edited row (`customized`) is never re-seeded (bootstrap-declared-webhooks.ts:143). The `object` prop carries the ADR-0054 runtime proof for the whole materialization pipeline (bound high-risk class `webhook-materialization`). Field-level line refs: materializer bootstrap-declared-webhooks.ts, runtime object plugins/plugin-webhooks/src/sys-webhook.object.ts, dispatcher auto-enqueuer.ts. 2026-08-28 — WHOLE FILE RE-ANCHORED (commit 9ee2dcfbd, adoption of the #12516 symbol-anchor grammar), and this file was the batch's worst starting position by a distance. Every citation in it was written as a BARE FILENAME with a line (`bootstrap-declared-webhooks.ts:193`, `auto-enqueuer.ts:266`, `sys-webhook.object.ts:160`), which `PATH_RE` cannot parse: a citation must be repo-rooted to be a citation at all. So all eleven `live` entries scanned as ZERO local paths and ZERO line citations — no existence check, no line bound, no key-mention anchoring, nothing. The gate was not passing this file; it was never asked about it, and the ledger's own counters could not say so. Combined with the second fact — no entry carried `verifiedAt`, which the review that accepted the re-anchoring sorts as OLDEST — these rows were unfalsifiable and unclocked at once. Re-closed by hand against c459da6bc: every consumer read on the current tree, no line carried forward. What the re-closure found: the claims were all TRUE and the pointers were all merely unresolvable — the opposite of `object.json` in batch 1, where the pointers resolved and the claims were wrong. Both are the same defect at different ends. Two prose repairs fell out of it: the parenthetical line numbers are gone in favour of the symbols that own each read, and the dispatcher's credential legs are named as `attachSecret` / `attachHeaders`, which REFUSE a subscription whose credential cannot be recovered rather than delivering without it — a fail-closed behaviour the old prose did not record. 2026-10-19 — THE MATERIALIZER RETIRED (ADR-0131 D2, ruling Q2 A on the declared-webhook seeder): `bootstrapDeclaredWebhooks` no longer exists, so no declared webhook becomes a `sys_webhook` DATA row. The dispatcher reads the declaration from the registry instead (`packages/plugins/plugin-webhooks/src/declared-webhooks.ts`, projected per cache refresh by `auto-enqueuer.ts#projectDeclared`), and switches it through the activation ledger (regime C). Every delivery-bearing prop stays LIVE on the new reader and is re-anchored there; `label` and `description` lost their only reader (the Setup row) and are DEAD, owed enforce-or-remove. The `object` proof now boots the stack and asserts the declared webhook reaches the dispatcher with `object`→`object_name` applied and NO row written.", + "_note": "WebhookSchema (outbound webhook — packages/spec/src/automation/webhook.zod.ts). Governed via a spec-only schema override in the gate (SPEC_ONLY_SCHEMAS): webhook is still NOT a registered metadata type (absent from kernel/metadata-type-schemas.ts) — registering it would turn on Studio webhook CRUD + saveMetaItem overlay + create-seeds; that reassessment is tracked in #3490 and deliberately deferred. TWO THINGS CLOSED THE OLD 'entire surface is dead' classification: (1) #3494 PRUNED the aspirational dead props outright — body / payloadFields / includeSession / retryPolicy / tags / authentication are gone from the schema; (2) the #3461 materializer bridge (PR #3489) makes every REMAINING prop live. `bootstrapDeclaredWebhooks` (plugins/plugin-webhooks/src/bootstrap-declared-webhooks.ts) materializes each stack/connector-authored webhook into a `sys_webhook` DATA row on boot — WebhookSchema.parse (:114) → mapWebhookToRow (:191): `object`→`object_name` (:195), `isActive`→`active` (:202), same-named `name`/`label`/`triggers`/`url`/`method`/`description`, `secret`→the encrypted `signing_secret` column (#7799), `headers`→the encrypted `headers_secret` column (#7986), and the REST of the envelope → `definition_json`. The dispatcher (AutoEnqueuer) reads those rows (auto-enqueuer.ts:175 `where:{active:true}`) and fans out on data.record.* events (plus the aggregate data.records.* a predicate write publishes, dispatched under the opt-in bulk_update/bulk_delete triggers — #4639), reading `object_name`/`name`/`url`/`method`/`triggers` off the row, `timeoutMs` back out of `definition_json`, and BOTH credentials through `engine.resolveSecretField()` against their encrypted columns — the signing key from `signing_secret` (#7799) and the custom headers from `headers_secret` (#7986). So all 11 remaining props are LIVE; there is no dead/experimental prop left, so nothing carries an authorWarn (the old per-webhook `url` heads-up is gone — authoring is no longer a no-op). Seed-not-clobber: an admin-edited row (`customized`) is never re-seeded (bootstrap-declared-webhooks.ts:143). The `object` prop carries the ADR-0054 runtime proof for the whole materialization pipeline (bound high-risk class `webhook-materialization`). Field-level line refs: materializer bootstrap-declared-webhooks.ts, runtime object plugins/plugin-webhooks/src/sys-webhook.object.ts, dispatcher auto-enqueuer.ts. 2026-08-28 — WHOLE FILE RE-ANCHORED (commit 9ee2dcfbd, adoption of the #12516 symbol-anchor grammar), and this file was the batch's worst starting position by a distance. Every citation in it was written as a BARE FILENAME with a line (`bootstrap-declared-webhooks.ts:193`, `auto-enqueuer.ts:266`, `sys-webhook.object.ts:160`), which `PATH_RE` cannot parse: a citation must be repo-rooted to be a citation at all. So all eleven `live` entries scanned as ZERO local paths and ZERO line citations — no existence check, no line bound, no key-mention anchoring, nothing. The gate was not passing this file; it was never asked about it, and the ledger's own counters could not say so. Combined with the second fact — no entry carried `verifiedAt`, which the review that accepted the re-anchoring sorts as OLDEST — these rows were unfalsifiable and unclocked at once. Re-closed by hand against c459da6bc: every consumer read on the current tree, no line carried forward. What the re-closure found: the claims were all TRUE and the pointers were all merely unresolvable — the opposite of `object.json` in batch 1, where the pointers resolved and the claims were wrong. Both are the same defect at different ends. Two prose repairs fell out of it: the parenthetical line numbers are gone in favour of the symbols that own each read, and the dispatcher's credential legs are named as `attachSecret` / `attachHeaders`, which REFUSE a subscription whose credential cannot be recovered rather than delivering without it — a fail-closed behaviour the old prose did not record. 2026-10-19 — THE MATERIALIZER RETIRED (ADR-0131 D2, ruling Q2 A on the declared-webhook seeder): `bootstrapDeclaredWebhooks` no longer exists, so no declared webhook becomes a `sys_webhook` DATA row. The dispatcher reads the declaration from the registry instead (`packages/plugins/plugin-webhooks/src/declared-webhooks.ts`, projected per cache refresh by `auto-enqueuer.ts#projectDeclared`), and switches it through the activation ledger (regime C). Every delivery-bearing prop stays LIVE on the new reader and is re-anchored there; `label` and `description` lost their only reader (the Setup row) and are DEAD, owed enforce-or-remove. The `object` proof now boots the stack and asserts the declared webhook reaches the dispatcher with `object`→`object_name` applied and NO row written. 2026-10-20 — CORRECTED (#24900): `label` and `description` are LIVE, not dead. The 2026-10-19 verdict measured the Setup `sys_webhook` list only; the Studio metadata list (objectui `ResourceListPage.tsx#defaultColumns`) and quick-find read the declared registry item over `GET /api/v1/meta/webhook`, measured booted against examples/app-showcase. Both rows carry the evidence; no enforce-or-remove is owed.", "props": { "name": { "status": "live", @@ -9,10 +9,12 @@ "verifiedAt": "2026-10-19" }, "label": { - "status": "dead", - "note": "Display-only — surfaced in Studio, never sent on the wire. 2026-08-28: RE-ANCHORED (commit 9ee2dcfbd) — see the file `_note` for what the re-closure found across this ledger; re-closed by hand against c459da6bc, DATED for the first time. 2026-10-19: DEAD since ADR-0131 D2 retired the boot seeder `bootstrapDeclaredWebhooks`, which was its only reader — it copied this value into the declared webhook's `sys_webhook` row, where the Setup list showed it. A declared webhook has no row any more, and nothing displays a declaration: the dispatcher reads only what it delivers on (`auto-enqueuer.ts#projectDeclared`), the activation door only the name. Display-only, so the loss is a label nobody sees, never a delivery that changes. Enforce-or-remove (ADR-0049) is owed: a Setup surface that lists declared webhooks (the declared-sharing-rule Disable and Clone surface is the pattern) makes it live again, otherwise it retires.", - "verifiedAt": "2026-10-19", - "authorHint": "A declared webhook's `label` is shown nowhere: declared webhooks are read from the registry and have no Setup row. Remove it — it changes no delivery. A webhook your organization authors in Setup keeps its label." + "status": "live", + "verifiedAt": "2026-10-20", + "evidenceScope": "cross-repo", + "evidence": "objectui @616df6598b42, the .objectui-sha pin: packages/app-shell/src/views/metadata-admin/ResourceListPage.tsx#defaultColumns:1116 (the Studio metadata list's default `label` column, which line 495 falls back to because `webhook` registers no `listColumns`, and whose cell line 813 reads as `row.item[c.key]`); objectui @616df6598b42: packages/app-shell/src/views/metadata-admin/QuickFind.tsx#MetadataQuickFind:114 (the Studio metadata quick-find indexes every item's `label` off the same `client.list` read and draws it beside the item's name)", + "producer": "objectui @616df6598b42: packages/app-shell/src/console/AppContent.tsx#AppContent:163 (mounts `MetadataResourceListPage` on the Studio route `metadata/:type`); objectui @616df6598b42: packages/app-shell/src/views/metadata-admin/ResourceListPage.tsx#MetadataResourceListPage:145 (renders `DefaultMetadataList`: no objectui registration gives `webhook` a `ListPage` or `listColumns` at this pin); objectui @616df6598b42: packages/data-objectstack/src/metadata-client.ts#MetadataClient:1117 (`list('webhook')` reads `GET /api/v1/meta/webhook`); framework: packages/rest/src/meta-item-read-gate.ts#createMetaListAnswer (the one list answer both transports serve; `webhook` has no metadata-type redactor, so the authored value reaches the body)", + "note": "RE-GRADED dead → live 2026-10-20 (#24900) under the #7131 previews ruling (README, 'Designer previews count as consumers'), on the `hook.label` / `email_template.label` precedent: for a display key, being shown to a human is the whole of the claimed effect, and a Studio list column and the metadata quick-find are that showing. The 2026-10-19 `dead` verdict looked at the Setup `sys_webhook` list only; the Studio metadata list reads the registry item itself, so retiring the seeder never cut it. READER, read at the `.objectui-sha` pin 616df6598b42: `MetadataResourceListPage` falls through to `DefaultMetadataList`, whose columns are `config.listColumns ?? defaultColumns(...)` = the primary key, `label` and `description`; `webhook` has no registered preview (`registerMetadataPreview('webhook'` has zero hits at the pin) and no Setup catalog (`SETUP_CATALOG_TYPES` is permission/position/sharing_rule). PRODUCER, measured booted rather than read: a throwaway `pnpm dev -- --fresh` boot of the real examples/app-showcase composition at 223ae939bb, with a probe `label` and `description` authored onto its declared webhook (reverted after), signed in as the dev admin, answered `GET /api/v1/meta/webhook` 200 with `items` holding the one declared webhook carrying both probe values and `_packageId: com.example.showcase`; `GET /api/v1/meta/types` listed `webhook`, and `GET /api/v1/meta/package` listed that package with `scope: project`, which is what the list page's package scope admits. Docs-shaped annotation, deliberately KEPT and not authorWarn'd; `live` here does not mean the dispatcher acquired a use for it — `auto-enqueuer.ts#projectDeclared` still reads only what it delivers on. HISTORY, kept as history — the note this row carried before the re-grade: Display-only — surfaced in Studio, never sent on the wire. 2026-08-28: RE-ANCHORED (commit 9ee2dcfbd) — see the file `_note` for what the re-closure found across this ledger; re-closed by hand against c459da6bc, DATED for the first time. 2026-10-19: DEAD since ADR-0131 D2 retired the boot seeder `bootstrapDeclaredWebhooks`, which was its only reader — it copied this value into the declared webhook's `sys_webhook` row, where the Setup list showed it. A declared webhook has no row any more, and nothing displays a declaration: the dispatcher reads only what it delivers on (`auto-enqueuer.ts#projectDeclared`), the activation door only the name. Display-only, so the loss is a label nobody sees, never a delivery that changes. Enforce-or-remove (ADR-0049) is owed: a Setup surface that lists declared webhooks (the declared-sharing-rule Disable and Clone surface is the pattern) makes it live again, otherwise it retires." }, "object": { "status": "live", @@ -64,10 +66,12 @@ "verifiedAt": "2026-10-19" }, "description": { - "status": "dead", - "note": "Display-only — surfaced/editable in Studio, inert in dispatch. 2026-08-28: RE-ANCHORED (commit 9ee2dcfbd) — see the file `_note` for what the re-closure found across this ledger; re-closed by hand against c459da6bc, DATED for the first time. 2026-10-19: DEAD since ADR-0131 D2 retired the boot seeder `bootstrapDeclaredWebhooks`, which was its only reader — it copied this value into the declared webhook's `sys_webhook` row, where the Setup list showed it. A declared webhook has no row any more, and nothing displays a declaration: the dispatcher reads only what it delivers on (`auto-enqueuer.ts#projectDeclared`), the activation door only the name. Display-only, so the loss is a label nobody sees, never a delivery that changes. Enforce-or-remove (ADR-0049) is owed: a Setup surface that lists declared webhooks (the declared-sharing-rule Disable and Clone surface is the pattern) makes it live again, otherwise it retires.", - "verifiedAt": "2026-10-19", - "authorHint": "A declared webhook's `description` is shown nowhere: declared webhooks are read from the registry and have no Setup row. Remove it — it changes no delivery; keep the explanation as a code comment instead." + "status": "live", + "verifiedAt": "2026-10-20", + "evidenceScope": "cross-repo", + "evidence": "objectui @616df6598b42, the .objectui-sha pin: packages/app-shell/src/views/metadata-admin/ResourceListPage.tsx#defaultColumns:1117 (the Studio metadata list's default `description` column, which line 495 falls back to because `webhook` registers no `listColumns`, and whose cell line 813 reads as `row.item[c.key]`); objectui @616df6598b42: packages/app-shell/src/views/metadata-admin/QuickFind.tsx#MetadataQuickFind:115 (the Studio metadata quick-find indexes every item's `description` off the same `client.list` read and draws it beside the item's name)", + "producer": "objectui @616df6598b42: packages/app-shell/src/console/AppContent.tsx#AppContent:163 (mounts `MetadataResourceListPage` on the Studio route `metadata/:type`); objectui @616df6598b42: packages/app-shell/src/views/metadata-admin/ResourceListPage.tsx#MetadataResourceListPage:145 (renders `DefaultMetadataList`: no objectui registration gives `webhook` a `ListPage` or `listColumns` at this pin); objectui @616df6598b42: packages/data-objectstack/src/metadata-client.ts#MetadataClient:1117 (`list('webhook')` reads `GET /api/v1/meta/webhook`); framework: packages/rest/src/meta-item-read-gate.ts#createMetaListAnswer (the one list answer both transports serve; `webhook` has no metadata-type redactor, so the authored value reaches the body)", + "note": "RE-GRADED dead → live 2026-10-20 (#24900) under the #7131 previews ruling (README, 'Designer previews count as consumers'), on the `hook.label` / `email_template.label` precedent: for a display key, being shown to a human is the whole of the claimed effect, and a Studio list column and the metadata quick-find are that showing. The 2026-10-19 `dead` verdict looked at the Setup `sys_webhook` list only; the Studio metadata list reads the registry item itself, so retiring the seeder never cut it. READER, read at the `.objectui-sha` pin 616df6598b42: `MetadataResourceListPage` falls through to `DefaultMetadataList`, whose columns are `config.listColumns ?? defaultColumns(...)` = the primary key, `label` and `description`; `webhook` has no registered preview (`registerMetadataPreview('webhook'` has zero hits at the pin) and no Setup catalog (`SETUP_CATALOG_TYPES` is permission/position/sharing_rule). PRODUCER, measured booted rather than read: a throwaway `pnpm dev -- --fresh` boot of the real examples/app-showcase composition at 223ae939bb, with a probe `label` and `description` authored onto its declared webhook (reverted after), signed in as the dev admin, answered `GET /api/v1/meta/webhook` 200 with `items` holding the one declared webhook carrying both probe values and `_packageId: com.example.showcase`; `GET /api/v1/meta/types` listed `webhook`, and `GET /api/v1/meta/package` listed that package with `scope: project`, which is what the list page's package scope admits. Docs-shaped annotation, deliberately KEPT and not authorWarn'd; `live` here does not mean the dispatcher acquired a use for it — `auto-enqueuer.ts#projectDeclared` still reads only what it delivers on. HISTORY, kept as history — the note this row carried before the re-grade: Display-only — surfaced/editable in Studio, inert in dispatch. 2026-08-28: RE-ANCHORED (commit 9ee2dcfbd) — see the file `_note` for what the re-closure found across this ledger; re-closed by hand against c459da6bc, DATED for the first time. 2026-10-19: DEAD since ADR-0131 D2 retired the boot seeder `bootstrapDeclaredWebhooks`, which was its only reader — it copied this value into the declared webhook's `sys_webhook` row, where the Setup list showed it. A declared webhook has no row any more, and nothing displays a declaration: the dispatcher reads only what it delivers on (`auto-enqueuer.ts#projectDeclared`), the activation door only the name. Display-only, so the loss is a label nobody sees, never a delivery that changes. Enforce-or-remove (ADR-0049) is owed: a Setup surface that lists declared webhooks (the declared-sharing-rule Disable and Clone surface is the pattern) makes it live again, otherwise it retires." } } }