Repository navigation
feat(spec): ServiceSlotContracts types the webhooks and trigger-api slots - #22922
Conversation
…lots
Adds `webhooks: IWebhookService` and `'trigger-api': ITriggerApiService` to
the non-core slot ledger. Both providers already register an occupant typed
as the contract (plugin-webhooks' WebhookOutboxPlugin, trigger-api's
ApiTriggerPlugin), so the binding is evidenced; a keyed lookup now answers
the contract instead of `unknown`.
The contracts test pins `getService('webhooks')?.handleRedeliver` and
`getService('trigger-api')?.handleInboundHook` to the declared members
through the runtime's keyed lookup signature, and makes the non-core key
set exact (it was a subset check reading as "exactly").
Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF
Co-authored-by: Claude <noreply@anthropic.com>
…tries Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 139 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 bd69ddf2105618ad314247144a6993f986b6b0da && git checkout bd69ddf2105618ad314247144a6993f986b6b0da
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6a40a09ed952f83b1f765d47cd76aee4a41f6672 da4a5675586debb05017d0754da6911ce0f31060 && git checkout -B drift-repro 6a40a09ed952f83b1f765d47cd76aee4a41f6672 && git merge --no-ff da4a5675586debb05017d0754da6911ce0f31060
node scripts/docs-audit/affected-docs.mjs --json 6a40a09ed952f83b1f765d47cd76aee4a41f6672 |
|
Contract reviewServed-tier: Read-only, adversarial, on PR #22922 ( ① Derived judgments1. The binding is true — holds. 2. The widening, and that it is only widening — holds. 3. The pin, and the narrowed premise — holds, nothing on the card left unmet. 4. The changeset — every sentence true to the diff. The heading; the two entries with their registering packages (item 1); FROM → TO ( 5. CI — two reds, one cause, judged not this PR's; the rest green or rostered. 35 check-runs: 30 ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Contract reviewServed-tier: Rendered 2026-10-12T09:47Z, read-only and adversarial, on PR #22922 ( ① Derived judgments0. The head and the diff. 1. Public surface:
2. The evidence bar for an entry holds. Right. The file's rule is that an entry is a claim made only where the binding is established by the registering provider. At the merge-base: 3. Placement in 4. The pin, and the premise it narrows. Right, with the narrowing accepted. The card's "Done when" says 5. "Regenerate whatever the slot map feeds": nothing moves. Right. 6. The changeset, sentence by sentence. 7. The check-runs on this head, as read. 42 check-runs: 37 ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #22848
Clause-②: yes (widening: a published type gains two keys)
What changes
ServiceSlotContracts(packages/spec/src/contracts/core-service-contracts.ts) gains two entries:webhooks: IWebhookService.@objectstack/plugin-webhooksregisters this slot:WebhookOutboxPlugin(webhook-outbox-plugin.ts:411) registers an occupant declared asconst service: IWebhookService.'trigger-api': ITriggerApiService.@objectstack/trigger-apiregisters this slot:ApiTriggerPlugin(plugin.ts:169) registers an occupant declared asconst service: ITriggerApiService.Both providers already type the occupant as the contract. So the registration itself meets the file's evidence bar ("an entry is a claim, made only where the binding is established"). Neither slot is a
CoreServiceNamemember, so both entries go inServiceSlotContractsand not inCoreServiceContracts. Each entry has TSDoc naming the contract and the package that registers it.This is type information only. No runtime file changes. No generated artifact changes:
check:generatedreports all 14 up to date at HEAD63710c5572.The pin (the card's "Done when")
The pin is in
core-service-contracts.test.ts, whichcheck:test-typecheckcompiles.K extends keyof ServiceSlotContractsname in,ServiceSlotContractofK(orundefined) out. That is the shape ofDomainHandlerDeps.resolveServiceandgetRequestKernelService. Through it,getService('webhooks')?.handleRedeliveris pinned toIWebhookService['handleRedeliver'], andgetService('trigger-api')?.handleInboundHooktoITriggerApiService['handleInboundHook']. Each slot is pinned to its contract orundefined.IHttpServer,ISecurityServiceandIDataEngine(each orundefined) forhttp.server,securityanddata. The existing pins are untouched and green:protocolandmcpareunknown,objectqlis the full engine, andhttp-serveris the alias.keyof ServiceSlotContractsminus the core keys must equal the seven listed keys. A key added or dropped without updating the list is a type error.The new pins use
expectTypeOf, not localtype _Xaliases. The file's existing aliases are recorded as TS6196 debt intest-typecheck-debt.json, and new aliases would have grown that shrink-only ledger. The ledger is unchanged: 52 files / 246 errors / 135 signatures.Ablation (spec pin)
The fix was committed first (
9fe6996719). Thennode scripts/ablation-replace.mjs(WRAP mode, which restores automatically) deletedwebhooks: IWebhookService;fromcore-service-contracts.tsand ranpnpm --filter @objectstack/spec check:test-typecheck.13ce41bf32ee→80becbb0dba7.core-service-contracts.test.ts:"webhooks"is not assignable tokeyof ServiceSlotContracts;handleRedeliverdoes not exist;13ce41bf32ee), andgit diff HEADis empty.Reverse verification: the runtime reads the rebuilt declarations
The 36-package dependency closure of the four consumers was built:
turbo build, concurrency 2, 36 of 36 succeeded. Thenablation-replace(WRAP) inserted one line atpackages/runtime/src/domains/webhooks.ts. The line reads a member that does not exist,notAMember, from the uncastdeps.resolveService(context, WEBHOOKS_SLOT, …)result. The command waspnpm --filter @objectstack/runtime exec tsc --noEmit.c920bff6c2bd→6aaa357c3a81.src/domains/webhooks.ts(115,17): error TS2339: Property 'notAMember' does not exist on type 'IWebhookService'. A stale declaration set has nowebhookskey. There the lookup would fall to thestringoverload, which answersany, and the line would compile.c920bff6c2bd), andgit diff HEADis empty.ablation-replacerefused it (anchor 1 → 1) before running tsc. Its exit 1 is not a reading, and the run above is the corrected one.Readings at BASE
7de0265fc7ServiceSlotContractsextendsCoreServiceContracts, and quoted kebab keys already exist ('http-server').ServiceSlotContractofKanswers the entry, orunknownwhen there is none.DomainHandlerDeps.resolveService(domain-handler-registry.ts:182, with an untypedstringoverload at:183);getRequestKernelService(:299/:300);resolve-execution-context.ts:57).ObjectKernel.getService(packages/core/src/kernel.ts:816) is generic over a caller-chosenT, takesname: stringand returnsT. It does not read the map. So the card's "kernel.getService('webhooks')is typed" holds for the runtime's keyed lookups, not for the kernel accessor.core-service-contracts.test.ts, which is where the new pin went.IWebhookService.handleRedeliver?(webhook-service.ts:96) andITriggerApiService.handleInboundHook?(trigger-api-service.ts:148). The contracts barrel exports both (contracts/index.ts:36,:66).ctx.registerService(name: string, …)does not read the map either, so no registration is re-checked.runtime/src/domains/webhooks.ts:114andruntime/src/domains/automation-hooks.ts:120. Both still compile. Each casts toPickof the contract's one member, orundefined, and that cast is now redundant (see Acceptance notes). Three test files callkernel.getServicewith an explicitIWebhookServiceorITriggerApiServicegeneric:webhook-redeliver-member.test.ts:189,hono-webhooks-redeliver.conformance.test.ts:174andinbound-hook-member.test.ts:169. The map does not affect them. The pinned objectui checkout (4997995022) has zero hits forServiceSlotContractorCoreServiceContract; the control grep for@objectstack/spechits.check:generatedregenerates. Nothing. All 14 artifacts are current, andcheck:api-surfacereports the public API surface unchanged: the snapshot records that an export exists, not its members.Verification (HEAD
63710c5572;origin/mainmerged throughos-regen-merge.sh, and the merge touched nopackages/specfile)pnpm --filter @objectstack/spec build: VERDICT command-exit 0.pnpm --filter @objectstack/spec typecheck(tsc, the scripts typecheck andcheck:test-typecheck): exit 0; test layer OK, 52 / 246 / 135 held.pnpm --filter @objectstack/spec test: 648 files, 19395 passed and 1 todo.test:repo: 55 files, 971 passed. Both ran atfbe0be035b, andgit diff fbe0be035b 63710c5572 -- packages/specis empty.@objectstack/runtime(tsc; test layer 27 / 190 / 68 held);@objectstack/plugin-webhooks(tsc, scripts tsc, test layer);@objectstack/trigger-api(tsc);@objectstack/http-conformance(tsc; test layer 3 / 27 / 10 held).check:generated(14 of 14 current) andcheck:api-surface(unchanged): green at HEAD.dispatch-gates.mjs --commands, no paths, at63710c5572): 85 commands, of which 83 ran and exited 0. Two are NOT MEASURED by the dispatch's instruction:check:dual-build-cjs-loads(no whole-workspace build) andcheck:type-check-debt(it is the--re-measureform). The--ranreconciliation reports 85 accounted for, 0 unrun.check:published-readme-exports: PREREQUISITE NOT MET (exit 3; 19 unbuilt packages);check:console-injection: no console dist, so it skipped at exit 0.The three PR-context guards are run against this PR after it opens.
Acceptance notes
runtime/src/domains/webhooks.ts:114andruntime/src/domains/automation-hooks.ts:120, the cast toPickof the contract's member (orundefined) now restates what the lookup answers. Both still compile. Neither file is in this PR. A later edit to either file can drop the cast.ServiceSlotContracts. There are zero such objects in this repository and in the pinned objectui checkout. The map is read only throughkeyofand indexed access.contracts/trigger-api-service.tsstill says thetrigger-apiregistration "is a separate change: until it lands no kernel fills the slot".ApiTriggerPluginnow registers the slot (plugin.ts:169). This is outside this PR's file surface and is noted only.Generated by Claude Code