Initial Commit
This commit is contained in:
62
openspec/archive/2026-05-18-add-api-plugin/proposal.md
Normal file
62
openspec/archive/2026-05-18-add-api-plugin/proposal.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Add @suaveplan/api-plugin — plugin system foundation (1 of 5)
|
||||
|
||||
**Package**: `@suaveplan/api-plugin` (NEW, universal-tier-server)
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package (split from upstream `@suaveplan/api`)
|
||||
|
||||
- **Spec delta**: ./specs/api-plugin/spec.md
|
||||
- **Companions**: `add-api-tenant`, `add-api-openapi`, `add-api-migrations`, `enterprise-api`
|
||||
- **Position in chain**: foundation — gates all 4 companions
|
||||
|
||||
## Why
|
||||
|
||||
The upstream `@suaveplan/api` package (8.7k LOC) bundles 7 architectural layers into one. The plugin system (`plugin/` + `hooks/` + topo-sort util — 2,034 LOC) is the foundation every other layer depends on. Splitting it out lets the tenant / openapi / migrations / app layers compose against a stable, fully-tested plug-and-play primitive without dragging the whole framework along.
|
||||
|
||||
The upstream plugin system has known design issues that genesis fixes: dual-runtime hook execution (registry calls lifecycle hooks directly AND HookExecutor registers them — duplication hazard), unsafe `getService<T>(name)` casts, unenforced `initTimeout` option, and asymmetric error policy between init (abort-on-throw) and shutdown (swallow-and-log). Forty-plus in-flight `enterprise-api-plugin-*` change folders are all blocked on this landing.
|
||||
|
||||
## What changes
|
||||
|
||||
Net-new package at `0.1.0`. ADDED:
|
||||
|
||||
- `definePlugin<TConfig>(descriptor): PluginDescriptor<TConfig>` — typed plugin factory. Descriptor includes `name`, `version`, `dependencies`, `optionalDependencies`, `priority?`, `enabledByDefault?`, `configSchema?: ZodType`, and lifecycle hooks (`onRegister/onInit/onReady/onClose`).
|
||||
- `PluginRegistry` class — single canonical registry. Methods: `register`, `registerAll`, `get`, `has`, `getAll`, `getSortedPlugins`, `initialize(context)`, `shutdown()`, `disable(name)`. Internally owns the topo-sort + cycle detection.
|
||||
- `PluginContext` — runtime injection: `{ logger: LoggerLike, eventBus: EventBusLike, getOwnConfig<T>(): T, registerService<K extends ServiceKey>(name: K, service: ServiceRegistry[K]): void, getService<K extends ServiceKey>(name: K): ServiceRegistry[K] | undefined, hasService(name: string): boolean, signal: AbortSignal }`.
|
||||
- `ServiceRegistry` interface (empty by default) — designed for **module augmentation** so plugins declare their typed slots: `declare module "@suaveplan/api-plugin/service-registry" { interface ServiceRegistry { db: DbAdapter } }`.
|
||||
- `topologicalSort(nodes): { sorted, unresolved }` — Kahn's algorithm + DFS cycle-detection diagnostic. Equal-in-degree nodes sorted alphabetically by id (deterministic).
|
||||
- `validateDependencies(nodes)` — single canonical validator (upstream has two; we ship one).
|
||||
- `HookExecutor` for **non-lifecycle hooks only** (request/db/custom plugin-contributed hooks). Lifecycle hooks (`onRegister/onInit/onReady/onClose`) are owned by `PluginRegistry` and called once per plugin — never via the executor. Eliminates upstream's dual-runtime hazard.
|
||||
- `PluginError extends SuaveplanError<PluginErrorCode>` with `defineCodes("PLUGIN", [...])` registry. Codes: `PLUGIN_NOT_FOUND`, `PLUGIN_ALREADY_REGISTERED`, `PLUGIN_INIT_FAILED`, `PLUGIN_DEP_MISSING`, `PLUGIN_CIRCULAR_DEPENDENCY`, `PLUGIN_INIT_TIMEOUT`, `PLUGIN_SHUTDOWN_FAILED`, `PLUGIN_INVALID_CONFIG`, `PLUGIN_HOOK_FAILED`, `PLUGIN_ABORTED`.
|
||||
- `PluginRegistryOptions` — `{ initTimeout?: number (default 30000), shutdownTimeout?: number (default 10000), strictOptionalDeps?: boolean, clock?: Clock }`. **`initTimeout` is enforced** — exceeded init rejects with `PluginError(PLUGIN_INIT_TIMEOUT)`. Same for shutdown.
|
||||
- `AbortSignal` propagation — every lifecycle hook receives `ctx.signal`; aborted ops reject with `PluginError(PLUGIN_ABORTED)`.
|
||||
- Subpath exports per feature module: `./define`, `./registry`, `./context`, `./service-registry`, `./hook-executor`, `./topo-sort`, `./error-codes`, `./types`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Tenant lifecycle hooks** (`onTenantCreate/Update/Delete/Suspend/Activate`, `onPluginEnable/Disable`). Belong in `@suaveplan/api-tenant` (sibling change `add-api-tenant`) so non-tenant apps don't pay the surface cost.
|
||||
- **Request lifecycle hooks** (`onRequest/onResponse/onRequestError`). Belong in `@suaveplan/api` orchestrator + middleware layer (`enterprise-api`) because they need Hono request context to fire.
|
||||
- **Database migration auto-application.** The registry's upstream `applyMigrations()` between register/init is dropped. Migrations are owned by `@suaveplan/api-migrations` and run explicitly by the app orchestrator at the appropriate boot phase.
|
||||
- **OpenAPI registry integration.** `@suaveplan/api-openapi` consumes the plugin context but does not live inside the plugin package.
|
||||
- **Filesystem plugin discovery** (`PluginLoader.autoDiscover()`). Has two known bugs upstream (prefix mismatch between filter and rewrite) and adds a Node-only filesystem dep to what is otherwise a universal-tier package. Belongs in a follow-up `@suaveplan/api-plugin-loader` only if a real consumer asks. Explicit `plugins: [foo, bar]` arrays are the v1 contract.
|
||||
|
||||
## Source mapping
|
||||
|
||||
| Capability | Upstream source | Notes |
|
||||
|---|---|---|
|
||||
| Plugin interface + descriptor | `../suaveplan/packages/server/api/src/plugin/plugin.interface.ts` L1-L201 | port shape; switch hook surface to 4 lifecycle hooks only (others move to sibling packages) |
|
||||
| `definePlugin` factory | `../suaveplan/packages/server/api/src/plugin/plugin-metadata.ts` L77-L123 | port shape; drop decorator helpers (require `reflect-metadata`) |
|
||||
| `PluginRegistry` class | `../suaveplan/packages/server/api/src/plugin/plugin-registry.ts` L44-L433 | port + enforce `initTimeout`, unify lifecycle runtime, switch errors to `defineCodes` registry, drop `applyMigrations` |
|
||||
| `PluginContext` | `../suaveplan/packages/server/api/src/plugin/plugin-context.ts` L43-L227 | port shape; `registerService`/`getService` become typed via `ServiceRegistry` module augmentation; drop `getPluginConfig(name)` cross-plugin coupling; ALS-based `getCurrentXxx()` helpers dropped (callers thread `ctx` explicitly) |
|
||||
| `topologicalSort` + `detectCycle` + `validateDependencies` | `../suaveplan/packages/server/api/src/utils/topological-sort.ts` L1-L157 | verbatim port + alphabetical tie-break for stable order; drop duplicate `validateDependencies` in registry |
|
||||
| `HookExecutor` (priority-ordered dispatch) | `../suaveplan/packages/server/api/src/hooks/hook-executor.ts` L1-L221 | port shape; **excludes the 4 lifecycle hook slots** (those are registry-owned); switch to `defineCodes` errors |
|
||||
| Plugin error subclasses | `../suaveplan/packages/server/api/src/errors/plugin-errors.ts` L1-L97 | replace with `defineCodes("PLUGIN", [...])` registry + `PluginError` discriminant |
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
None today — net-new on genesis. Forward-declared consumers (all sibling changes in this set + the 40+ in-flight `enterprise-api-plugin-*` packages):
|
||||
|
||||
- `@suaveplan/api-tenant` (sibling) — adds tenant lifecycle hooks on top
|
||||
- `@suaveplan/api-openapi` (sibling) — registers routes via plugin `onInit`
|
||||
- `@suaveplan/api` (sibling) — composes the registry into `SuaveApp`
|
||||
- Every `@suaveplan/api-plugin-*` package (40+ in `openspec/changes/`) — implements `PluginDescriptor`
|
||||
@@ -0,0 +1,145 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: api-plugin (add-api-plugin)
|
||||
|
||||
**Package**: `@suaveplan/api-plugin`
|
||||
**Status**: PROPOSED
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: definePlugin factory produces a typed PluginDescriptor
|
||||
|
||||
The package SHALL export `definePlugin<TConfig>(descriptor): PluginDescriptor<TConfig>` that returns the descriptor with full `TConfig` type inference flowing through `configSchema?: ZodType<TConfig>`, `defaults?: Partial<TConfig>`, and the 4 lifecycle hooks (`onRegister/onInit/onReady/onClose`), each receiving a `PluginContext` typed for `TConfig`.
|
||||
|
||||
#### Scenario: descriptor preserves TConfig through the type system
|
||||
|
||||
- **WHEN** `definePlugin({ name, version, configSchema: MyConfigSchema, onInit: (ctx) => ctx.getOwnConfig() })` is invoked
|
||||
- **THEN** `ctx.getOwnConfig()` returns `z.infer<typeof MyConfigSchema>` (no `unknown` widening)
|
||||
|
||||
**Verified by:** `src/define/define.test.ts::"definePlugin preserves TConfig type"` AND `src/define/define.spec-conformance.test.ts::"expectTypeOf getOwnConfig matches configSchema infer"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PluginRegistry runs the 4-phase lifecycle in dep order
|
||||
|
||||
`PluginRegistry.initialize(context)` SHALL run topologically-sorted plugins through `onRegister → onInit → onReady` in that order, calling each hook on each plugin exactly once. `shutdown()` SHALL run `onClose` in **reverse** dep order. Each hook receives a `PluginContext` whose `signal: AbortSignal` is bound to the registry's `initSignal` / `shutdownSignal`.
|
||||
|
||||
#### Scenario: lifecycle hooks fire in dep order
|
||||
|
||||
- **WHEN** plugins `a` (no deps) and `b` (depends on `a`) register, and `initialize()` is awaited
|
||||
- **THEN** `a.onRegister`, `a.onInit`, `a.onReady` complete before any of `b`'s lifecycle hooks
|
||||
|
||||
#### Scenario: shutdown reverses the order
|
||||
|
||||
- **WHEN** the same registry is shut down
|
||||
- **THEN** `b.onClose` completes before `a.onClose`
|
||||
|
||||
**Verified by:** `src/registry/registry.test.ts::"initialize runs onRegister/onInit/onReady in dep order"` AND `src/registry/registry.test.ts::"shutdown calls onClose in reverse dep order"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Topological sort uses Kahn's algorithm with deterministic tie-break
|
||||
|
||||
`topologicalSort(nodes)` SHALL implement Kahn's algorithm. Equal-in-degree nodes SHALL be processed in alphabetical order of `id` (stable across runs, independent of registration order). Missing required deps go into the `unresolved` array. Cycles raise `PluginError(PLUGIN_CIRCULAR_DEPENDENCY)` carrying the offending cycle path.
|
||||
|
||||
#### Scenario: equal-in-degree nodes sort alphabetically
|
||||
|
||||
- **WHEN** plugins `z`, `a`, `m` are registered in that order with no deps
|
||||
- **THEN** `getSortedPlugins()` returns them in order `[a, m, z]` regardless of registration order
|
||||
|
||||
#### Scenario: cycle throws with cycle path
|
||||
|
||||
- **WHEN** plugins form a cycle `a → b → c → a` and `initialize()` is awaited
|
||||
- **THEN** the promise rejects with `PluginError(PLUGIN_CIRCULAR_DEPENDENCY)` whose `context.cycle` equals `["a", "b", "c", "a"]`
|
||||
|
||||
**Verified by:** `src/topo-sort/topo-sort.test.ts::"sorts equal-in-degree nodes alphabetically"` AND `src/topo-sort/topo-sort.test.ts::"throws PLUGIN_CIRCULAR_DEPENDENCY with cycle path"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PluginRegistry enforces initTimeout per hook
|
||||
|
||||
The registry SHALL accept `initTimeout` (default 30000ms) and `shutdownTimeout` (default 10000ms) in `PluginRegistryOptions`. If any single lifecycle hook on any plugin exceeds the per-hook budget, the registry SHALL reject with `PluginError(PLUGIN_INIT_TIMEOUT)` carrying `context.plugin` and `context.hook`. Timeout uses the injected `Clock` and is implemented via `AbortSignal` (the plugin's `ctx.signal` is aborted when the timer fires).
|
||||
|
||||
#### Scenario: hung onInit hits the timeout
|
||||
|
||||
- **WHEN** a plugin's `onInit` returns a Promise that never resolves and `initialize()` is awaited with `initTimeout: 100`
|
||||
- **THEN** the promise rejects within ~100ms with `PluginError(PLUGIN_INIT_TIMEOUT)` whose `context.plugin` is the offending plugin name
|
||||
|
||||
**Verified by:** `src/registry/registry.test.ts::"initTimeout aborts a hung onInit"` AND `src/registry/registry.test.ts::"shutdownTimeout aborts a hung onClose"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: PluginContext.getService is type-safe via ServiceRegistry module augmentation
|
||||
|
||||
The package SHALL export `interface ServiceRegistry {}` (empty by default) from `./service-registry`. `registerService<K extends keyof ServiceRegistry>(name: K, service: ServiceRegistry[K])` and `getService<K extends keyof ServiceRegistry>(name: K): ServiceRegistry[K] | undefined` SHALL be typed against augmentations. Untyped string keys (not in `ServiceRegistry`) SHALL fail TypeScript compilation; runtime behaviour preserved for forward compatibility.
|
||||
|
||||
#### Scenario: augmented service slot types correctly
|
||||
|
||||
- **WHEN** a consumer declares `declare module "@suaveplan/api-plugin/service-registry" { interface ServiceRegistry { db: DbAdapter } }` and calls `ctx.getService("db")`
|
||||
- **THEN** the return type is `DbAdapter | undefined` (no cast required)
|
||||
|
||||
#### Scenario: unknown service name fails compilation
|
||||
|
||||
- **WHEN** a consumer calls `ctx.getService("unknown-slot")` without augmenting `ServiceRegistry`
|
||||
- **THEN** the compile fails
|
||||
|
||||
**Verified by:** `src/context/context.spec-conformance.test.ts::"expectTypeOf getService(K) returns ServiceRegistry[K] | undefined"` AND `src/context/context.spec-conformance.test.ts::"unknown service key produces a type error"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: HookExecutor handles non-lifecycle hooks only
|
||||
|
||||
The package SHALL ship `HookExecutor` with `registerHook(name, handler, pluginName, priority)`, `unregisterPlugin(name)`, `execute(name, ...args)` (sequential, abort on first throw → `PluginError(PLUGIN_HOOK_FAILED)`), and `executeAll(name, ...args)` (parallel via `Promise.allSettled`). Lifecycle hooks (`onRegister`/`onInit`/`onReady`/`onClose`) SHALL NOT be dispatched via the executor — registry owns them. The executor's hook namespace is for plugin-contributed custom hooks AND the request/tenant/db hooks that ship in sibling packages.
|
||||
|
||||
#### Scenario: registry does not invoke lifecycle hooks via executor
|
||||
|
||||
- **WHEN** a plugin's `onInit` is called by the registry and the executor is queried for `getHookCount("onInit")`
|
||||
- **THEN** the count is 0 — lifecycle hooks are not registered with the executor
|
||||
|
||||
#### Scenario: custom hooks dispatch with priority
|
||||
|
||||
- **WHEN** two plugins register handlers for a custom hook `"my-extension"` with priorities 10 and 5, then `executor.execute("my-extension", ...args)` runs
|
||||
- **THEN** the priority-10 handler runs first
|
||||
|
||||
**Verified by:** `src/hook-executor/hook-executor.test.ts::"lifecycle hooks are not registered with the executor"` AND `src/hook-executor/hook-executor.test.ts::"dispatch order respects priority"` AND `src/hook-executor/hook-executor.test.ts::"execute aborts on first throw with PLUGIN_HOOK_FAILED"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Every PLUGIN_* error code is thrown from a production path
|
||||
|
||||
`PLUGIN_CODES` SHALL define exactly 10 codes (`PLUGIN_NOT_FOUND`, `PLUGIN_ALREADY_REGISTERED`, `PLUGIN_INIT_FAILED`, `PLUGIN_DEP_MISSING`, `PLUGIN_CIRCULAR_DEPENDENCY`, `PLUGIN_INIT_TIMEOUT`, `PLUGIN_SHUTDOWN_FAILED`, `PLUGIN_INVALID_CONFIG`, `PLUGIN_HOOK_FAILED`, `PLUGIN_ABORTED`). Every code SHALL be raised by at least one production call site.
|
||||
|
||||
#### Scenario: spec conformance exercises every code
|
||||
|
||||
- **WHEN** the conformance test exercises each code's production path
|
||||
- **THEN** every code appears in the assertions
|
||||
|
||||
**Verified by:** `src/api-plugin.spec-conformance.test.ts::"every PLUGIN_* code is thrown from a production path"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Universal-tier purity, subpath exports, sibling docs
|
||||
|
||||
The package SHALL run unchanged in Bun, Node, and the browser (no `node:fs`, no `process.cwd()` outside lazy CLI paths). `package.json` SHALL declare a subpath export per feature module. Every implementation `.ts` SHALL have a sibling `.md` of ≥200 words. No `export *` in feature barrels.
|
||||
|
||||
#### Scenario: dep-graph asserts package shape
|
||||
|
||||
- **WHEN** `tests/dep-graph.test.ts` inspects package.json `dependencies` + `exports`
|
||||
- **THEN** dependencies are restricted to `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/types`, `tslib`, `zod`, `@opentelemetry/api`; every feature module is reachable via a declared subpath
|
||||
|
||||
**Verified by:** `tests/dep-graph.test.ts::"dependencies match the universal allowlist"` AND `tests/dep-graph.test.ts::"subpath exports cover every feature module"` AND `tests/dep-graph.test.ts::"every implementation .ts has a sibling .md"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 100% coverage with property test for topo-sort stability
|
||||
|
||||
The package SHALL enforce 100/100/100/100 statements/branches/functions/lines via Istanbul. `tests/property.test.ts` SHALL use fast-check (numRuns: 200) to assert that for arbitrary acyclic dep graphs `[id → deps]`, `topologicalSort` produces an order in which every node appears after every one of its declared dependencies AND the order is reproducible (calling sort twice on the same input yields identical output).
|
||||
|
||||
#### Scenario: dep-order invariant holds for arbitrary graphs
|
||||
|
||||
- **WHEN** fast-check generates random acyclic dep graphs and `topologicalSort` is called
|
||||
- **THEN** for every node N in the result, every dep of N appears earlier in the result
|
||||
|
||||
**Verified by:** `tests/property.test.ts::"sorted order satisfies dep invariant"` AND `tests/property.test.ts::"sort is deterministic across repeated calls"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
50
openspec/archive/2026-05-18-add-api-plugin/tasks.md
Normal file
50
openspec/archive/2026-05-18-add-api-plugin/tasks.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Tasks — add-api-plugin
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [ ] 1.1 Create `packages/api/api-plugin/` with `src/{define,registry,context,service-registry,hook-executor,topo-sort,error-codes,types}/` + `tests/setup/`
|
||||
- [ ] 1.2 `package.json` v0.1.0, type module, sideEffects false. Deps: `@suaveplan/error` `@suaveplan/events` `@suaveplan/types` `tslib` (`workspace:^` / `catalog:build`), `zod` (`catalog:forms`), `@opentelemetry/api` (`catalog:observability`). Dev: `@suaveplan/testing` `@vitest/coverage-istanbul` `vitest` `fast-check` (`catalog:testing`), `@types/node` `typescript` (`catalog:`), `vite` `vite-plugin-dts` (`catalog:build`). 8 subpath exports.
|
||||
- [ ] 1.3 `tsconfig.json` + `tsconfig.lib.json` extending `../../../tsconfig.base.json`
|
||||
- [ ] 1.4 `vite.config.ts` (dts, ES, externals)
|
||||
- [ ] 1.5 `vitest.config.ts` (`pool: forks`, `provider: istanbul`, thresholds 100/100/100/100, `testTimeout: 30000`, `setupFiles: ['./tests/setup/polyfills.ts']`)
|
||||
- [ ] 1.6 `tests/setup/polyfills.ts`
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [ ] 2.1 `src/error-codes.ts` + `.md` + test — 10 PLUGIN.* codes + `PluginError`
|
||||
- [ ] 2.2 `src/types/api-plugin.ts` + `.md` — `PluginDescriptor`, `PluginInstance`, `PluginState` (`pending`/`registered`/`initializing`/`initialized`/`ready`/`disabled`/`error`), `PluginNode`
|
||||
- [ ] 2.3 `src/topo-sort/topo-sort.{ts,test.ts,md,index.ts}` — Kahn's + DFS cycle detect + alpha tie-break; export `validateDependencies`
|
||||
- [ ] 2.4 `src/service-registry/service-registry.{ts,md,index.ts}` — empty `ServiceRegistry` interface for module augmentation, `ServiceKey = keyof ServiceRegistry` helper
|
||||
- [ ] 2.5 `src/context/context.{ts,test.ts,md,index.ts}` — `createPluginContext(opts)`; typed `getService`/`registerService`/`hasService`; `getOwnConfig<T>()`; `signal: AbortSignal`
|
||||
- [ ] 2.6 `src/hook-executor/hook-executor.{ts,test.ts,md,index.ts}` — priority-sorted handlers, `execute` (sequential abort-on-throw) + `executeAll` (parallel allSettled); does NOT include lifecycle hook slots
|
||||
- [ ] 2.7 `src/define/define.{ts,test.ts,md,index.ts}` — `definePlugin` factory with TConfig inference
|
||||
- [ ] 2.8 `src/registry/registry.{ts,test.ts,md,index.ts}` — `PluginRegistry` class; topo-sort delegation; 4-phase lifecycle; `initTimeout`/`shutdownTimeout` enforcement via AbortSignal; `defineCodes`-typed errors; OTel span per hook
|
||||
- [ ] 2.9 `src/index.ts` — named re-exports only
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [ ] 3.1 Co-located `*.test.ts` per spec scenario
|
||||
- [ ] 3.2 `src/api-plugin.spec-conformance.test.ts` — every PLUGIN.* thrown from production; `expectTypeOf` on `definePlugin<TConfig>` + `getService<K>` + `getOwnConfig<T>`
|
||||
- [ ] 3.3 `tests/property.test.ts` — fast-check topo-sort dep invariant + determinism (200 runs each)
|
||||
- [ ] 3.4 `tests/dep-graph.test.ts` — deps allowlist + subpath coverage + sibling .md presence
|
||||
- [ ] 3.5 All tests import runner from `@suaveplan/testing/runner`
|
||||
- [ ] 3.6 Every `it(...)` has `{ timeout: 5000 }` (30000 for AbortSignal-timeout tests)
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [ ] 4.1 Sibling `.md` per impl file (≥200 words)
|
||||
- [ ] 4.2 `README.md` — Quick-start, Modules list, ServiceRegistry augmentation example, sample plugin
|
||||
|
||||
## 5. Gates
|
||||
|
||||
- [ ] 5.1 `bunx turbo lint typecheck test --filter=@suaveplan/api-plugin --force` — green
|
||||
- [ ] 5.2 `bunx turbo test:coverage --filter=@suaveplan/api-plugin --force` — 100/100/100/100
|
||||
- [ ] 5.3 `bun scripts/openspec-conformance.ts @suaveplan/api-plugin` — exit 0
|
||||
- [ ] 5.4 `bunx turbo build --filter=@suaveplan/api-plugin --force` — green
|
||||
- [ ] 5.5 Banned-pattern grep clean; no `export *` in feature barrels; no direct vitest/bun:test imports
|
||||
|
||||
## 6. Archive
|
||||
|
||||
- [ ] 6.1 Fold `specs/api-plugin/spec.md` → `openspec/specs/api-plugin/spec.md`
|
||||
- [ ] 6.2 Move change folder → `openspec/archive/<yyyy-mm-dd>-add-api-plugin/`
|
||||
- [ ] 6.3 Refresh MANIFEST + STATUS (`@suaveplan/api-plugin` → ✅)
|
||||
Reference in New Issue
Block a user