Repository navigation
feat(plugin-webhooks)!: declared webhooks are held per namespace, each holder dispatched and switched as its own (ADR-0150 child B, webhook family 2/3) - #32577
Conversation
…h holder dispatched and switched as its own (ADR-0150 child B, webhook family 2/3) webhook joins the namespace-scoped bare-name types, the package-scoped read types and the package-slotted store types. The declared-webhook reader answers one entry per (package, name) slot; the dispatcher subscribes each slot as declared:PKG/NAME (the environment's own keeps declared:NAME); each slot reads its own activation row, and a row no slot owns switches a two-holder name off, never on. The redelivery guard judges a delivery by the slot its ref_id names, and refuses a package-less ref of a two-holder name naming both. The activation door's webhook store is addressed by (package, name), its bare address answers a two-holder name 409 with the candidates (was 503), and POST /security/_activation/webhook/:ns/:name addresses one namespace's holder. Claude-Session: https://claude.ai/code/session_01Utp6CZu9RRx2o8sFUUpEPU Co-authored-by: Claude <noreply@anthropic.com>
… route-ledger note check:doc-authoring refuses an internal issue id in sibling-package string prose; the id moves to an adjacent comment. Claude-Session: https://claude.ai/code/session_01Utp6CZu9RRx2o8sFUUpEPU Co-authored-by: Claude <noreply@anthropic.com>
… row (149) Written by `check-route-ledger-census.mjs --fix`. Claude-Session: https://claude.ai/code/session_01Utp6CZu9RRx2o8sFUUpEPU Co-authored-by: Claude <noreply@anthropic.com>
…load at collection check:test-source-alias: the dispatcher's package domain reaches it through a dynamic import, so the first case to get there would load it inside its clocked window. Claude-Session: https://claude.ai/code/session_01Utp6CZu9RRx2o8sFUUpEPU Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 7 package(s): 39 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 16 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 74 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ea2a8ed8d4e2742031e556e06d38f72a0123ae60 && git checkout ea2a8ed8d4e2742031e556e06d38f72a0123ae60
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 994c7d673d3cebeccdf15993e190c2a0f03a2353 68145a92a397a1d451250386ab4acfa009780aeb && git checkout -B drift-repro 994c7d673d3cebeccdf15993e190c2a0f03a2353 && git merge --no-ff 68145a92a397a1d451250386ab4acfa009780aeb
node scripts/docs-audit/affected-docs.mjs --json 994c7d673d3cebeccdf15993e190c2a0f03a2353
|
|
epic:#31527#1 · os-bill · session_01Utp6CYGYEUhpi29FpjLmxT Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentMust-fix
Should-fix
Nits
The narrowing itself is right against the spec on Implemented-by: VERDICT: FAIL |
|
epic:#31527#1 · os-bill · session_01Utp6CYGYEUhpi29FpjLmxT Epic red-line read at
|
…priming pass reads, and webhook keeps the loader-level cases The boot priming pass walks DEFAULT_METADATA_TYPE_REGISTRY, which lists no webhook, so the pass registers none of webhook's rows. The per-type boot-pass cases now iterate UNBUNDLED ∩ that registry; webhook keeps the loader-level cases, and a CONTROL pins that the boot pass registers and writes no webhook row (round 2, R2-B). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CZu9RRx2o8sFUUpEPU
…her dispatched nor redelivered, and its ledger row is never re-attributed The declared-webhook reader read the registry's raw holders, so a DISABLED package's declaration of a name another package also declares was dispatched and redeliverable, unlike the merged listing (#25308). The reader now reads two slices (round 2, R2-A): - served (a disabled package is no holder): the slots dispatched, the bare read, the redelivery veto and the name guard; - every holder: the ledger's row ownership, and the activation door's `adoptPackageless` (new `holdersOf`), so a row naming no package under two holders is no holder's own whichever of them is enabled. The redelivery veto names a disabled package as the reason, and the remedies (the veto's switch, the declared-name conflict, the withheld-row warning) name the slot's namespace address `webhook/NS/NAME` when the slot names a package. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CZu9RRx2o8sFUUpEPU
…mDb into its own holder's slot, for one holder and two The metadata boot priming pass reads no webhook row; the protocol's boot hydration does, through hydrateOverlayIntoRegistry. Pinned for one holder (a row naming it or naming no package is its slot) and two holders in both registration orders (a row naming one holder is that holder's slot, and the other keeps its declaration) (round 2, R2-B). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CZu9RRx2o8sFUUpEPU
… delivered as `declared:NAME` still delivers as `declared:PKG/NAME` A declared webhook's reference names its package since this change, so a long name of a long package passed `sys_http_delivery`'s caps and the delivery was lost at insert (round 2, R2-C, epic red line 3). - `ref_id` is widened from 128 to 384: `declared:` + the longest package id the metadata store holds (255) + `/` + the longest name whose `declared:NAME` fitted 128 (119). A widened keyed varchar is the `safe` drift op; no stored row changes. - Measured: the `dedup_key` cap (191, index-bound) binds first. A webhook dedup key past it is a fixed-length sha256 digest of the same string, so the delivery stays at-most-once; a key that fits is never digested. Pinned on a real SqlDriver: a name at the old 128 boundary under one package holder is delivered, and the dedup key on both sides of the cap. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CZu9RRx2o8sFUUpEPU
…ks by (package, name), and the published read of a two-holder name is pinned - The report's webhook ledger row naming package P is an orphan when P no longer ships the name, whichever other package does, and covers only P's name for the suspected re-enablements; a sys_webhook row holding a name two packages declare is one finding per withheld holder. Its remedies name the namespace address. One holder: the findings are as before. - `GET /meta/webhook/NAME/published` of a name two packages hold answers 409 naming both, as the item read does; one holder answers as before. - The changeset carries the round-2 FROM → TO rows (round 2, R2-A, R2-C, R2-D). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CZu9RRx2o8sFUUpEPU
…caller's bound, and the ledgers learn round 2's new test surface Three gates the round-2 sweep turned red, each fixed here: - check:objectql-double-limit: the new boot-hydration test's `find` double applies the caller's `limit` after the filter, by presence; - check:engine-double-contract: its `findOne` double (opened with `assertEngineFindOnePredicate`) is recorded in the pinned ledger (`--write`); - check:test-source-alias: plugin-webhooks now aliases `@objectstack/service-messaging` to source (round 2's `ref_id` boundary pin), so its registry entry narrows to the three still unaliased. Its `console.warn` read is typed for the package's test typecheck (TS7031). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CZu9RRx2o8sFUUpEPU
|
epic:#31527#1 · os-bill · session_01Utp6CYGYEUhpi29FpjLmxT Contract reviewServed-tier: ① Derived judgmentsR2-A (a disabled package is no holder for what serves; a durable read counts every holder): met, with two gaps.
R2-B (stored webhook rows at boot): pins exist for the order-free cases only.
R2-C (delivery references): met at the per-record site, unpinned at the bulk site.
R2-D (remedies name the namespace address) and the upgrade-impact report by (package, name): met.
CONTROL (one-holder answers unchanged): holds, except as declared.
Moved answers declared and pinned.
Class sweep.
② Semver level
③ Boundary flags
④ Design judgmentMust-fix
Should-fix
Nit
Implemented-by: VERDICT: FAIL |
|
epic:#31527#1 · os-bill · session_01Utp6CYGYEUhpi29FpjLmxT Epic red-line read at
|
|
epic:#31527#1 · os-bill · session_01Utp6CYGYEUhpi29FpjLmxT Epic ruling R3 on #31699 PR 2 · 2026-11-12T10:15ZRecords ruled on: contract review 6820826396 and red-line read 6820837378 at Governing text: the epic's red lines 1 and 2; R2-A of 6816940784 (a durable read counts every holder); the maintainer's rule of 2026-11-12 09:41Z (「简单的问题是否可以后续单独立卡处理」): only correctness, red-line or security issues block a merge, and smaller items become follow-up cards. R3-A. Blocking, the one code fix: M1.
R3-B. Text corrections that ship in the same push (they are the shipped changeset, so they cannot wait for a later card). No re-review round for them:
R3-C. Optional in the same push, otherwise follow-up: M4's two pins ( R3-D. Follow-up cards, filed by the dev thread and nested under #31699:
Q2: accepted. Q3: no change to row (1)'s FROM. It is right as written (M5). Next. Push R3-A and R3-B together. The seat reads the M1 delta itself, with no full review round, and lands after #32497 per the chain order. |
…der of the name, and the two unpinned round-2 branches are pinned A delivery row is durable, so a `declared:NAME` reference (one written before package-scoped references, or the environment's own) is judged against every package holder of the name, a disabled package's included (`holdersOf`): two or more are refused naming each, where an enabled holder beside a disabled one was allowed as the enabled one's. One holder is judged by its declaration as before; a lone disabled holder's refusal now names it. Pinned in both registration orders, with controls for both enabled (refused), one holder (allowed; disabled, refused naming it). Two branches round 2 added without a pin get one each: the registry holding every holder of a name disabled serves nothing a metadata service lists of it, and the bulk-event path digests a dedup key past 191 as the per-record path does. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CZu9RRx2o8sFUUpEPU
…, six moved answers, and only real exports Text only, per epic ruling 6842178655 on contract review 6841104065: - S1: the Clause-② widening and the Dispatch bullet name the clash inside one namespace that the cold boot sealed: FROM neither holder dispatched TO both, the doors answering 409 naming both, so only a package disable or a ledger row written directly stops one. - S2: the exports paragraph names the one public export that moves (`declaredWebhookRefId(name, packageId?)`); the reader members, helpers and `WEBHOOK_LEDGER_ADDRESSING` are said to be internal. No export added. - S3: a namespace whose packages are all disabled answers 404 only when nothing else serves the name; beside another namespace's enabled holder it answers 409 `not-held-by-qualifier` naming that holder. - S4: the Clause-② line names (5), PostgreSQL or MySQL before `os migrate apply` with a reference over 128 characters. - The disabled co-holder bullet takes the last release as its one baseline (nit 5). Under it the redelivery of a `declared:NAME` delivery beside a disabled holder moves from allowed to refused, so the line names it as (6) and the BREAKING banner covers six callers. - Nit 4: the activation door bullet adds the one-holder row, 409 to written, where a row naming no package sits beside the holder's own. No test or source change. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgUJAvdCcvoqdsFGVz8
…ot pass reads; the webhook, slotted but not read by it, answers its package's row in either order The merge of main brought #32791's CONTROL, which iterates every unbundled member of PACKAGE_SLOTTED_STORE_TYPES and asks for the row the boot pass registered. The pass walks DEFAULT_METADATA_TYPE_REGISTRY, which lists no `webhook`, so for the webhook the bare read is the loader's own: 3 cells red on the merged tree (the package-less row saved last, on each host). - The CONTROL iterates BOOT_PASS_READS, the slotted types the pass reads, as #32521's and #32458's describes already do. - A webhook CONTROL pins what the webhook read answers: the pass registers none of its rows, and the bare read answers the package's row in either save order on every host. FROM (`webhook` out of the slotted set, on the merged tree): the row saved first. Ablating `webhook` out of the slotted set reds 3, the new CONTROL among them. - The changeset declares that moved answer as Clause-② (7), BREAKING for seven callers, per the epic seat's ruling, and cites the 18.3.0 tree read for the disabled co-holder bullet (badf24d). No source change. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgUJAvdCcvoqdsFGVz8
…older's exact and layered reads, as main's #32741 rules for a sealed type The merge of main brought #32741: a package-less row at a holder's exact address is declined for a sealed type, on every read and in every hydration state. Seven of this PR's webhook pins went red on the merged tree: - two holders: the plain exact reads of the environment's item (three stamps) and the layered read of a package-less stored row answered 409 ambiguous-in-closure here; each now answers the holder's own declaration, the package-less item declined, nothing picked between the holders. That FROM was never released (two holders are refused at install on main). - one holder: the exact read of the environment's item, the layered read's overlay, and the boot hydration of a package-less row. Measured on the merged tree with `webhook` out of the three type sets (core, metadata and objectql dists rebuilt, each set read back without it): the same declines, so they are #32741's own answers, not this PR's, and no Clause-② entry is added. The pins now pin those answers, each title naming its FROM. The changeset drops the exact/layered 409 it no longer answers, and says the boot hydration declines the package-less row too, while a row naming a package that does not declare the name still grafts onto the first-registered holder and of two holder-named rows only the last loaded applies (#32692). No source change. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgUJAvdCcvoqdsFGVz8
|
epic:#31527#1 · os-sales · session_01L6bhgUJAvdCcvoqdsFGVz8 Contract-review delta request · triage#1 · 2026-11-13T03:32ZHead-sha: For triage#1, who rendered contract review 6841104065 (PASS with should-fix) on Commits
Where each item landed (the changeset
Gates and tests on
Open questions: dev report 6842933276 on #31699. They are: GET /meta's step 2 (unmeasured; the seat ruled it goes to the p2 follow-up unless this delta asks for the probe), #32692's residue on this head, and (6)'s FROM. Generated by Claude Code |
|
epic:#31527#1 · os-sales · session_01L6bhgUJAvdCcvoqdsFGVz8 Duplicate: this comment was sent twice by mistake. The delta request is comment 6842991779 above; this one carries nothing to review. Generated by Claude Code |
|
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH Contract-review-claim: session_01Q4EjjMnyLqJdzuktEmRPXH · head c0500c6 · 2026-11-13T03:37Z Delta review of the should-fix S1–S4 from the PASS on |
|
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentMust-fix: none. My five should-fix items
New should-fix
Per file in the delta. The three test files agree with the code they cite and with the rules Implemented-by: VERDICT: PASS Generated by Claude Code |
…very under two enabled holders, and the Dispatch bullet's baseline Contract-review delta 6843291373 S6 and its nit, ruled into this PR by the epic seat. Text only. - Clause-② (8): on the last release (badf24d, 18.3.0, no one-holder rule) a redelivery of a declared:NAME delivery of a name two enabled packages hold was judged as the first registered holder's declaration; it is now refused 409 DELIVERY_NOT_ELIGIBLE naming both. BREAKING now lists eight callers; (6) and (8) share one fix. - Redelivery bullet: the two-holder refusal gets its FROM for (8) and (6). - Dispatch bullet: the "neither subscribed" FROMs are main's since #31677; the last release dispatched the first registered holder alone. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgUJAvdCcvoqdsFGVz8
|
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH Contract-review-claim: session_01Q4EjjMnyLqJdzuktEmRPXH · head 68145a9 · 2026-11-13T04:49Z Delta review of S6 and its nit (changeset text only), by the same reviewer, at |
|
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentMust-fix: none. Should-fix: none open.
The changeset now agrees, sentence by sentence, with the code it describes at this head. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
epic:#31527#1 · os-sales · session_01L6bhgXQnNkXTT1Wu7i7Y3i ACCEPT · 2026-11-13T05:12ZHead-sha:
Land: ready + auto-merge. Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 43370657646 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Requested by Bill · project thread
epic:#31527#1 · os-bill · session_01Utp6CZu9RRx2o8sFUUpEPU
维护者速读
ref_id改为declared:PKG/NAME(环境自己的声明仍是declared:NAME);新增限定启用门POST /security/_activation/webhook/:ns/:name;两个持有者时,裸读/meta/webhook/NAME、/published和裸启用门都答 409 并列出两者(启用门原来答 503)。第 2 轮:被禁用的包不算服务中的持有者(不派发、不可重投递,但账本归属仍按全部持有者算);ref_id上限 128 → 384,dedup_key超过 191 时用 sha256 摘要,原来能投递的名字仍能投递;升级影响报告按(包,名)读。第 3 轮:不带包的旧投递(declared:NAME)重投递时按全部持有者计数,停用包也算,两个及以上即拒并逐个点名;changeset 与正文的几处说法按实测改正。第 4 轮(triage 复审 PASS 后的应修项,及合入 main):只改文字,changeset 写明同命名空间冷启动封存的冲突两边都派发(S1),只列真实的公开导出(S2),全停用命名空间旁有他命名空间的启用持有者时答 409(S3),Clause-② 记七处;合入 main 后 metadata: a stored row bound to the host package is counted as two holders (host package + environment) in the bare action read's loader judgement (regression of #32560 / PR #32719) #32791 的 CONTROL 只跑启动 pass 读的类型,另加一个 webhook CONTROL(纯测试)。第 5 轮(delta 复审 PASS 后的 S6 与一条 nit):只改文字,Clause-② 记八处;Dispatch 一条写明「两边都不订阅」是 webhooks, connectors, email templates on (package, name) (ADR-0150 child E5) #31677 以来 main 的答案,18.3.0 派发先注册者。DatabaseLoader保存的同名 webhook,不点名包读历史、比较、回滚改为 409 列出两者;第四处随合入 main 的 fix(objectql, metadata-protocol)!: by-name doors judge a name's holders over the served set, so a disabled package's item is not a holder (ADR-0150 D3) #32497 而来:只有停用包持有的 webhook 名,MetadataFacade.get/getEntry与SchemaRegistry.resolveItem由停用包的声明改为undefined,exists由 true 改为 false):两个持有者的裸/meta/webhook/NAME(及其/published)由先注册者改为 409;按ref_id = 'declared:NAME'选投递行的消费方,升级后的新行要改为匹配declared:PKG/NAME。ref_id加宽在已有的 PostgreSQL/MySQL 库上要os migrate apply(safe的widen_varchar),执行前,包 id 加名字超过 118 字符的单持有者 webhook 会在插入时被拒(第五处,裁定 Q2 已接受)。第六处:一启一停两个持有者时,不带包的旧投递declared:NAME的重投递,由按启用者判(上次发布 18.3.0 的树badf24dff,及 fix(objectql, metadata-protocol)!: by-name doors judge a name's holders over the served set, so a disabled package's item is not a holder (ADR-0150 D3) #32497 之后的 main)改为 409 列出两者。第七处:MetadataManager.get不点名包读一个存有一个包的行和一条 package-less 行的 webhook 名,由先存的那行改为包的那行,两种顺序一样。第八处:两个都启用的持有者时,declared:NAME的重投递由按先注册者判(上次发布 18.3.0,当时没有一持有者规则)改为 409 列出两者。滚动升级期间,旧节点重投递新形状的行会 fail closed(拒绝,不会发错持有者);跨升级同一 webhook 的投递之间不保证顺序。只有一个持有者且其包已启用时,除新投递的ref_id、未迁移库上的第五处、启用门在持有者自己的行旁有 package-less 行时由 409 改为写入、以及第七处外,所有答案不变。回滚即 revert。packages/plugins/plugin-webhooks/src/declared-webhooks.ts(packageScopedRegistryOf的 served / every、slotsOf、webhookSlotSwitch、DeclaredWebhookSource),packages/runtime/src/domains/catalog-activation.ts(resolveQualifiedWebhook),packages/plugins/plugin-webhooks/src/redeliver-guard.ts(judgeDeclared,第 3 轮)。pnpm --filter @objectstack/plugin-webhooks exec vitest run src/declared-webhooks.test.ts与pnpm --filter @objectstack/runtime exec vitest run src/domains/catalog-activation-door.test.ts,应全绿;"one declaration per (package, name) slot"(含 "a disabled co-holder")和 "a declared webhook two packages declare" 两节逐条钉住。Part of #31699
Clause-②: yes (narrowing) — eight answers move for a caller that worked before. (1) The bare
GET /meta/webhook/NAMEof a name two packages hold (a disabled package holds none here, #32497), and its published readGET /meta/webhook/NAME/published(which asks the bare item read for the item's package), are refused409 RESOURCE_CONFLICT(ambiguous-in-environment, naming both), where they served the first registered package's declaration; before this change that state existed only for a clash the cold boot sealed with a warning. (2) A declared webhook a package ships is subscribed, and its new deliveries are written, underref_id = declared:PKG/NAME, where they carrieddeclared:NAME; a consumer that selects deliveries byref_id = 'declared:NAME'no longer matches the ones written after the upgrade. (3) The history, diff and rollback of a webhook name two packages saved throughDatabaseLoader(MetadataManager.getHistory/diff/rollback), naming no package, are refused409 RESOURCE_CONFLICT(ambiguous-in-environment, naming both), where they answered every version of the one row both saves wrote, and a rollback to the version of a package removed since is refused409 RESOURCE_CONFLICT(not-held-by-qualifier, naming it), where it wrote that body into the remaining holder's row: #32351's rules reachwebhookwithPACKAGE_SLOTTED_STORE_TYPES. (4) For a webhook name only disabled packages hold,MetadataFacade.get/getEntryandSchemaRegistry.resolveItem(@objectstack/objectql) answerundefinedwhere they answered the disabled package's declaration, andMetadataFacade.existsanswers false where it answered true: #32497's rule for the package-scoped readers reacheswebhookwithPACKAGE_SCOPED_READ_TYPES, and the by-name door already answered404. (5) On an existing PostgreSQL or MySQL database beforeos migrate apply, a one-holder webhook whose package id and name together pass 118 characters, so that itsdeclared:PKG/NAMEreference passes 128, has its new deliveries refused at insert (the event lost, logged once), where they were delivered asdeclared:NAME; onceos migrate applywidenssys_http_delivery.ref_idto 384 they are delivered. (6) A redelivery of adeclared:NAMEdelivery of a name one enabled and one disabled package hold is refused409 DELIVERY_NOT_ELIGIBLEnaming both, where it was judged as the enabled holder's declaration (the last release's dispatcher reader withheld the disabled holder, asmaindoes again since #32497). (7) The by-name read naming no package throughDatabaseLoader(MetadataManager.get/getDiagnosed) of a webhook name stored as one package's row and a package-less row answers the package's row in either save order, where it answered the row saved first: the store keeps a row per(package, name)(PACKAGE_SLOTTED_STORE_TYPES), and the boot pass, which registers no webhook row, does not decide it. (8) A redelivery of adeclared:NAMEdelivery of a name two enabled packages hold is refused409 DELIVERY_NOT_ELIGIBLEnaming both, where it was judged as the first registered holder's declaration (the last release,badf24dffat 18.3.0, had no one-holder rule, so two packages could each declare a webhook name, and its dispatcher reader answered the first registered one). Widened beside them: the engine's package door, item seam and cold-boot seal admit a second package holder of a webhook name that declares another namespace, and the dispatcher, which subscribed neither holder of such a name, subscribes each one; a clash inside one namespace that install refuses but the cold boot sealed with a warning (two co-owners of one namespace, two packages of a shareable namespace, or a package with no namespace beside another holder), FROM neither holder dispatched TO both dispatched, each under its own(package, name), while the bare and namespace doors answer409naming both, so only a package disable or a ledger row written directly stops one; the bareGET /meta/webhook/NAMEof a name one enabled and one disabled package hold, where the disabled one registered first, answers the enabled holder's declaration (200) where it answered404; and the unscoped plural reads (MemoryLoaderandMetadataManagerloadMany/loadManyKeyed) of a webhook name two packages saved answer both, where they answered the one saved last.Metadata-protocol: added (a second package's webhook declaration of a name another namespace holds is now accepted, where it was refused
422 NAMESPACE_CONFLICT; no key, value or requirement an author writes against is refused or tightened)Skills-docs: none —
content/docs/automation/webhooks.mdx§3.0 is updated in this PR (theref_idshape and the namespace door beside the bare one; seat-approved addition, 04:03Z), and §4.3'sdedup_keysentence (ruling R3, Q4); no published skill teaches a declared webhook's name scope, itsref_idor the activation door (grep -rn "declared:\|_activation/webhook\|sys_http_delivery\|ref_id" skills/: one hit,skills/objectui/guides/data-integration.md:79, a comment about published keys, unrelated).Narrowing: yes, first-party callers 0 for (1) to (6) and (8) (measured); (7) below. (1) No first-party package pair holds one webhook name across two packages; the full dogfood battery, which boots the showcase, CRM and every example app, is green with the refusal on. (2) No in-tree consumer selects
sys_http_deliveryby a literaldeclared:NAME: the four dogfood suites that read a delivery'sref_idare moved to the new shape in this PR (upgrade-18x-harness,webhook-materialization,wall-declared-webhook-deployment-wide,bare-name-one-holder-flow-family), and the CLI's 18.x ownership ceremony keeps writingdeclared:NAME(seat ruling Q4), which the redelivery guard still reads. (3) No first-party caller readsMetadataManagerhistory: the REST/meta/:type/:name/history,/rollbackand/diffdoors go through the protocol's own repository (rest-route-ledger.ts:277, as #32351 states), and a grep of non-test sources finds no caller ofMetadataManager.getHistory/rollback/diffoutsidemetadata-manager.ts. (4)MetadataFacadeis never constructed in-tree, and the one non-test caller ofSchemaRegistry.resolveItemoutside the facade, the protocol's by-name read (protocol-meta-item-reads.ts:577), already answered404for that name (grep, as #32497's changeset states for the other types). (5) is an operator step,os migrate applywith the upgrade. (6) and (8) need a name two packages hold, which no first-party pair does, as in (1). (7): no non-test source nameswebhooktoMetadataManager.get/getDiagnosed(grep), and the dispatcher, the doors, the guards and the report read the registry ormetadataService.list('webhook'); the protocol's generic by-name read callsgetDiagnosedat its step 2 for every type (protocol-meta-item-reads.ts:1056), and whether a bareGET /meta/webhook/NAMEreaches it in this shape is not measured (round 4, open question 1).This is the second of three PRs for #31699: email_template (#32214, merged), webhook here, then connector. Only
webhookjoinsNAMESPACE_SCOPED_BARE_NAME_TYPEShere (the flow joined it with #31696, merged).Seat rulings this PR implements
Comment 6813532941:
declared:NAME, pinned.claude/project-thread-3giljf, a new draft PR,Part of #31699.Round 2, comment 6816940784 (after CR 6816920433 and red-line 6816929999):
origin/mainmerged (no rebase). fix(metadata)!: the boot priming pass hands an unbundled slotted type's row on stamped with its row's package, so a save never inserts a package-less row #32548's boot-pass pins iterateDEFAULT_METADATA_TYPE_REGISTRY ∩ PACKAGE_SLOTTED_STORE_TYPES; a webhook CONTROL pins that the boot pass registers and writes no webhook row. How a stored package-scoped webhook row loads at boot is measured and pinned for one and two holders.webhookis not added to the type registry.declared:NAMEfitted still delivers:ref_id's cap is widened, thededup_keycap was measured to bind first, and a key past it is digested.(package, name); the remedies name the namespace address; row (1) names/published, pinned.Round 3, comment 6820856607 (after CR 6820826396 and red-line 6820837378):
holdersOf(name), a disabled package's included; with two or more holders it is refused naming each. Commit6b72041b.17cec0fd0). M3: the changeset's package-less row sentence says what boot does and names metadata-protocol: the boot hydration binds a stored webhook row of a name two packages hold by registration or load order; key its artifact lookup by (package, name) (ADR-0150 D3/D4) #32692. M5: themeta-qualified-item-read.test.tsdocblock and Q3 below say main answers 200 with the first registered declaration. S3: the un-migrated database condition is a plain FROM → TO. Q4:webhooks.mdx§4.3. Commit77becda1.coveredby(package, name)). Commits6b72041b,48b4cba4.(package, name); blocks objectql: narrow the phase-1 one-holder refusal from environment to namespace for webhook, connector and email_template (ADR-0150 child B, webhook family) #31699's close-out, lands before PR 3), metadata-protocol: the generic by-name item read counts a disabled package as a holder for every package-scoped type but action (ADR-0150 D3) #32694 (the protocol's generic by-name read counts a disabled holder, every package-scoped type butaction; nested under metadata-protocol: a name held only by stored rows of two packages, one disabled, answers by save order, and /layers takes the first composite when the environment holds its own item (ADR-0150 D3) #32354), plugin-webhooks: a ledger row naming a package that no longer declares a webhook switches the remaining holder, and the 18.x ownership ceremony falls back to the first-listed holder (ADR-0150 D4/D8) #32695 (S1 and S4).Round 4, comment 6842178655 (after triage#1's contract review 6841104065, PASS with should-fix):
declaredWebhookRefId; an all-disabled namespace answers409 not-held-by-qualifierbeside another namespace's enabled holder; the Clause-② line names (5); the door bullet gains the one-holder409→ written row; the disabled co-holder bullet takes the last release as its one baseline, and under it (6). Commit40c798157.badf24dff). Commitsa5c37e97c,26a846fe7.Round 5, comment 6843291373 (triage#1's delta review on
c0500c639, PASS with one should-fix and one nit; the epic seat ruled both into this PR, text only):badf24dff(18.3.0, no one-holder rule) adeclared:NAMEredelivery under two enabled holders was judged as the first registered holder's, and it is now refused naming both. Declared as (8), the Redelivery bullet gains that FROM, and BREAKING lists eight callers, (6) and (8) sharing one fix. Commit68145a92a.neither subscribedFROMs aremain's since webhooks, connectors, email templates on (package, name) (ADR-0150 child E5) #31677, and that 18.3.0 dispatched the first registered holder alone. Same commit.Two slices (R2-A, corrected by R3)
slotsOfslots,list()(the dispatcher)declaredWebhookNamed(the reader's bare resolver: the bare activation door, the name guard)declaredWebhookAt(the redelivery veto of adeclared:PKG/NAMEdelivery)bindDeclaredWebhookNameGuard404declared:NAMEdelivery (judgeDeclared, the reader'sholdersOf)webhookSlotSwitch(ledger row ownership),reportUnownedRowsadoptPackageless(the reader's newholdersOf)backfillActivationLedgerAtBoot)GET /meta/webhook/NAME,/published,MetadataFacade.get281722eb4)/publishedas for that holder alone; a lone disabled holder answers404(the facadeundefined). Before the merge it counted every holder (409naming both), #32694's premise for the webhookAfter the merge of #32497,
SchemaRegistry.packageScopedHoldersOfanswers the served holders unless asked otherwise, so the plugin's every-holder reading asks for every holder (includeDisabledPackages: true,91acdcc3f); on the merged tree before that commit, 8 pins were red ("[FROM delivered TO withheld]", "[FROM A's switch TO no holder's]" and "[FROM allowed TO refused]", each in both orders; CONTROL one holder disabled; every holder disabled).What changes (FROM → TO)
installPackage,registerItem,sealBareNameHolders): a second package of another namespace declares a webhook name422 NAMESPACE_CONFLICTbase/system/sys, a built-in name422422, same envelope and sentenceDeclaredWebhookSource.list,AutoEnqueuer), a name two packages holdmainsince #31677; 18.3.0badf24dff, before the one-holder rule: the first registered holder alone)base/system/sys, a package with no namespace beside another holder)409naming both, so only a package disable or a ledger row written directly stops oneref_id= partition key, a package's declarationdeclared:NAMEdeclared:PKG/NAMEdeclared:PKG/NAME"declared:NAMEdeclared:NAMEGET /meta/webhook/NAME, two holders409ambiguous-in-environmentnaming bothGET /meta/webhook/NAME/published, two holders409naming both (it asks the bare item read for the package)meta-qualified-item-read"bare … and …/published: 409 naming both", one-holder CONTROLGET /meta/webhook/NAME?package=IDgetItemPACKAGE_SCOPED_READ_TYPES): that package's own declarationsys_metadata_activation,webhook)(package_id, name)row firstPOST /security/_activation/webhook/:name, two holders503 SERVICE_UNAVAILABLE(the reader's error carried norefusal)409 RESOURCE_CONFLICTwith every candidate, nothing writtenadoptPackageless)409 RESOURCE_CONFLICTnaming both rows, nothing written (the name-addressed store)(package, name)(WEBHOOK_LEDGER_ADDRESSING)webhook/:ns/:nameswitches exactly that namespace's holder" (A4)POST /security/_activation/webhook/:ns/:name400(shape):ns's packages; two co-owners409 ambiguous-in-namespace; none of:nsholds it409 not-held-by-qualifier; unknown namespace or name404alpha/qa_shared_hook200400400declared:PKG/NAMEdeclared:NAME, one enabled holderdeclared:NAME, one holder, disabled409 DELIVERY_NOT_ELIGIBLE)declared:NAME, one enabled and one disabled holderbadf24dff, whose listing withheld the disabled holder;mainsince #32497 the same)409 DELIVERY_NOT_ELIGIBLEnaming each holder, sent to neither (R3-A)badf24dff, which had no one-holder rule;mainsince #31677: reachable only by a sealed clash, refused naming both)409 DELIVERY_NOT_ELIGIBLEnaming each, sent to neitherdeclared:/NAME,declared:PKG/A/B)declared:PKG/NAMEPKG/NAMEexists, so it refuses (fail closed)sys_webhookwrite of a name two packages declare)409409MetadataManager's in-memory webhook store(package, name)declared:PKG/NAMEdeliverybadf24dff): the enabled holder alone dispatched and resolved, its listing withholding the disabled one;mainbetween #31677 and #32497 dispatched neither, since #32497 the enabled one (round 1 here: the disabled one dispatched and redeliverable)GET /meta/webhook/NAME,/publishedandMetadataFacade.get404when it is the disabled one; re-enabled, the first registered served (measured: this head withwebhookout of the three type sets, as main has them, 4 red, the objectql and runtime pins in both orders). Round 3's head before the merge of #32497 (2200b8c9):409naming both, the disabled holder counted/publishedanswers as for that holder alone; re-enabled,409naming bothmeta-qualified-item-read"[FROM the first registered, or 404 when it is the disabled one, TO the enabled holder's]", both ordersGET /meta/webhook/NAME;MetadataFacade.get/getEntry/existsandSchemaRegistry.resolveItem404; the library reads the disabled package's declaration,existstrue (measured the same way;2200b8c9the same)404;undefined,existsfalse404, nothing written409 RESOURCE_CONFLICTnot-held-by-qualifiernaming that holder, nothing written (S3)sys_http_delivery.ref_idcapdeclared:+ 255 package id +/+ 119 name)os migrate apply, one holder whose package id and name pass 118 charactersdeclared:NAMEdeclared:PKG/NAME(the event lost, logged once); after the migration, delivereddedup_keypast 191sha256:digest of the same string; a key that fits unchangedcoveredby(package, name)(S2)sys_webhookrow of a name two packages declare_activation/webhook/NAME_activation/webhook/NS/NAMEwhen the slot names a package; the environment's own keeps the bare addressGET /meta/webhook/NAME?package=ID(and the qualified path), the environment's package-less item (registered, or a stored row hydrated, stamped none,''orsys_metadata), two holders7f68e294b:409ambiguous-in-closurenaming both)?layers=true&package=ID, a package-less stored row, two holders7f68e294b:409naming both)DatabaseLoader's webhook rows (sys_metadata)package_idNULL; a second package's save wrote over the first's(package, name),package_idfilledsys_metadata_history.package_idof a webhook saveMetadataManager.getHistory/diff/rollbackof a webhook name two packages saved, naming no package[3, 2, 1]); a rollback written into it409 RESOURCE_CONFLICTambiguous-in-environmentnaming both, nothing written; naming a package, its own versions and rowwebhook409not-held-by-qualifiernaming it, nothing writtenmigrateDatabaseLoaderRowPackages, a webhook loader row whose body names a packagepackage-less)webhookMemoryLoaderandMetadataManagerunscoped plural reads (loadMany,loadManyKeyed) of a webhook name two packages savedwebhookout ofPACKAGE_SLOTTED_STORE_TYPES, 3 red: the set pin and both orders; CONTROL green)metadata-manager-keyed-plural-read(#32687's per-type cases, which now namewebhook), both orders; CONTROL one holderMetadataManager.get/getDiagnosednaming no package, a webhook name stored as one package's row and a package-less rowwebhookout of the slotted set)database-loader.test.ts"CONTROL webhook, slotted but not a type the pass reads…", 2 orders × 3 hostsFile surface
Claim (seat Q3):
packages/plugins/plugin-webhooks/src/declared-webhooks.ts,auto-enqueuer.ts,redeliver-guard.ts;packages/runtime/src/domains/catalog-activation.ts; the three sets (packages/core/src/package-scoped-resolution.ts,packages/objectql/src/package-scoped-read.ts,packages/metadata/src/package-slotted-store.ts) andpackages/objectql/src/bare-name-one-holder.ts's doc; their tests; the dogfood files; the changeset.Seat-approved additions:
packages/runtime/src/dispatcher-plugin.ts(the four-segment mount) andpackages/runtime/src/route-ledger.ts(its server-only row): Claim amendment 6813666631.content/docs/automation/webhooks.mdx§3.0, scoped to the two sentences this PR makes false (theref_idshape, and the namespace door beside the bare one): approved by the seat at 04:03Z. §4.3'sdedup_keysentence: ruling R3, Q4.packages/metadata/src/loaders/database-loader.test.ts(R2-B);packages/objectql/src/webhook-stored-row-boot-hydration.test.ts(new, R2-B's boot path);packages/services/service-messaging/src/objects/http-delivery.object.tsandpackages/plugins/plugin-webhooks/vitest.config.ts(R2-C: the cap, and a source alias so the plugin's tests read the service's source rather than a staledist; the alias registry inscripts/check-test-source-alias.mjsnarrows to match, as its gate requires);packages/plugins/plugin-security/src/upgrade-impact-report.tsand its test (R2-D);packages/runtime/src/domains/meta-qualified-item-read.test.ts(R2-D,/published). Gate-forced:scripts/engine-double-contract.pinned.jsonrecords the new boot-hydration test'sfindOnedouble (--write).packages/objectql/src/package-scoped-exact-qualifier.test.ts,packages/metadata/src/metadata-history.test.ts,packages/metadata/src/migrations/bind-database-loader-row-packages.test.ts.281722eb4(origin/main2f80f2897, with fix(objectql, metadata-protocol)!: by-name doors judge a name's holders over the served set, so a disabled package's item is not a holder (ADR-0150 D3) #32497 and fix(metadata)!: the metadata service's own reads keep every locale of a package's email template and every package's item of a by-package type #32687) and491e70bc5(origin/main1ec547fc6, with feat(core, objectql, metadata, metadata-protocol, plugin-security): a permission-set name is held once per namespace, judged by one predicate (ADR-0150 child B) #32742):packages/metadata/src/metadata-manager-keyed-plural-read.test.ts, whose type list fix(metadata)!: the metadata service's own reads keep every locale of a package's email template and every package's item of a by-package type #32687 pins against the live slotted set, now nameswebhook(05499a374). The plugin fix (91acdcc3f) and the new pins are in files already listed.9c5f3355(objectql: narrow the phase-1 one-holder refusal from environment to namespace for flow (ADR-0150 child B, flow family) #31696, the flow's narrowing): six test files whose "not narrowed" example was the webhook now name the connector (2200b8c9): corepackage-scoped-resolution.test.ts, metadatametadata-manager-action-slots.test.tsandmetadata-manager-match-endpoint.test.ts, objectqlpackage-scoped-e8-readers.test.ts,package-scoped-resolver.test.tsandregistry-bare-name-one-holder-flow-family.test.ts.Tests
declared-webhooks.test.ts, new describe "one declaration per (package, name) slot (ADR-0150 D3/D4/D5, objectql: narrow the phase-1 one-holder refusal from environment to namespace for webhook, connector and email_template (ADR-0150 child B, webhook family) #31699)", 12 pins.auto-enqueuer-disabled-report.test.ts: the CONTROL expects themy_appslot's ref.catalog-activation-door.test.ts, new describe "a declared webhook two packages declare (ADR-0150, objectql: narrow the phase-1 one-holder refusal from environment to namespace for webhook, connector and email_template (ADR-0150 child B, webhook family) #31699)", 7 pins.meta-write-org-scope.test.ts: its unmapped type is nowqueue, becausewebhooknow has a package-scoped resolver.WIRED_SINCEgainswebhook, plus a webhook/metadescribe), ai-family (SWITCHEDgainswebhook), flow-family (the switch pin, "[objectql: narrow the phase-1 one-holder refusal from environment to namespace for webhook, connector and email_template (ADR-0150 child B, webhook family) #31699 PR 2, FROM 422 TO admitted]", and the CONTROL now the connector alone).bare-name-one-holder-flow-family.dogfood.test.ts: the cold-boot warning names only the connector; a nested describe boots two holders ofqa_shared_hookand pins: each slot is off as shipped; the bare door answers 409;alpha/qa_shared_hookanswers 200, and alpha alone is dispatched, withref_id declared:com.qa.bare.alpha/qa_shared_hook, tohttps://hooks.example/alpha; theta installs as a third slot.upgrade-18x-harness,webhook-materializationandwall-declared-webhook-deployment-wideread the newref_id(the harness's ceremony re-source at line 242 keepsdeclared:NAME, Q4).Round 2:
declared-webhooks.test.ts, nested describe "a disabled co-holder (objectql: narrow the phase-1 one-holder refusal from environment to namespace for webhook, connector and email_template (ADR-0150 child B, webhook family) #31699 round 2)", every two-holder pin run in both registration orders: "[FROM delivered TO withheld]" (dispatch, the bare resolver, the redelivery veto naming the disabled package,holdersOf, and re-enabling resumes delivery), "[FROM A's switch TO no holder's]", the name guard judging the served set, CONTROL both enabled, CONTROL a lone disabled holder; beside it, the remedies pin and its CONTROL (the environment's own declaration keeps the bare address).catalog-activation-door.test.ts: "[FROM adopted TO beside it]" (bare and namespace address), CONTROL every holder is one (adopted, D8), "B disabled and alone" (404, nothing written).meta-qualified-item-read.test.ts: the bare read and/publishedof a two-holder name answer409naming both, the namespace address reads one; CONTROL one holder (200;/published404, nothing published).database-loader.test.ts:BOOT_PASS_READS = UNBUNDLED ∩ DEFAULT_METADATA_TYPE_REGISTRYdrives fix(metadata)!: the boot priming pass hands an unbundled slotted type's row on stamped with its row's package, so a save never inserts a package-less row #32548's boot-pass cases; "the types are taken from the tree" pins that the intersection is every slotted type butwebhook; CONTROL webhook: the boot pass registers none of its rows and writes nothing.webhook-stored-row-boot-hydration.test.ts(new): a realSchemaRegistryandObjectStackProtocolImplementation.loadMetaFromDb. CONTROL one holder: a row naming it, or naming no package (D8), is its slot, and no environment holder appears. Two holders, both orders: a row naming one holder is that holder's slot, and the other keeps its shipped declaration.webhook-delivery-real-sql-driver.test.ts, describe at the old 128 boundary, one holder (declared:NAMEexactly 128): "[FROM lost TO delivered]" (ref_idstored whole asdeclared:PKG/NAME, the dedup key asha256:digest), CONTROL a short name keeps the plain key, and the 191 / 192 boundary unit.upgrade-impact-report.test.ts: "[FROM one TO each]", "[FROM no finding TO orphan]", CONTROL one holder.Round 3:
declared-webhooks.test.ts, in the disabled co-holder describe: "[FROM allowed TO refused]" (A enabled, B disabled, adeclared:NAMEdelivery refused naming both), both orders; CONTROL both enabled (refused naming both), both orders; CONTROL one holder (allowed; disabled, refused naming it). M4: "every holder disabled: the item a metadata service lists of the name is not dispatched" (re-enabled, dispatched).auto-enqueuer.test.ts, M4: "a dedup key past 191 is the digest on the bulk path too; one that fits is unchanged".upgrade-impact-report.test.ts, S2: another package's switch of a name covers none of the orphan's package's names, and a row naming no package does (D8).Round 3, from the main merge
a0131ec1d(seat order, 14:41Z: probe each rule the merge brought that is keyed by the type sets this PR addswebhookto, under two webhook holders; pin what moves, record what does not):package-scoped-exact-qualifier.test.ts, new describe "[objectql: narrow the phase-1 one-holder refusal from environment to namespace for webhook, connector and email_template (ADR-0150 child B, webhook family) #31699 PR 2] a package-less webhook of a name two packages hold is no holder's own, plain and layered (fix(metadata-protocol, cli)!: a package-less item of a name two packages hold is refused at both exact addresses for every narrowed type, and the layered read judges its exact address (ADR-0150 D4) #32566)", 9 pins: the premise; "[FROM each holder's overlay TO 409]" (the layered read of a package-less stored row); CONTROL one holder (the row is the overlay over the holder's code layer); CONTROL a row bound to alpha; the plain exact read of the environment's item refused naming both, three stamps, each list serving its own; CONTROL one holder, and an item bound to alpha; CONTROL the stored package-less row declined at the plain read.metadata-history.test.ts, new describe "[objectql: narrow the phase-1 one-holder refusal from environment to namespace for webhook, connector and email_template (ADR-0150 child B, webhook family) #31699 PR 2] a declared webhook's history is per package (metadata: history and rollback for a slotted type are keyed by (type, name), so rolling back one package's version can overwrite the other package's row (ADR-0150 D4) #32351)", 4 pins: the premise; "[FROM no package TO the package]"; "[FROM one row, every version TO a row each, 409]"; "[FROM B's body written TO 409]". The describe names the webhook, so taking it out of the set turns these red rather than dropping the 16 cases metadata: history and rollback for a slotted type are keyed by (type, name), so rolling back one package's version can overwrite the other package's row (ADR-0150 D4) #32351's loop derives for it.bind-database-loader-row-packages.test.ts: "[objectql: narrow the phase-1 one-holder refusal from environment to namespace for webhook, connector and email_template (ADR-0150 child B, webhook family) #31699 PR 2, FROM left TO bound] a webhook loader row whose body names a package is bound to it; a package-less one is left".Round 3, from the main merge
281722eb4(#32497: a by-name door judges a name's served holders; #32687: the unscoped plural reads answer each package's item):declared-webhooks.ts(91acdcc3f): the every-holder reading asks the registry for every holder (includeDisabledPackages: true), since its holder read now answers the served ones. On the merged tree before that commit, 8 pins were red: "[FROM delivered TO withheld]", "[FROM A's switch TO no holder's]" and "[FROM allowed TO refused]", each in both orders; CONTROL one holder disabled; every holder disabled.MetadataFacade.getanswer the enabled holder's; re-enabled,409naming both); "[FROM the disabled body TO undefined, library]" (a lone disabled holder: the protocol read404as before;MetadataFacade.get/getEntryandSchemaRegistry.resolveItemundefined,existsfalse).meta-qualified-item-read.test.ts: "[FROM the first registered, or 404 when it is the disabled one, TO the enabled holder's]", both orders (the item read serves the enabled one,/publishedanswers as for it alone; re-enabled, both409).metadata-manager-keyed-plural-read.test.ts: fix(metadata)!: the metadata service's own reads keep every locale of a package's email template and every package's item of a by-package type #32687's per-type cases run forwebhook(two packages' saves answer both, in the order first saved, both orders; CONTROL one holder).Round 3, from the main merge
2173fae50(origin/mainbfbcd81fb, with #32676: the boot priming pass primes every holder of a name of a type stored per(package, name)):database-loader.test.ts, fix(metadata)!: the boot priming pass primes every holder of a name two packages hold, of a type stored per (package, name), so a bare read of that name is refused after the pass as without it #32676's describe: its boot-pass cases iterated the live slotted set, so 9 went red forwebhookon the merge ref. The pass walksDEFAULT_METADATA_TYPE_REGISTRY, which does not listwebhook, so it primes none of its rows, on main as here; every read after it answers as without it, nothing dropped and nothing picked by order. The cases now run forBOOT_PASS_READS, and a webhook CONTROL pins those reads on four hosts (7f68e294b).Round 4, from the main merge
a5c37e97c(origin/main04c35f095, with #32791 as PR #32802,25ee50765: a stored package-less row the boot pass files under the host's package is one holder of an action name):database-loader.test.ts, metadata: a stored row bound to the host package is counted as two holders (host package + environment) in the bare action read's loader judgement (regression of #32560 / PR #32719) #32791's describe: its CONTROL "every by-package type" iterated the live slotted set and asked for the row the pass registered, so forwebhook3 cells went red on the merged tree (the package-less row saved last, on each host:com.alpha.appwhere it expected the package-less row). The pass registers no webhook row, so the read is the loader's own. The CONTROL now iteratesBOOT_PASS_READS, as metadata: the boot priming pass primes one holder per name for unbundled slotted types, so a bare read of a two-holder name answers the last row saved instead of 409 (epic red line 2) #32521's and metadata: the boot priming pass hands a column-bound slotted row to register unstamped, so with two holders of a name the DatabaseLoader save inserts a package-less row (unbundled slotted types) #32458's describes do, and a webhook CONTROL pins the loader's answer: the package's row in either order on every host, the pass registering neither. FROM, withwebhookout of the slotted set on the merged tree: the row saved first. Declared as (7).package-scoped-exact-qualifier.test.tsandwebhook-stored-row-boot-hydration.test.ts(the merge also brought fix(metadata-protocol)!: a package-less row at a holder's exact address answers one thing on every read and in every hydration state: declined for a sealed type, refused with two holders for an overlay type (ADR-0150 D4) #32741,a888c5c9e: a package-less row at a holder's exact address is declined for a sealed type): 7 webhook pins went red. Two holders: each holder's own declaration, where they answered409. One holder: the environment's item, the layered overlay and the boot-hydrated package-less row are declined; withwebhookout of the three sets (dists rebuilt) the same, so they are fix(metadata-protocol)!: a package-less row at a holder's exact address answers one thing on every read and in every hydration state: declined for a sealed type, refused with two holders for an overlay type (ADR-0150 D4) #32741's own, and no Clause-② entry is added. Re-pinned, each naming its FROM (c0500c639).Round 3's scratch probes, two holders: #32566's layered read moved (since #32741, declined, above); #32351's
DatabaseLoaderhistory moved; the row-binding migration did not move between the heads.Red on main, then green (round 1).
400to four segments). The bare-address 409 pin passes on main, because its fake reader carriesrefusal; the503 → 409move is pinned in plugin-webhooks (the error'srefusal, A3) and in the dogfood door, which boots the real reader.Ablations, each through
scripts/ablation-replace.mjs(mutation landed, restored to the HEAD blob with an emptygit diff HEAD):A1, the ref id without the package: 2 red (one holder dispatched as
declared:PKG/NAME; FROM neither TO both).A2, a slot's own row ignored (
own = undefinedinwebhookSlotSwitch): 8 red (the three existing ledger pins and the existing redelivery refusal of a webhook switched off through the ledger; the new ledger slot pin, Q1, D8, and the slot-exact redelivery).A3,
refusaldropped fromDeclaredWebhookAmbiguousError: 1 red (the bare read's 409 carrying the refusal).A4, the door's webhook store back to name addressing: 2 red (the namespace door's own row; Q1's row beside the package-less one).
A5, the served slice read as every holder (
served = every): 6 red ("[FROM delivered TO withheld]", "[FROM A's switch TO no holder's]" and the served-set name guard, each in both orders).A6, ledger ownership read from the served slots: 2 red ("[FROM A's switch TO no holder's]", both orders).
A7, the door's
adoptPackagelesscounting the served holders: 1 red ("[FROM adopted TO beside it]").A8,
ref_idback to 128: 1 red ("[FROM lost TO delivered]").A9, no dedup digest: 2 red ("[FROM lost TO delivered]"; the 191 / 192 boundary).
A10, the report's ledger rows judged by the bare shipper: 1 red ("[FROM no finding TO orphan]").
A11, the report's shippers without the listed webhooks: 1 red ("[FROM one TO each]").
Round 3, on
2200b8c9(the main mergea0131ec1dtouches none of the files below):R3-1 (M1), the refusal of a
declared:NAMEdelivery when more than one package holds the name, switched off: 2 red ("[FROM allowed TO refused]", both orders); 290 of 292 green.R3-2, the lone holder's refusal not naming the package (the owner taken from the one holder dropped): 1 red (CONTROL one holder, disabled, refused naming it).
R3-3 (M4), the every-holder-disabled branch for an item a metadata service lists: 1 red ("every holder disabled: the item a metadata service lists of the name is not dispatched").
R3-4 (M4), the bulk path without the digest: 1 red ("a dedup key past 191 is the digest on the bulk path too; one that fits is unchanged").
R3-5 (S2),
coveredjudged by name alone: 1 red ("[FROM covered TO a candidate]"). R3-6, the package-less row's coverage dropped: 1 red (the same pin, its D8 half).Round 3, after the main merges (each restored to the HEAD blob, whole-tree
git status --porcelainclean):includeDisabledPackages(the merged tree before91acdcc3f): 8 red, listed above.webhookout of all three type sets, as main has them (the core, metadata and objectql dists rebuilt, and each set read back from its dist withoutwebhook): 5 red in objectql e8 and runtimemeta-qualified-item-read(the disabled co-holder pins in both orders, and the lone-disabled library pin). The FROM column of those rows is this reading.webhookout ofPACKAGE_SLOTTED_STORE_TYPES: 3 red inmetadata-manager-keyed-plural-read(the set pin, and both orders of the two-package case); its CONTROL green.Round 4, on
26a846fe7:webhookout ofPACKAGE_SLOTTED_STORE_TYPES: 3 red indatabase-loader.test.ts, the new webhook CONTROL among them; 271 green.webhookout ofPACKAGE_SCOPED_READ_TYPES: in the exact-qualifier webhook describe only the premise goes red (1 of 9). The declines come from fix(metadata-protocol)!: a package-less row at a holder's exact address answers one thing on every read and in every hydration state: declined for a sealed type, refused with two holders for an overlay type (ADR-0150 D4) #32741's sealed-type rule, not from the read set; these pins hold that the rule, under two holders, picks neither.webhookout of all three sets (dists rebuilt): the three one-holder CONTROLs answer as at the head, the FROM of the one-holder rows above.Two round-2 pins pin a path this PR does not change, so they have no ablation: the R2-B boot-hydration pins (
loadMetaFromDbbinds the rows alike with main's three sets, measured), and the/published409(with main's sets the same registry answers200, the first registered package's, per the round-2 review, M5).Gates and verification
Round 5, on
68145a92a(changeset text only; no source or test change):check-changeset-no-major,check:empty-changeset,check-adr-0087-registration,check:changeset-gate-self-testsandcheck:nul-bytes, exit 0. A trial merge withorigin/main994c7d673(it brings #32999, which touchesdatabase-loader.test.ts) is clean; metadata on it 2169 of 2170, the one red the knownTS7016of a declaration-less localspecdist.Round 4, on
c0500c639(40c798157text; the main mergea5c37e97c,origin/main04c35f095,--remerge-diffempty;26a846fe7andc0500c639tests and changeset text):check-adr-0087-registration,check-changeset-no-major,check:changeset-fixed,check:empty-changeset,check:changeset-gate-self-tests,check:nul-bytes,check:test-source-alias,check:engine-double-contract(its pinned file auto-merged): exit 0. ESLint on the three changed test files,typecheckof metadata and objectql: exit 0.origin/maind4b14a850(six commits after04c35f095, three of them touching this PR's files) is clean, and the overlapping suites pass on it: objectql's six files 662/662, corepackage-scoped-resolution32/32, runtime's three files 413/413, plugin-webhooks 292/292. So, per the seat's rule, no further merge.Earlier heads, in short:
c080016c7ran all 130 derived gates and the nine package suites (webhook dogfood 61/61);d4f90f14dand7f68e294bre-ran the gates their files feed and the touched suites, exit 0. The full dogfood battery last ran ond9cea3dc7(454 files passed).Acceptance notes
0763bf6ec, in the main merge281722eb4); feat(core, objectql, metadata, metadata-protocol, plugin-security): a permission-set name is held once per namespace, judged by one predicate (ADR-0150 child B) #32742 (merged as5defbfeb9) is in the main merge491e70bc5, fix(metadata)!: the boot priming pass primes every holder of a name two packages hold, of a type stored per (package, name), so a bare read of that name is refused after the pass as without it #32676 (merged as2ca09de55) in2173fae50, and metadata: a stored row bound to the host package is counted as two holders (host package + environment) in the bare action read's loader judgement (regression of #32560 / PR #32719) #32791 (PR fix(metadata): a stored package-less row the boot pass files under the host's package is one holder of an action name, not two, in the bare read's judgement #32802,25ee50765), fix(metadata-protocol)!: a package-less row at a holder's exact address answers one thing on every read and in every hydration state: declined for a sealed type, refused with two holders for an overlay type (ADR-0150 D4) #32741 (a888c5c9e) and fix(metadata-protocol)!: a pair address reads the package it names: its envelope is that package's, and an App pair two of whose modules hold the name is refused naming both (ADR-0150 D5) #32808 (c50d10c00) ina5c37e97c.catalog-activation.tsis also touched by feat(plugin-security, runtime, core)!: the permission-set grant, clone and activation doors take a package, and a grant is unique per package #32306 (merged); the main mergea0131ec1dkeeps both changes inregisterSecurityActivationRoutes: the permission store keepsPERMISSION_LEDGER_ADDRESSING, the webhook's store takesWEBHOOK_LEDGER_ADDRESSING, and every other type keeps the name-addressed store.declared:PKG/NAME, and an event is delivered once (pinned in the plugin and in the dogfood upgrade harness, 2b). A delivery written before the upgrade keepsdeclared:NAME, and its redelivery reaches the one holder as before. A package-less ledger row is bound to the one holder by the existing boot backfill (activation-ledger-backfill.ts). On PostgreSQL or MySQL,os migrate applywidensref_id(ruling R3, Q2).declared:PKG/NAME, no row is sent to another holder, and deliveries queued across the upgrade are not ordered against each other.bare-name-one-holder.ts("subscribes one declared webhook per name") is left as it is, as email_template's was in feat(objectql)!: email template names held per namespace, a send resolves its template from its package, and ?package= reads an email template exactly (ADR-0150 child B, webhook family 1/3) #32214; the type's narrowing paragraph beside it says what holds now.409, so the console needs?package=and the namespace address. Not in this PR.action; for the webhook, fix(objectql, metadata-protocol)!: by-name doors judge a name's holders over the served set, so a disabled package's item is not a holder (ADR-0150 D3) #32497 has since made that read judge the served set, pinned above, so its premise no longer holds for this type, and whether it closes is the seat's call), plugin-webhooks: a ledger row naming a package that no longer declares a webhook switches the remaining holder, and the 18.x ownership ceremony falls back to the first-listed holder (ADR-0150 D4/D8) #32695 (a departed co-holder's ledger row read as the remaining holder's switch; the 18.x ceremony's first-listed fallback).connectorNameRefusalresolves with the caller and counts holders without it; now that fix(objectql, metadata-protocol)!: by-name doors judge a name's holders over the served set, so a disabled package's item is not a holder (ADR-0150 D3) #32497 has landed, a disabled package's flow could dispatch to the enabled holder's connector instance. That state needs two connector holders, which this PR does not admit:connectorstays outsideNAMESPACE_SCOPED_BARE_NAME_TYPEShere, and the flow-family CONTROL still pins its cross-namespace422. The fix, in fix(objectql, metadata-protocol)!: by-name doors judge a name's holders over the served set, so a disabled package's item is not a holder (ADR-0150 D3) #32497'sbundleAddressOfshape with both orders pinned, goes into the connector PR (3 of 3).origin/main04c35f095at68145a92a(2334 lines: 964 source, 1312 tests and dogfood, 58 docs and changeset), under 3000.Round 4's open questions
GET /meta/webhook/NAME's step 2 (unmeasured). The shape: one holder, its package's stored row and a package-less stored row. The protocol's by-name read calls the metadata service'sgetDiagnosed(theDatabaseLoaderread of (7)) at its step 2 for every type (protocol-meta-item-reads.ts:1056), only when steps 1 and 1b answer nothing; step 1 declines package-less stored rows (Regime C). Whether the bare read reaches step 2 here is not measured. Options: A, the probe on triage's ask; B, the p2 follow-up card plugin-webhooks: follow-ups of PR #32577 — pin and door for two webhook holders that still meet in one namespace, the disabled co-holder pin, two checklist items, liveness and impact-report nits, GET /meta step 2 (ADR-0150 D4) #33016. The epic seat ruled B unless the delta asks.mainsince fix(objectql, metadata-protocol)!: by-name doors judge a name's holders over the served set, so a disabled package's item is not a holder (ADR-0150 D3) #32497. The 18.3.0 tree (badf24dff, read, not booted) judged adeclared:NAMEredelivery by the listing, which withholds a disabled package, so the enabled holder's answer was released. Recommended: keep (6).Round 2's open questions, as ruled (R3, 6820856607)
os migrate applybefore references pass 128 characters; the changeset carries the FROM → TO and the upgrade line./publishedover a registry holding both items answer200with the first registered package's declaration (M5); the docblock inmeta-qualified-item-read.test.tsnow says so.dedup_keydocs sentence: approved and done (webhooks.mdx§4.3).🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6CZu9RRx2o8sFUUpEPU