Files
suaveplan/openspec/archive/2026-05-18-extend-api-foundation-host-services/specs/api/spec.md
2026-05-27 23:29:53 +01:00

3.7 KiB

Capability Spec Delta: api (extend-api-foundation-host-services)

Package: @suaveplan/api Status: PROPOSED

ADDED Requirements

Requirement: SuaveApp seeds canonical host services before plugin init

SuaveApp.initialize() SHALL call this._registry.seedServices(...) immediately before this._registry.initialize(). The seeded map SHALL include app (the OpenAPIHono instance), openAPIRegistry, logger, eventBus, clock always; AND optionally migrationRunner (when supplied via SuaveAppOptions.migrationRunner) AND db (when supplied via the new SuaveAppOptions.db). After init, every plugin's ctx.getService("app") / ctx.getService("openAPIRegistry") / etc. SHALL resolve to the corresponding instance with typed inference via the ServiceRegistry module augmentation.

Scenario: plugin resolves seeded app via ctx.getService

  • WHEN a SuaveApp is built with a plugin whose onInit(ctx) { recordApp(ctx.getService("app")) } runs, and app.initialize() is awaited
  • THEN recordApp was called with the OpenAPIHono instance the app holds at this._hono (referential equality, not a wrapper)

Scenario: missing migrationRunner option leaves the slot undefined

  • WHEN new SuaveApp({ ...options /* no migrationRunner */ }) is built and a plugin asks ctx.getService("migrationRunner")
  • THEN the result is undefined

Verified by: src/app/seed-services.test.ts::"plugin resolves seeded app via ctx.getService" AND src/app/seed-services.test.ts::"openAPIRegistry / logger / eventBus / clock are seeded" AND src/app/seed-services.test.ts::"missing migrationRunner leaves slot undefined"


Requirement: ServiceRegistry module augmentation declares canonical host slots

The package SHALL ship a side-effect-importable module at ./services/registry-augmentation that declares:

declare module "@suaveplan/api-plugin/service-registry" {
  interface ServiceRegistry {
    app: OpenAPIHono;
    openAPIRegistry: OpenAPIRegistry;
    migrationRunner?: MigrationRunner;
    db?: SurrealAdapter;
    logger: LoggerLike;
    eventBus: EventBusLike;
    clock: Clock;
  }
}

Consumers SHALL get full type inference on ctx.getService("app") (returns OpenAPIHono | undefined), etc. The augmentation SHALL be auto-loaded by the package root barrel (src/index.ts side-effect imports it) so any consumer of @suaveplan/api picks it up without an explicit import. Subpath consumers of @suaveplan/api/services/registry-augmentation get it directly.

Scenario: ctx.getService("app") type-narrows to OpenAPIHono | undefined

  • WHEN expectTypeOf(ctx.getService("app")).toEqualTypeOf<OpenAPIHono | undefined>() is asserted in the conformance test
  • THEN the type check passes

Verified by: src/services/registry-augmentation.spec-conformance.test.ts::"every host slot type-narrows correctly via expectTypeOf" AND src/services/registry-augmentation.test.ts::"side-effect import auto-loads via root barrel"


Requirement: SuaveAppOptions.db is optional

SuaveAppOptions SHALL accept an optional db?: SurrealAdapter field. When supplied, it's seeded into ServiceRegistry["db"]; when omitted, the slot stays undefined. The constructor SHALL store it on this._db for later seeding. No change to existing options.

Scenario: db option seeded when provided

  • WHEN new SuaveApp({ ...config, db: surrealAdapter }) is built and a plugin queries ctx.getService("db")
  • THEN the result is surrealAdapter (referential)

Verified by: src/app/options.test.ts::"db option seeded when provided" AND src/app/options.test.ts::"omitting db leaves the slot undefined"