You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(spec,runtime): a service-slot lookup returns the slot's contract, not any (#4127) (#4168)
#4127's most valuable item was the one it did not do: add a gate for the class.
Its four contract gaps were found by a human sweeping the dispatcher by hand. A
sweep is not repeatable, and this one was not complete.
The root was one line — `getService(name: string): any` in
domain-handler-registry.ts. Against `any`, a domain calling a method its
contract declares and a domain calling a method nobody declares typecheck
identically. That is what let #4087 ship a `/storage` handler passing two
arguments no implementation takes, and what hid #4127's four.
`CoreServiceContracts` is the slot -> contract ledger. `CoreServiceName` named
the slots and `contracts/*` described them; nothing connected the two. It does
now, and `getService<K>(name: K)` resolves through it, so a call outside the
contract is a compile error at the call site.
An entry is a claim, so entries are made only where the binding is evidenced:
by the provider that registers the slot (service-storage -> file-storage;
objectql -> data, whose own comment reads "ObjectQL implements IDataEngine"),
or by dispatcher work that proved it (#4143/#4150 for automation, notification,
i18n). `ui` is deliberately unmapped — the slot exists and domains/ui.ts serves
it, but no IUiService was ever written. An unmapped slot resolves to `unknown`,
not `any`, so it must be cast deliberately and the gap stays legible.
Two findings within minutes of turning it on:
- `/auth` called a method that does not exist. domains/auth.ts probed
`authService.handler(request, response)`; `IAuthService` declares
`handleRequest(request)` and `AuthManager` implements exactly that, with no
`handler`. False on every deployment — #4143's dead `automation.trigger`
again. #4127's sweep never mentions `/auth` in either its gap list or its
"clean" list: the file the compiler flagged first is the one the human pass
skipped. Not a live hole — the Hono adapter calls `handleRequest` itself and
only falls through when no usable auth service answered — but reading the
contract makes the branch reachable for the first time, so a host calling
`handleAuth` directly WITH an auth service now gets it instead of
mockAuthFallback's `mock_<uuid>` session.
- `POST /analytics/sql` invoked an optional method unguarded. `generateSql?` is
optional on IAnalyticsService — unlike `query`/`getMeta` beside it — so a
provider without it answered a 500 from TypeError instead of saying the
capability is absent. Answers `handled: false` now, the same 404 the entry
gate already gives for absent analytics capability.
`isServiceServeable` becomes a type guard (`svc is NonNullable<T>`). Every
domain already calls it first on a resolved slot, so one predicate narrows away
the `undefined` for the whole body — the null check and the capability check
were always the same check.
The test-side hole #4127 predicted, closed for this batch: THREE tests across
two files mocked `{ handler }` for auth, including one whose subject was the
resolution path, so it proved the lookup worked and nothing about the call.
`ContractMock<T>` guards mock keys against the contract; signatures stay
`unknown` so vi.fn() does not force everything back to `as any`. The automation
mock's `trigger` stays as a labelled negative control outside the checked
literal — a test asserting the route never calls it is the point.
The 12 domains not calling `getService` are untouched. `resolveService`, which
also takes non-CoreServiceName names like `protocol` and `objectql`, is left
for a later batch rather than widened here.
Refs #4127
0 commit comments