Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
# `api-types` — Foundation gap 5 cleanup: nullable tenant on RequestContext
**Package**: `@suaveplan/api-types`
**Status**: PROPOSED
**Type**: type-soundness-fix
## Why
The `foundation-fix-gap-5-default-tenant` change widened the runtime
behaviour: when the resolver chain misses and no `defaultTenant` is
configured, `RequestContext.tenant` / `RequestContext.tenantId` are
`null` at runtime. The static types in `@suaveplan/api-types` were NOT
updated — they still declared `tenant: TenantContext` and
`tenantId: TenantId` as non-nullable.
Consequences observed in `request-pipeline-helpers.ts`:
```ts
tenantId: (args.tenant?.id ?? null) as never,
tenant: (args.tenant ?? null) as never,
```
The `as never` is a documented escape hatch — but it is a type lie that
defeats every handler-side guard the type system was supposed to
enforce. Out-of-spec debt; resolved before moving on per the user's
directive.
## What changes
MODIFIED: `RequestContext<Caps>` field types in
`@suaveplan/api-types/plugin-context`:
```ts
readonly tenantId: TenantId | null;
readonly tenant: TenantContext | null;
```
Existing `tenant`/`tenantId` consumers in the monorepo already use the
null-safe `?.` access pattern (verified across `e2e/foundation-stack`,
`apps/demo`, `apps/testbed`, and every `api-plugin-*`). No handler
needed a code change beyond stripping the now-redundant casts.
The `JobContext` and `ConnectionContext` flavours are NOT widened.
Per-tenant jobs and connection-scoped contexts only exist after a
tenant has been resolved — their resolver chain runs separately and
never returns `null` to the handler. Widening those would force a
spurious null-check on every job/connection handler.
The `as never` casts in `request-pipeline-helpers.ts` lines 143-150 are
removed. The two remaining `as` casts at the same site are
generic-erasure points (capabilities/instruments resolve to typed
slices at the route handler call-site, not at the untyped pipeline
seam); they now use the explicit
`as RequestContext["capabilities"]` form with a multi-line code comment
explaining the boundary instead of a blanket `as never`.
## Impact
- `@suaveplan/api-types` — spec MODIFIED Requirement (this delta).
- `@suaveplan/api-host` — `as never` casts stripped from
`buildRequestContext`; generic-erasure casts retained with the
documented boundary comment.
- E2E foundation-stack: 11 Playwright tests still green; no handler
changes needed since fixture plugin already used `ctx.tenant?.id`.
## Verification
```
bunx turbo lint typecheck test build --filter=@suaveplan/api-types --filter=@suaveplan/api-host
bunx turbo test:coverage --filter=@suaveplan/api-host --filter=@suaveplan/api-types
cd e2e/foundation-stack && bunx playwright test # 11/11
```
All green. No `as never` remains for `tenant`/`tenantId` in
`request-pipeline-helpers.ts`. No `biome-ignore` / `@ts-ignore` /
`@ts-expect-error` added anywhere.

View File

@@ -0,0 +1,38 @@
# api-types — Foundation gap 5 cleanup delta
## MODIFIED Requirements
### Requirement: Plugin context separation
The package SHALL export `PluginContext<Caps>` and `RequestContext<Caps>`. `RequestContext` SHALL extend `PluginContext` (modulo the differently-typed `db` and `capabilities` slots) and add `request`, `principal`, `tenantId`, `tenant`, `requestId`, `span`. The `db` slot — exposed BOTH as a top-level context field AND as a capability-slice entry when `db` is declared in `Caps` — MUST be typed as `AdminDatabaseLike | null` on `PluginContext` and `TenantScopedDatabaseLike | null` on `RequestContext`. The two types MUST NOT be mutually assignable so an admin-DB consumer cannot accidentally accept a tenant-scoped one without an explicit cast. `null` SHALL be the runtime value when the host has no configured DB adapter, OR — for `RequestContext` only — when the tenant resolver chain returned no tenant on this request.
`RequestContext.tenant` SHALL be typed `TenantContext | null` and `RequestContext.tenantId` SHALL be typed `TenantId | null`. The nullable union is required because the `foundation-fix-gap-5-default-tenant` change made `defaultTenant: null` the host default — the resolver chain may legitimately miss on auth-optional routes, and the runtime value is then `null` on both fields. Handlers consuming tenant-bound capabilities MUST null-check OR declare `auth: "required"` on the route so the pipeline rejects the request before the handler runs. `JobContext.tenant` / `JobContext.tenantId` and `ConnectionContext.tenant` / `ConnectionContext.tenantId` SHALL remain non-nullable — per-tenant jobs and connection-scoped contexts only exist after a tenant has been resolved.
Both context flavours SHALL also expose the orchestration-level `secrets: SecretsLike` slot directly — the slot is always present at the type level (the host injects a default when no adapter is configured) and is independent of the capability map. `JobContext` and `ConnectionContext` SHALL likewise expose a tenant-scoped `db: TenantScopedDatabaseLike | null` top-level field AND inherit `secrets` through the same Omit-based extension.
#### Scenario: db slot has a different type per context
- **WHEN** a value of type `PluginContext<{db: true}>` and a value of type `RequestContext<{db: true}>` are compared
- **THEN** `expectTypeOf<PluginContext<...>.capabilities.db>().not.toEqualTypeOf<RequestContext<...>.capabilities.db>()` passes
#### Scenario: capability map drives capability-slice field availability
- **WHEN** `PluginContext<{db: true, "auth.session": true}>` is constructed
- **THEN** `ctx.capabilities.db` and `ctx.capabilities["auth.session"]` are required AND `ctx.capabilities.events` is NOT present in the type (unless `"events"` is also declared)
#### Scenario: secrets slot is always present on every context
- **WHEN** a plugin's lifecycle hook destructures `{ secrets } = ctx` where `ctx: PluginContext<readonly []>`
- **THEN** `secrets` is typed as `SecretsLike` and resolves at runtime to whichever adapter the host configured (or the `EmptySecrets` default)
#### Scenario: top-level db is null-permissive
- **WHEN** a host is booted without an `adapters.db` slot
- **THEN** `PluginContext.db === null` AND `RequestContext.db === null` at runtime AND the type system requires every consumer to null-check before use
#### Scenario: RequestContext.tenant is null-permissive after gap-5
- **WHEN** a route is reached with no tenant resolved AND no `defaultTenant` configured
- **THEN** `RequestContext.tenant === null` AND `RequestContext.tenantId === null` at runtime AND the type system requires every handler to null-check or short-circuit via `auth: "required"` before reading tenant-bound fields
**Verified by:** `src/_typetests/plugin-context.spec-conformance.test.ts::"db slot type differs between PluginContext and RequestContext"` AND `src/_typetests/plugin-context.spec-conformance.test.ts::"capability map drives ctx field set"` AND host integration in `@suaveplan/api-host`'s `src/host/host.test.ts::"ctx.db === null when adapters.db is undefined"`

View File

@@ -0,0 +1,20 @@
# Tasks
- [x] Widen `RequestContext.tenant` to `TenantContext | null` in
`packages/api-stack/api/api-types/src/plugin-context/plugin-context.types.ts`.
- [x] Widen `RequestContext.tenantId` to `TenantId | null` at the same site.
- [x] Add nullability rationale to the `RequestContext` TSDoc block —
pointer to gap-5 + handler null-check guidance.
- [x] Strip `as never` from `tenant` and `tenantId` assignments in
`packages/api-stack/api/api-host/src/request-pipeline/request-pipeline-helpers.ts`.
- [x] Replace `capabilities`/`instruments` `as never` casts with
explicit `as RequestContext["capabilities"]` / `as RequestContext["instruments"]`
plus a multi-line comment explaining the generic-erasure boundary.
- [x] Audit handler-side consumers (apps/demo, apps/testbed,
e2e/foundation-stack, api-plugin-*); confirm none dereference
`ctx.tenant.X` without `?.` and no code change needed.
- [x] `bunx turbo lint typecheck test build --filter=@suaveplan/api-types --filter=@suaveplan/api-host` green.
- [x] `bunx turbo test:coverage --filter=@suaveplan/api-host --filter=@suaveplan/api-types` green.
- [x] `cd e2e/foundation-stack && bunx playwright test` — 11/11 green.
- [x] Fold MODIFIED delta into `openspec/specs/api-types/spec.md`.
- [x] Archive change folder under `openspec/archive/2026-05-26-foundation-fix-gap-5-cleanup/`.