Repository navigation
Commit 9432bac
feat(spec): ServiceSlotContracts types the webhooks and trigger-api slots (#22922)
Fixes #22848
Clause-②: yes (widening: a published type gains two keys)
> ~~Held out of the merge queue by #22949.~~ Released 2026-10-12: #22949
closed, and `main` was merged into this branch with the net diff
unchanged. The seat queues it once the new head is green and its
remaining pre-queue items are met.
## What changes
`ServiceSlotContracts`
(`packages/spec/src/contracts/core-service-contracts.ts`) gains two
entries:
- `webhooks: IWebhookService`. `@objectstack/plugin-webhooks` registers
this slot: `WebhookOutboxPlugin` (`webhook-outbox-plugin.ts:411`)
registers an occupant declared as `const service: IWebhookService`.
- `'trigger-api': ITriggerApiService`. `@objectstack/trigger-api`
registers this slot: `ApiTriggerPlugin` (`plugin.ts:169`) registers an
occupant declared as `const 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
`CoreServiceName` member, so both entries go in `ServiceSlotContracts`
and not in `CoreServiceContracts`. 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:generated` reports all 14 up to date at HEAD
`63710c5572`.
## The pin (the card's "Done when")
The pin is in `core-service-contracts.test.ts`, which
`check:test-typecheck` compiles.
- **The two probes.** The test declares a lookup with the runtime's
keyed signature: a `K extends keyof ServiceSlotContracts` name in,
`ServiceSlotContract` of `K` (or `undefined`) out. That is the shape of
`DomainHandlerDeps.resolveService` and `getRequestKernelService`.
Through it, `getService('webhooks')?.handleRedeliver` is pinned to
`IWebhookService['handleRedeliver']`, and
`getService('trigger-api')?.handleInboundHook` to
`ITriggerApiService['handleInboundHook']`. Each slot is pinned to its
contract or `undefined`.
- **CONTROL: other slots.** The same lookup still answers `IHttpServer`,
`ISecurityService` and `IDataEngine` (each or `undefined`) for
`http.server`, `security` and `data`. The existing pins are untouched
and green: `protocol` and `mcp` are `unknown`, `objectql` is the full
engine, and `http-server` is the alias.
- **CONTROL: exact key set.** The test "holds exactly the non-core slots
whose contract is written" used to check only a subset: an array typed
as the key union, plus a length. It is now exact at the type level.
`keyof ServiceSlotContracts` minus 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 local `type _X` aliases. The file's
existing aliases are recorded as TS6196 debt in
`test-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`). Then `node
scripts/ablation-replace.mjs` (WRAP mode, which restores automatically)
deleted `webhooks: IWebhookService;` from `core-service-contracts.ts`
and ran `pnpm --filter @objectstack/spec check:test-typecheck`.
- **The mutation landed.** Anchor count 1 → 0, blob `13ce41bf32ee` →
`80becbb0dba7`.
- **The run went RED** (exit 1). Six new error signatures appeared, all
in `core-service-contracts.test.ts`:
- TS2345 ×3: `"webhooks"` is not assignable to `keyof
ServiceSlotContracts`;
- TS2339: `handleRedeliver` does not exist;
- TS2344 ×2: the slot-type pin and the exact-key-set pin.
- **The restore held.** Blob equals HEAD (`13ce41bf32ee`), and `git diff
HEAD` is 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. Then
`ablation-replace` (WRAP) inserted one line at
`packages/runtime/src/domains/webhooks.ts`. The line reads a member that
does not exist, `notAMember`, from the uncast
`deps.resolveService(context, WEBHOOKS_SLOT, …)` result. The command was
`pnpm --filter @objectstack/runtime exec tsc --noEmit`.
- **The mutation landed.** Anchor 1 → 0, blob `c920bff6c2bd` →
`6aaa357c3a81`.
- **The run went RED with exactly one error.**
`src/domains/webhooks.ts(115,17): error TS2339: Property 'notAMember'
does not exist on type 'IWebhookService'`. A stale declaration set has
no `webhooks` key. There the lookup would fall to the `string` overload,
which answers `any`, and the line would compile.
- **The restore held.** Blob equals HEAD (`c920bff6c2bd`), and `git diff
HEAD` is empty.
- **A first attempt was a no-op.** Its replacement contained the anchor
again, so `ablation-replace` refused 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 `7de0265fc7`
1. **How the map types lookups.** `ServiceSlotContracts` extends
`CoreServiceContracts`, and quoted kebab keys already exist
(`'http-server'`). `ServiceSlotContract` of `K` answers the entry, or
`unknown` when there is none.
- Three runtime seams are keyed by the map:
- `DomainHandlerDeps.resolveService` (`domain-handler-registry.ts:182`,
with an untyped `string` overload at `:183`);
- `getRequestKernelService` (`:299` / `:300`);
- the execution-context resolver (`resolve-execution-context.ts:57`).
- `ObjectKernel.getService` (`packages/core/src/kernel.ts:816`) is
generic over a caller-chosen `T`, takes `name: string` and returns `T`.
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.
- The existing slot-typing tests are in
`core-service-contracts.test.ts`, which is where the new pin went.
2. **The contracts.** `IWebhookService.handleRedeliver?`
(`webhook-service.ts:96`) and `ITriggerApiService.handleInboundHook?`
(`trigger-api-service.ts:148`). The contracts barrel exports both
(`contracts/index.ts:36`, `:66`).
3. **Registrations.** Both slots are registered (see "What changes"),
and each occupant is declared as the contract it is now keyed to. No
registration fails to satisfy its interface. `ctx.registerService(name:
string, …)` does not read the map either, so no registration is
re-checked.
4. **Callers.** Two callers resolve these slots through the typed seam:
`runtime/src/domains/webhooks.ts:114` and
`runtime/src/domains/automation-hooks.ts:120`. Both still compile. Each
casts to `Pick` of the contract's one member, or `undefined`, and that
cast is now redundant (see Acceptance notes). Three test files call
`kernel.getService` with an explicit `IWebhookService` or
`ITriggerApiService` generic: `webhook-redeliver-member.test.ts:189`,
`hono-webhooks-redeliver.conformance.test.ts:174` and
`inbound-hook-member.test.ts:169`. The map does not affect them. The
pinned objectui checkout (`4997995022`) has zero hits for
`ServiceSlotContract` or `CoreServiceContract`; the control grep for
`@objectstack/spec` hits.
5. **What `check:generated` regenerates.** Nothing. All 14 artifacts are
current, and `check:api-surface` reports the public API surface
unchanged: the snapshot records that an export exists, not its members.
## Verification (HEAD `63710c5572`; `origin/main` merged through
`os-regen-merge.sh`, and the merge touched no `packages/spec` file)
- `pnpm --filter @objectstack/spec build`: VERDICT command-exit 0.
- `pnpm --filter @objectstack/spec typecheck` (tsc, the scripts
typecheck and `check: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 at `fbe0be035b`, and
`git diff fbe0be0 63710c5 -- packages/spec` is empty.
- Consumer typechecks against the rebuilt spec, all exit 0:
- `@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) and `check:api-surface`
(unchanged): green at HEAD.
- Gate derivation (`dispatch-gates.mjs --commands`, no paths, at
`63710c5572`): 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) and `check:type-check-debt` (it is the
`--re-measure` form). The `--ran` reconciliation reports 85 accounted
for, 0 unrun.
- The artifact-roster block from the same derivation: 47 rows ran, 46 at
exit 0. Two did not measure:
- `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
- **Redundant casts.** At `runtime/src/domains/webhooks.ts:114` and
`runtime/src/domains/automation-hooks.ts:120`, the cast to `Pick` of the
contract's member (or `undefined`) 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.
- **Implementers.** Adding required keys to an interface breaks any code
that builds an object typed as the whole `ServiceSlotContracts`. There
are zero such objects in this repository and in the pinned objectui
checkout. The map is read only through `keyof` and indexed access.
- **Stale header sentence.** The header of
`contracts/trigger-api-service.ts` still says the `trigger-api`
registration "is a separate change: until it lands no kernel fills the
slot". `ApiTriggerPlugin` now registers the slot (`plugin.ts:169`). This
is outside this PR's file surface and is noted only.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: objectstack-fleet[bot] <332303061+objectstack-fleet[bot]@users.noreply.github.com>1 parent 111ffff commit 9432bac
3 files changed
Lines changed: 81 additions & 5 deletions
File tree
- .changeset
- packages/spec/src/contracts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
| 23 | + | |
22 | 24 | | |
23 | 25 | | |
24 | 26 | | |
| |||
110 | 112 | | |
111 | 113 | | |
112 | 114 | | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
113 | 140 | | |
114 | | - | |
115 | | - | |
116 | | - | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
117 | 148 | | |
118 | 149 | | |
119 | 150 | | |
120 | 151 | | |
121 | 152 | | |
122 | 153 | | |
123 | 154 | | |
124 | | - | |
| 155 | + | |
125 | 156 | | |
126 | 157 | | |
127 | 158 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
| 44 | + | |
| 45 | + | |
44 | 46 | | |
45 | 47 | | |
46 | 48 | | |
| |||
173 | 175 | | |
174 | 176 | | |
175 | 177 | | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
176 | 200 | | |
177 | 201 | | |
178 | 202 | | |
| |||
0 commit comments