Initial Commit
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# Extend api-plugin + api with host-seeded ServiceRegistry slots
|
||||
|
||||
**Packages**: `@suaveplan/api-plugin`, `@suaveplan/api`
|
||||
**Status**: PROPOSED
|
||||
**Type**: feature (additive — no breaking changes)
|
||||
|
||||
- **Spec deltas**: ./specs/api-plugin/spec.md, ./specs/api/spec.md
|
||||
|
||||
## Why
|
||||
|
||||
The just-shipped 5-package api split exposed a foundation-layer gap: `PluginContext` provides `getService<K extends keyof ServiceRegistry>(name)` but nothing on the host side ever seeds `app`, `openAPIRegistry`, `migrationRunner`, or `db` into the registry. The first concrete consumer (the in-flight `api-plugin-auth`, blocked at scaffold time) cannot resolve those services without forcing operators to register them by hand — which breaks the plug-and-play promise.
|
||||
|
||||
Fix: add `PluginRegistry.seedServices(map)` to `@suaveplan/api-plugin`, augment `ServiceRegistry` from `@suaveplan/api` with the canonical host slots, and seed them in `SuaveApp.initialize()` BEFORE `registry.initialize()` runs so every plugin's `onRegister` / `onInit` / `onReady` can resolve them via typed `ctx.getService("app")`, etc.
|
||||
|
||||
## What changes
|
||||
|
||||
### @suaveplan/api-plugin (additive)
|
||||
|
||||
- ADDED: `PluginRegistry.seedServices<K extends keyof ServiceRegistry>(map: { [K in keyof ServiceRegistry]?: ServiceRegistry[K] }): void` — bulk-registers entries in an internal seeded-services map. Idempotent; later seeds override earlier ones. Throws `PluginError(PLUGIN_INVALID_STATE)` if called after `initialize()` has started.
|
||||
- MODIFIED: `createPluginContext` (and the per-plugin context factory) pre-populates the shared service map from the registry's seeded entries before plugin `onRegister`. Plugins calling `ctx.getService("X")` see the seeded entry without needing a sibling plugin's `registerService` to have run first.
|
||||
- ADDED: 1 new error code: `PLUGIN_INVALID_STATE` (already part of the spec's 10 → 11 codes total — used by `seedServices` post-initialize).
|
||||
- No public-surface removals.
|
||||
|
||||
### @suaveplan/api (additive)
|
||||
|
||||
- ADDED: ServiceRegistry module augmentation declaring canonical host slots:
|
||||
|
||||
```ts
|
||||
// src/services/registry-augmentation.ts (genesis-original)
|
||||
declare module "@suaveplan/api-plugin/service-registry" {
|
||||
interface ServiceRegistry {
|
||||
app: OpenAPIHono;
|
||||
openAPIRegistry: OpenAPIRegistry;
|
||||
migrationRunner?: MigrationRunner;
|
||||
db?: SurrealAdapter;
|
||||
logger: LoggerLike;
|
||||
eventBus: EventBusLike;
|
||||
clock: Clock;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- MODIFIED: `SuaveAppOptions` gains optional `db?: SurrealAdapter` field (for plugins that need direct DB access — wired into the seeded slot). Defaults to `undefined`; no behaviour change for callers that don't supply it.
|
||||
- MODIFIED: `SuaveApp.initialize()` calls `this._registry.seedServices({ app: this._hono, openAPIRegistry: this._openAPIRegistry, migrationRunner: this._migrations, db: this._db, logger: this._logger, eventBus: this._events, clock: this._clock })` immediately before `this._registry.initialize()`.
|
||||
- ADDED: subpath export `./services/registry-augmentation` so consumers can side-effect-import the augmentation if they only consume sub-packages.
|
||||
- No public-surface removals.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Backwards-compatible runtime shim.** This is additive — existing callers of `ctx.getService("X")` for non-augmented keys continue to return `undefined` and silently work. No deprecation needed.
|
||||
- **Per-request services (request-id, tenant, etc.).** Those still flow via Hono `c.get(key)` — they're per-request, not per-plugin.
|
||||
- **Plugin manifest-side service declarations.** Plugins continue to declare what they `registerService` via the existing surface; this change only adds host-seeded slots.
|
||||
|
||||
## Source mapping
|
||||
|
||||
Genesis-original — surfaced by `add-api-plugin-auth`'s scaffold-time blocker (the subagent stopped because `PluginContext.app` doesn't exist on the shipped surface, then noticed `SuaveApp` never seeds `app`/`openAPIRegistry`/etc into the registry).
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
- `@suaveplan/api-plugin-auth` (in-flight) — unblocks. Resolves `app` / `openAPIRegistry` / `migrationRunner` via typed `ctx.getService` instead of operator pre-registration.
|
||||
- `@suaveplan/api-plugin-user`, `@suaveplan/api-plugin-vault` (in-flight) — inherit the same resolution path.
|
||||
- Every future `@suaveplan/api-plugin-*` package — same.
|
||||
|
||||
Risk: low. Additive on both packages; no existing API surface changes. Existing api-plugin + api tests stay green (verified by post-patch dependent sweep).
|
||||
@@ -0,0 +1,44 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: api-plugin (extend-api-foundation-host-services)
|
||||
|
||||
**Package**: `@suaveplan/api-plugin`
|
||||
**Status**: PROPOSED
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: PluginRegistry.seedServices bulk-registers entries before plugin init
|
||||
|
||||
The package SHALL add `PluginRegistry.seedServices<TMap extends Partial<{ [K in keyof ServiceRegistry]: ServiceRegistry[K] }>>(map: TMap): void`. Calling it stores every key/value pair in an internal seeded-services map. When `initialize()` later constructs per-plugin contexts, every seeded entry SHALL appear in `ctx.getService(name)` BEFORE any plugin's `onRegister` runs. Calling `seedServices` after `initialize()` has started SHALL raise `PluginError(PLUGIN_INVALID_STATE)`.
|
||||
|
||||
#### Scenario: seeded service is resolvable from the first plugin's onRegister
|
||||
|
||||
- **WHEN** `registry.seedServices({ app: stubApp })` is called, then a plugin with `onRegister(ctx) { ctx.getService("app") }` is registered, then `registry.initialize()` is awaited
|
||||
- **THEN** the plugin's `onRegister` resolves `stubApp` (no `undefined`)
|
||||
|
||||
#### Scenario: later seed overrides earlier seed
|
||||
|
||||
- **WHEN** `registry.seedServices({ logger: l1 })` is followed by `registry.seedServices({ logger: l2 })` before `initialize()`
|
||||
- **THEN** the next plugin's `ctx.getService("logger")` returns `l2`
|
||||
|
||||
#### Scenario: seedServices after initialize raises PLUGIN_INVALID_STATE
|
||||
|
||||
- **WHEN** `registry.initialize()` is awaited and then `registry.seedServices({ app: stubApp })` is called
|
||||
- **THEN** `PluginError(PLUGIN_INVALID_STATE)` is thrown
|
||||
|
||||
**Verified by:** `src/registry/seed-services.test.ts::"seeded service resolvable from first plugin's onRegister"` AND `src/registry/seed-services.test.ts::"later seed overrides earlier"` AND `src/registry/seed-services.test.ts::"seedServices after initialize raises PLUGIN_INVALID_STATE"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PluginRegistry exposes PLUGIN_INVALID_STATE in the code registry
|
||||
|
||||
`PLUGIN_CODES` SHALL include `PLUGIN_INVALID_STATE` (taking the registry to 11 codes total). The code SHALL be raised by `seedServices` post-`initialize()`; existing 10 codes are preserved.
|
||||
|
||||
#### Scenario: existing codes unchanged
|
||||
|
||||
- **WHEN** the spec-conformance test enumerates `PLUGIN_CODES`
|
||||
- **THEN** the set is the union of the existing 10 codes plus `PLUGIN_INVALID_STATE` (no removals)
|
||||
|
||||
**Verified by:** `src/api-plugin.spec-conformance.test.ts::"PLUGIN_CODES contains PLUGIN_INVALID_STATE alongside the existing 10"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
@@ -0,0 +1,68 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# 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:
|
||||
|
||||
```ts
|
||||
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"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
@@ -0,0 +1,41 @@
|
||||
# Tasks — extend-api-foundation-host-services
|
||||
|
||||
> Two-package patch. Order matters: api-plugin first (gates the api side). The two test/gate sweeps run separately per package; no umbrella.
|
||||
|
||||
## 1. Patch @suaveplan/api-plugin
|
||||
|
||||
- [ ] 1.1 Add `PLUGIN_INVALID_STATE` to `src/error-codes.ts` (preserves the existing 10 codes — total 11)
|
||||
- [ ] 1.2 Add `src/registry/seed-services.ts` (or extend `src/registry/registry.ts`) with `seedServices(map)` method on `PluginRegistry`. Internal `seededServices: Map<string, unknown>`. Idempotent re-seed.
|
||||
- [ ] 1.3 Modify `createPluginContext` (or wherever the per-plugin context is built) to pre-populate the shared `services` map from the registry's seeded entries before `onRegister` runs
|
||||
- [ ] 1.4 Tests:
|
||||
- [ ] 1.4.1 `src/registry/seed-services.test.ts` — covers the 3 scenarios in the spec delta (seed → resolve, later seed overrides, post-init throws)
|
||||
- [ ] 1.4.2 Update `src/api-plugin.spec-conformance.test.ts` — asserts `PLUGIN_INVALID_STATE` present + thrown from a real production path (`seedServices` after init)
|
||||
- [ ] 1.5 Update sibling `.md` for registry + error-codes
|
||||
- [ ] 1.6 Gates:
|
||||
- [ ] 1.6.1 `bunx turbo lint typecheck test --filter=@suaveplan/api-plugin --force` green
|
||||
- [ ] 1.6.2 `bunx turbo test:coverage --filter=@suaveplan/api-plugin --force` — still 100/100/100/100
|
||||
- [ ] 1.6.3 `bun scripts/openspec-conformance.ts @suaveplan/api-plugin` exit 0
|
||||
- [ ] 1.6.4 `bunx turbo build --filter=@suaveplan/api-plugin --force` green
|
||||
- [ ] 1.6.5 Dependent sweep `bunx turbo test --filter=...@suaveplan/api-plugin --force` still green (verifies no regression in tenant/openapi/migrations/api)
|
||||
|
||||
## 2. Patch @suaveplan/api
|
||||
|
||||
- [ ] 2.1 Add `src/services/registry-augmentation.{ts,md,index.ts}` declaring the canonical ServiceRegistry slots. Root barrel side-effect imports it.
|
||||
- [ ] 2.2 Add subpath export `./services/registry-augmentation` in `package.json`
|
||||
- [ ] 2.3 Modify `src/app/app.ts` `SuaveApp.initialize()` to call `this._registry.seedServices(...)` before `this._registry.initialize()`
|
||||
- [ ] 2.4 Add `db?: SurrealAdapter` to `SuaveAppOptions` (in `src/types/api.ts` or wherever the options type lives); store on `this._db`. Update `AppBuilder.database(...)` to also store the adapter when provided as a constructed object.
|
||||
- [ ] 2.5 Add `@suaveplan/surrealdb` as an optional peer dep so the `db` slot type is resolvable (peerDependencyMeta.optional: true to avoid forcing the dep on api consumers who don't use SurrealDB)
|
||||
- [ ] 2.6 Tests:
|
||||
- [ ] 2.6.1 `src/app/seed-services.test.ts` — covers the 3 scenarios in the spec delta (plugin resolves app, slots seeded, missing migrationRunner)
|
||||
- [ ] 2.6.2 `src/services/registry-augmentation.test.ts` — covers the side-effect import + type augmentation
|
||||
- [ ] 2.6.3 `src/services/registry-augmentation.spec-conformance.test.ts` — `expectTypeOf` every slot
|
||||
- [ ] 2.6.4 `src/app/options.test.ts` — db option seeded / missing
|
||||
- [ ] 2.7 Sibling `.md` for registry-augmentation module
|
||||
- [ ] 2.8 Gates: same 5 as §1.6 but filtered for `@suaveplan/api`
|
||||
|
||||
## 3. Archive
|
||||
|
||||
- [ ] 3.1 Fold `specs/api-plugin/spec.md` into canonical `openspec/specs/api-plugin/spec.md` (append the new requirement; preserve existing)
|
||||
- [ ] 3.2 Fold `specs/api/spec.md` into canonical `openspec/specs/api/spec.md` (append the new requirements; preserve existing)
|
||||
- [ ] 3.3 Move `openspec/changes/extend-api-foundation-host-services/` → `openspec/archive/<date>-extend-api-foundation-host-services/`
|
||||
- [ ] 3.4 DO NOT refresh MANIFEST or STATUS — orchestrator consolidates after the full chain ships
|
||||
Reference in New Issue
Block a user