Skip to content

Commit 6663a66

Browse files
fix(spec): the retired connector.webhooks refusal prescribes the declared-webhook model (#27499)
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP <!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB5CGwFpmZc621vVaShTPStz)_ Fixes #27448 Clause-②: no Metadata-protocol: none **Prose only.** The refusal, its code, its path and the accept set do not move. Measured on the compile repro below: before and after, `objectstack compile` exits 2 with one issue, `code: invalid_type`, `expected: never`, `path: ["webhooks"]`. Only the `message` text changes. ## 维护者速读 - **改了什么**:连接器上已退役的 `webhooks` 键被拒收时,处方原说「顶层 `webhooks:` 会物化进 `sys_webhook`」,现改为 ADR-0131 D2 的真实模型:平台从元数据注册表读取声明的 webhook 并在记录事件上投递,不写 `sys_webhook` 行。 - **为什么改**:作者照旧文案去找一条永远不会出现的 `sys_webhook` 行;实测声明的 webhook 写入 0 行。 - **风险与回滚**:纯文案,拒收本身、错误码、路径与可接受集不变;回滚即还原本 PR。 - **先看**:`packages/spec/src/integration/connector.zod.ts` 的 `WEBHOOKS_RETIRED`;`packages/spec/src/migrations/entries/semantic/18.connector-resilience-keys-retired.ts` 的 `replacement` / `reason`;新 pin 在 `connector-resilience-keys-retirement.test.ts`。 - **两分钟验证**:`pnpm --filter @objectstack/spec exec vitest run src/integration/connector-resilience-keys-retirement.test.ts`,预期 22/22 通过,其中一条断言新处方含「reads a declared webhook from the metadata registry」且不含「materialized」。 - **需要你做的**:无,席位落地。 ## What changes - **The refusal.** `WEBHOOKS_RETIRED` (the `connector.webhooks` tombstone's prescription) now says the stack's top-level `webhooks:` collection is read from the metadata registry and delivered on record events, writes no `sys_webhook` row, and that `sys_webhook` holds only the webhooks an organization authors at runtime. It also drops "never materialized into `sys_webhook`" from the account of why a nested webhook never ran: the dispatcher never read it. - **The D3 entry** `connector-resilience-keys-retired`: `replacement` says the top-level collection is read from the metadata registry and writes no `sys_webhook` row; `reason` says a nested webhook was never registered as a `webhook` item, read by the webhook dispatcher or delivered (was "registered, materialized or delivered"). - **The protocol-18 rationale fragment** for the same family in `migrations/registry.ts.template` (the upgrade text `os migrate` and the generated upgrade guide project): the same correction. - **Generated reference.** `content/docs/references/integration/connector.mdx`, both `webhooks` rows, regenerated by `gen:docs` from the new `.describe()` (never by hand). - **Comments carrying the same sentence:** the `webhooks` JSDoc on `ConnectorBaseSchema` (ships in `dist/*.d.ts`), two section comments in `connector.zod.ts`, the conversion docblock in `conversions/registry.ts`, and the retired-key and retired-def entry files for this retirement. - **The liveness proof summary.** `packages/spec/scripts/liveness/proof-registry.mts`, row `webhook-materialization`: the summary said "a stack/connector-authored webhook is materialized into a dispatchable sys_webhook row on boot"; it now states what its proof (`packages/qa/dogfood/test/webhook-materialization.dogfood.test.ts`) pins: a webhook declared in the top-level `webhooks:` collection is dispatched from the metadata registry and writes no sys_webhook row, through the `declaredWebhooks` reader the plugin binds on the data engine alone, with `object`→`object_name` applied, on a boot without realtime. The comment beside its bindings says the same. `id`, `label`, `proofId`, `proofRef`, `bound` and `ledgerBindings` are unchanged. - A `patch` changeset for `@objectstack/spec`. Not touched: released `CHANGELOG.md` entries and `content/docs/releases/**` (release-owned). No hit in `skills/**` or `docs/adr/**` for the old sentence or its kin. ### The message, before and after <details><summary>Before (origin/main 273137d)</summary> ```text `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so it was never materialized into `sys_webhook` and never delivered, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection, which is materialized into `sys_webhook` and delivered on record events — note that doing so STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. ``` </details> <details><summary>After (this branch)</summary> ```text `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so the webhook dispatcher never read it and never delivered it, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection. The platform reads a declared webhook from the metadata registry and delivers it on record events; it writes no `sys_webhook` row for it (`sys_webhook` holds only the webhooks an organization authors at runtime). Note that declaring it STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. ``` </details> ## Tests - New pin in `packages/spec/src/integration/connector-resilience-keys-retirement.test.ts`: the `webhooks` refusal contains `reads a declared webhook from the metadata registry` and `it writes no \`sys_webhook\` row for it`, and matches neither `materialized into \`sys_webhook\`` nor `materiali[sz]`; the D3 entry's `replacement` contains `reads from the metadata registry` and `writes no \`sys_webhook\` row`, and neither `replacement` nor `reason` says `materiali[sz]`. The existing pins (path, `invalid_type`, the opening sentence, `top-level \`webhooks:\` collection`, the migrate sentence, no tracker number) are unchanged and still green. - Round 1, at head `4ec164b9` (liveness summary), under `os-verify-lock`: `pnpm --filter @objectstack/spec check:liveness` green (`✓ … every governed-type property … is classified`, state counts current: 1219 classified); the liveness script tests `vitest run scripts/liveness/` **12/12 files, 358/358**; `pnpm --filter @objectstack/spec test:repo` **86/86 files, 1420/1420**. - Round 0, at head `80e34d7f`, all under `os-verify-lock` (shared-box seconds): - `pnpm --filter @objectstack/spec test` (local project): **750/750 files, 22313 passed, 1 todo**. - `pnpm --filter @objectstack/spec test:repo` (the repo project, which holds `connector-resilience-keys-retirement.test.ts`): **86/86 files, 1420/1420 tests**. - `pnpm --filter @objectstack/spec typecheck`: exit 0 (`check:test-typecheck: OK`). - **Reverse check of the new pin (one-off, no file left).** With `connector.zod.ts` reset to `origin/main`'s bytes (old sentence present: 1 hit; new phrase: 0 hits), the retirement test file reads **1 failed | 21 passed (22)**: `AssertionError: expected '\`connector.webhooks\` was removed in @…' to contain 'reads a declared webhook from the met…'`. Restored with `git checkout HEAD -- packages/spec/src/integration/connector.zod.ts`; `git diff HEAD` empty and the blob hash matches HEAD. ## Acceptance notes - **Liveness proof prose is in scope (seat, round 1).** The `webhook-materialization` proof summary carried the same false sentence; corrected here (see What changes). Its proof already asserted ADR-0131 D2, so only text moved. - **Governed surfaces.** No hit for the old sentence or its kin in `skills/**` or `docs/adr/**`, so none is named. - Released `CHANGELOG.md` entries (`@objectstack/spec` and five connector/automation packages) still carry the original retirement note's "never materialized into `sys_webhook`". That clause is about the nested array and was true of the model when it shipped; release-owned text, not edited. ## Verification <details><summary>Measure first, on origin/main 273137d</summary> - **Compile refusal reproduced.** Scratch copy of `examples/app-showcase` (removed afterwards), `webhooks: [{url:'http://x/c', events:['create']}] as any` added to `showcase_status_api`, `npx objectstack compile`: **exit 2**, one issue `{"expected":"never","code":"invalid_type","path":["webhooks"]}` with the "Before" message above (quoted verbatim from that run). Same copy against this branch's rebuilt spec: **exit 2**, the same `expected`, `code` and `path`, the "After" message. - **A declared webhook writes no `sys_webhook` row, on the code path.** `packages/plugins/plugin-webhooks/src/declared-webhooks.ts` reads declared webhooks from `engine._registry.listItems('webhook')` (falling back to the kernel `metadata` service) on every cache refresh; no `sys_webhook` insert exists outside tests. Its pin `declared-webhooks.test.ts` › "nothing writes a sys_webhook row: the plugin boot binds the reader and seeds zero rows" (`engine.writes.filter((w) => w.object === 'sys_webhook')` equals `[]`): **1 passed | 21 skipped**, `VERDICT command-exit 0`. </details> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `4ec164b9`: 107 derived (round 0's 105 plus `check:pm-governed-merges` and `check:pnpm-flag-spellings`). Reconciled with `--ran`: **107 accounted, 105 run, 2 NOT-MEASURED, 0 UNRUN** (`✓ dispatch-gates --ran`). The two exit 3 are prerequisite misses that need a full repo build (`spec check:skill-examples`, `check:dual-build-cjs-loads`), not findings; CI's check-runs carry them. <details><summary>107 gate commands and exit codes (head 4ec164b)</summary> ```text node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-adr-links.mjs :: exit 0 node scripts/check-adr-links.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-adoption.mjs :: exit 0 node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-dev-prereqs.mjs --self-test :: exit 0 node scripts/check-doc-route-spelling.mjs --advisory :: exit 0 node scripts/check-doc-route-spelling.mjs --self-test :: exit 0 node scripts/check-docs-section-name.mjs :: exit 0 node scripts/check-docs-section-name.mjs --self-test :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-issue-citations.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 node scripts/check-section-landing-index.mjs :: exit 0 node scripts/check-section-landing-index.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/docs-audit/check-affected-docs.mjs :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0 pnpm --filter @objectstack/spec run check:api-surface :: exit 0 pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0 pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0 pnpm --filter @objectstack/spec run check:docs :: exit 0 pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm --filter @objectstack/spec run check:empty-state :: exit 0 pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0 pnpm --filter @objectstack/spec run check:export-origins :: exit 0 pnpm --filter @objectstack/spec run check:exported-any :: exit 0 pnpm --filter @objectstack/spec run check:generated :: exit 0 pnpm --filter @objectstack/spec run check:liveness :: exit 0 pnpm --filter @objectstack/spec run check:llms-txt :: exit 0 pnpm --filter @objectstack/spec run check:skill-examples :: exit 3 pnpm --filter @objectstack/spec run check:skill-refs :: exit 0 pnpm --filter @objectstack/spec run check:spec-changes :: exit 0 pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 pnpm --filter @objectstack/spec run check:upgrade-guide :: exit 0 pnpm --filter @objectstack/spec run check:variant-docs :: exit 0 pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:corpus-claim-drift :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:doc-anchors :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:doc-frontmatter :: exit 0 pnpm check:docs-audit-scope :: exit 0 pnpm check:docs-redirects :: exit 0 pnpm check:docs-single-h1 :: exit 0 pnpm check:docs-spec-enumerations :: exit 0 pnpm check:docs-transcript-drift :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 3 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:issue-citations :: exit 0 pnpm check:keyed-text-bounds :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:logger-receiver-detach :: exit 0 pnpm check:merge-driver :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:objectql-double-limit :: exit 0 pnpm check:org-identifier :: exit 0 pnpm check:page-declaration-shape :: exit 0 pnpm check:platform-object-tenancy-census :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:pm-governed-merges :: exit 0 pnpm check:pm-prior-rulings :: exit 0 pnpm check:pnpm-flag-spellings :: exit 0 pnpm check:published-files :: exit 0 pnpm check:published-readme-links :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:react-page-adapter-contract :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:registry-log-declared :: exit 0 pnpm check:rest-log-spy-declared :: exit 0 pnpm check:role-word :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:slot-lookup :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:spec-docblock-symbol-anchors :: exit 0 pnpm check:spec-parsed-alias :: exit 0 pnpm check:system-context-census :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:vendor-version-stamps :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 ``` </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufzgrsarJGTAxa3CuzzA)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WGufzgrsarJGTAxa3CuzzA --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 77e993e commit 6663a66

10 files changed

Lines changed: 76 additions & 38 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): the retired `connector.webhooks` refusal describes declared webhooks as they work now: read from the metadata registry, with no `sys_webhook` row
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
An author who writes `webhooks` on a connector is refused, and the refusal tells them to declare the webhook in the stack's top-level `webhooks:` collection. It used to add that such a webhook "is materialized into `sys_webhook`". That is no longer how the platform works (ADR-0131 D2): a declared webhook is read from the metadata registry and delivered on record events, and no `sys_webhook` row is written for it. `sys_webhook` holds only the webhooks an organization authors at runtime. An author following the old text looked for a row that never appears.
11+
12+
The refusal now says this. The same correction is made in the protocol-18 upgrade note for the connector resilience family (its replacement and reason) and in the generated connector reference.
13+
14+
Prose only. The refusal itself, its error code, its path and what the connector schema accepts are unchanged.

‎content/docs/references/integration/connector.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ const result = ConnectorSchema.parse(data);
193193
| **triggers** | `never` | optional | [REMOVED] `connector.triggers` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a connector trigger never started anything: `AutomationEngine.registerConnector` registers a connector's actions only, no polling loop read `intervalSeconds` (or the `interval` spelling it was renamed from), and no receiver was driven by a `webhook` trigger. Delete the key; the `ConnectorTrigger` shape leaves with it. To start work from an external system, write a flow that calls the connector's action in a `connector_action` node: for an external event, an `api` flow that the event's sender calls; for a scheduled pull, a `schedule` flow. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
194194
| **syncConfig** | `never` | optional | [REMOVED] `connector.syncConfig` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever ran a connector-attached sync: nothing read `strategy`, `direction`, `realtimeSync`, `timestampField`, `conflictResolution`, `batchSize`, `deleteMode` or `filters`, so the `latest_wins` and `soft_delete` defaults resolved and deleted nothing. Delete the key; the `DataSyncConfig` shape leaves with it. A sync is defined on its TARGET: a `mapping` (`targetObject`, `fieldMapping`, `mode`, `upsertKey`) whose `connectorSource` names the `rest` or `openapi` connector it pulls from, the read action and an optional timestamp `watermark`, with a `job` for the cadence. Its pull runs when a `job` drives it — a `job` whose `pull: { mapping }` names that mapping; the binding alone moves no rows. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
195195
| **fieldMappings** | `never` | optional | [REMOVED] `connector.fieldMappings` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever moved a value through a connector field mapping: nothing read `source`, `target`, `defaultValue`, `dataType`, `required` or `syncMode`. Delete the key; the `ConnectorFieldMapping` shape leaves with it. Map fields on the sync's TARGET instead: a `mapping`'s `fieldMapping` (`source` → `target`, with a `transform` the import path executes), which its `connectorSource` pulls through. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
196-
| **webhooks** | `never` | optional | [REMOVED] `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so it was never materialized into `sys_webhook` and never delivered, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection, which is materialized into `sys_webhook` and delivered on record events — note that doing so STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
196+
| **webhooks** | `never` | optional | [REMOVED] `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so the webhook dispatcher never read it and never delivered it, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection. The platform reads a declared webhook from the metadata registry and delivers it on record events; it writes no `sys_webhook` row for it (`sys_webhook` holds only the webhooks an organization authors at runtime). Note that declaring it STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
197197
| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. This edit left `os migrate meta` with the protocol-16.x conversions: apply it by hand, or run the tool on a release that still carries the conversion — https://objectstack.ai/docs/protocol-upgrade. |
198198
| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
199199
| **connectionTimeoutMs** | `never` | optional | [REMOVED] `connector.connectionTimeoutMs` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the platform never honoured it and cannot honour it where it was declared: a connector's outbound call is a WHATWG `fetch`, whose only cancellation surface is one `AbortSignal` over the whole operation, so nothing there observes the connection phase separately, and the value only ever travelled (onto the reported def and the materialization fingerprint) without ever bounding a connect. Delete the key. Use `requestTimeoutMs` for the deadline the platform does keep — it is applied as `resilientFetch`'s per-attempt timeout — and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
@@ -490,7 +490,7 @@ Connector type
490490
| **triggers** | `never` | optional | [REMOVED] `connector.triggers` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a connector trigger never started anything: `AutomationEngine.registerConnector` registers a connector's actions only, no polling loop read `intervalSeconds` (or the `interval` spelling it was renamed from), and no receiver was driven by a `webhook` trigger. Delete the key; the `ConnectorTrigger` shape leaves with it. To start work from an external system, write a flow that calls the connector's action in a `connector_action` node: for an external event, an `api` flow that the event's sender calls; for a scheduled pull, a `schedule` flow. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
491491
| **syncConfig** | `never` | optional | [REMOVED] `connector.syncConfig` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever ran a connector-attached sync: nothing read `strategy`, `direction`, `realtimeSync`, `timestampField`, `conflictResolution`, `batchSize`, `deleteMode` or `filters`, so the `latest_wins` and `soft_delete` defaults resolved and deleted nothing. Delete the key; the `DataSyncConfig` shape leaves with it. A sync is defined on its TARGET: a `mapping` (`targetObject`, `fieldMapping`, `mode`, `upsertKey`) whose `connectorSource` names the `rest` or `openapi` connector it pulls from, the read action and an optional timestamp `watermark`, with a `job` for the cadence. Its pull runs when a `job` drives it — a `job` whose `pull: { mapping }` names that mapping; the binding alone moves no rows. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
492492
| **fieldMappings** | `never` | optional | [REMOVED] `connector.fieldMappings` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever moved a value through a connector field mapping: nothing read `source`, `target`, `defaultValue`, `dataType`, `required` or `syncMode`. Delete the key; the `ConnectorFieldMapping` shape leaves with it. Map fields on the sync's TARGET instead: a `mapping`'s `fieldMapping` (`source` → `target`, with a `transform` the import path executes), which its `connectorSource` pulls through. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
493-
| **webhooks** | `never` | optional | [REMOVED] `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so it was never materialized into `sys_webhook` and never delivered, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection, which is materialized into `sys_webhook` and delivered on record events — note that doing so STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
493+
| **webhooks** | `never` | optional | [REMOVED] `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so the webhook dispatcher never read it and never delivered it, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection. The platform reads a declared webhook from the metadata registry and delivers it on record events; it writes no `sys_webhook` row for it (`sys_webhook` holds only the webhooks an organization authors at runtime). Note that declaring it STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
494494
| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. This edit left `os migrate meta` with the protocol-16.x conversions: apply it by hand, or run the tool on a release that still carries the conversion — https://objectstack.ai/docs/protocol-upgrade. |
495495
| **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration |
496496
| **connectionTimeoutMs** | `never` | optional | [REMOVED] `connector.connectionTimeoutMs` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — the platform never honoured it and cannot honour it where it was declared: a connector's outbound call is a WHATWG `fetch`, whose only cancellation surface is one `AbortSignal` over the whole operation, so nothing there observes the connection phase separately, and the value only ever travelled (onto the reported def and the materialization fingerprint) without ever bounding a connect. Delete the key. Use `requestTimeoutMs` for the deadline the platform does keep — it is applied as `resilientFetch`'s per-attempt timeout — and bound the connect phase at a connector provider or upstream gateway on a transport that can separate the phases. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |

‎packages/spec/scripts/liveness/proof-registry.mts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,14 +164,14 @@ export const HIGH_RISK_CLASSES: HighRiskClass[] = [
164164
id: 'webhook-materialization',
165165
label: 'Webhook materialization',
166166
summary:
167-
'a stack/connector-authored webhook is materialized into a dispatchable sys_webhook row on boot (#3461/#3489) — the authored value crosses manifest-decomposition → the ObjectQL registry (type `webhook`) → the bridge → engine.insert, and the bridge was first gated BEHIND the realtime/messaging dispatch guard, silently materializing nothing on a realtime-less boot despite green unit tests.',
167+
'a webhook declared in a stack\'s top-level `webhooks:` collection is dispatched from the metadata registry and writes no sys_webhook row (ADR-0131 D2; the boot seeder of #3461/#3489 is retired) — the authored value crosses manifest-decomposition → the ObjectQL registry (type `webhook`) → the `declaredWebhooks` reader the plugin binds on the data engine alone → the dispatcher, which delivers a matching record event with `object`→`object_name` applied. The proof boots WITHOUT realtime because the first bridge was gated BEHIND the realtime/messaging dispatch guard and silently bound nothing on a realtime-less boot despite green unit tests.',
168168
proofId: 'webhook-materialization',
169169
proofRef: 'packages/qa/dogfood/test/webhook-materialization.dogfood.test.ts#webhook-materialization',
170170
bound: true,
171171
// spec `object` → runtime `object_name` is the representative remap for the
172172
// whole authoring→dispatcher pipeline: its `live` status is only true
173-
// because the materializer runs (engine-only, independent of realtime), and
174-
// the proof boots WITHOUT realtime to pin exactly that.
173+
// because the declared-webhook reader is bound (engine-only, independent of
174+
// realtime), and the proof boots WITHOUT realtime to pin exactly that.
175175
ledgerBindings: [{ type: 'webhook', path: 'object' }],
176176
},
177177
{

‎packages/spec/src/conversions/registry.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10407,8 +10407,8 @@ const dashboardRefreshIntervalToRefreshIntervalSeconds: MetadataConversion = {
1040710407
* had no engine — nothing polled, counted consecutive failures or tripped a
1040810408
* breaker; `status` was read by nothing (the runtime publishes a COMPUTED
1040910409
* `state`, and participation is `enabled`); and a webhook nested in a
10410-
* connector was never registered as a `webhook` item, so it was never
10411-
* materialized into `sys_webhook` or delivered.
10410+
* connector was never registered as a `webhook` item, so the webhook
10411+
* dispatcher never read it or delivered it.
1041210412
*
1041310413
* A pure lossless delete, one notice per stripped key: none of the three ever
1041410414
* had an effect to preserve. In particular the nested `webhooks` are STRIPPED,

‎packages/spec/src/integration/connector-resilience-keys-retirement.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,23 @@ describe('connector resilience family retirement — the tombstones', () => {
116116
});
117117
}
118118

119+
it('the `webhooks` prescription teaches the declared-webhook model: read from the registry, no `sys_webhook` row', () => {
120+
// ADR-0131 D2: a declared top-level webhook is code. The dispatcher reads it
121+
// from the metadata registry and no `sys_webhook` row is written for it, so
122+
// a prescription that says it "is materialized into `sys_webhook`" sends the
123+
// author looking for a row that never appears.
124+
const issue = issueAt(ConnectorSchema.safeParse({ ...WELL_FORMED, webhooks: AUTHORED.webhooks }), 'webhooks');
125+
expect(issue!.message).toContain('reads a declared webhook from the metadata registry');
126+
expect(issue!.message).toContain('it writes no `sys_webhook` row for it');
127+
expect(issue!.message).not.toMatch(/materiali[sz]ed into `sys_webhook`/);
128+
expect(issue!.message).not.toMatch(/materiali[sz]/);
129+
// The D3 entry an upgrading author reads says the same.
130+
const entry = MIGRATIONS_BY_MAJOR[18]!.semantic.find((s) => s.id === 'connector-resilience-keys-retired');
131+
expect(entry!.replacement).toContain('reads from the metadata registry');
132+
expect(entry!.replacement).toContain('writes no `sys_webhook` row');
133+
for (const text of [entry!.replacement, entry!.reason]) expect(text).not.toMatch(/materiali[sz]/);
134+
});
135+
119136
it('the second carrier, the /meta door and `stack.connectors[]` refuse all three — with controls', () => {
120137
const door = getMetadataTypeSchema('connector');
121138
expect(door, 'no schema bound for `connector`').toBeDefined();

0 commit comments

Comments
 (0)