# Capability Spec Delta: api-lifecycle (api-lifecycle) **Package**: `@suaveplan/api-lifecycle` **Status**: PROPOSED > Every requirement carries a `**Verified by:**` line. ## ADDED Requirements ### Requirement: PluginState union pinned to design §4 The package SHALL export `PluginState` (re-exported from `@suaveplan/api-types`) with exactly: `"uninstalled" | "installing" | "installed" | "starting" | "started" | "rolled_back" | "crashed" | "stopping" | "stopped" | "uninstalling" | "install_failed" | "start_failed" | "uninstall_failed"`. The package SHALL also export `PLUGIN_STATES` as a runtime tuple matching the union. #### Scenario: union matches design §4 exactly - **WHEN** `expectTypeOf()` is checked against the 13 literals - **THEN** the union is exactly those 13 — no more, no less #### Scenario: runtime tuple matches the union - **WHEN** the test asserts `PLUGIN_STATES` covers every union member - **THEN** every member is present in the tuple **Verified by:** `src/states/states.spec-conformance.test.ts::"PluginState union matches design §4 exactly"` AND `src/states/states.test.ts::"PLUGIN_STATES tuple covers every union member"` --- ### Requirement: Transitions table is exhaustive The package SHALL export `PluginTransitions` — a frozen `(from, to, trigger)` table. Every transition documented in design §4's table MUST be present. A transition not in the table MUST throw `LIFECYCLE.ILLEGAL_TRANSITION` when attempted. #### Scenario: every documented transition is legal - **WHEN** the test enumerates the §4 table - **THEN** every documented `(from, to)` pair has an entry in `PluginTransitions` #### Scenario: undocumented transition rejected - **WHEN** `transition({ from: "installed", to: "stopped" })` is attempted (illegal: must pass through `starting`/`stopping`) - **THEN** `LIFECYCLE.ILLEGAL_TRANSITION` is thrown AND `details.from === "installed"` AND `details.to === "stopped"` **Verified by:** `src/transitions/transitions.test.ts::"every documented transition is legal"` AND `src/transitions/transitions.test.ts::"undocumented transition rejected"` --- ### Requirement: createPluginFsm wires `@suaveplan/fsm` The package SHALL export `createPluginFsm({ initial, sideEffects, mutex })` returning a `Fsm` backed by `@suaveplan/fsm`. Transitions MUST honour the mutex contract (FIFO queue, at most one in-flight per plugin). #### Scenario: fsm starts in declared initial state - **WHEN** `createPluginFsm({ initial: "uninstalled", ... })` is created - **THEN** `fsm.state === "uninstalled"` #### Scenario: concurrent triggers queue - **WHEN** two `install` triggers are dispatched concurrently - **THEN** the second waits until the first completes AND completes against the post-first state (likely rejecting if the FSM left `uninstalled`) **Verified by:** `src/fsm-factory/fsm-factory.test.ts::"fsm starts in declared initial state"` AND `src/mutex-contract/mutex-contract.test.ts::"concurrent triggers queue FIFO"` --- ### Requirement: Side-effect descriptors per transition The package SHALL export `SIDE_EFFECTS`: a map from `(from, to)` to the documented side-effect list. `installing → installed` MUST emit `plugin.installed` and complete the OTel span. `started → stopping` MUST pause jobs, unsubscribe events, unmount routes with 503 + Retry-After, call user `stop` hook with `gracefulMs` (default 30000). `stopping → stopped` MUST force-cancel stragglers AND dispose capabilities in reverse-registration order. Every transition documented in §4's table MUST appear here. #### Scenario: every documented side-effect is in the table - **WHEN** the test compares `SIDE_EFFECTS` keys against §4's transition table - **THEN** every documented transition has an entry #### Scenario: capability disposal order is reverse-registration - **WHEN** the `stopping → stopped` descriptor is inspected - **THEN** it declares `disposalOrder: "reverse-registration"` **Verified by:** `src/side-effects/side-effects.test.ts::"every documented side-effect is in the table"` AND `src/side-effects/side-effects.test.ts::"stopping→stopped disposes in reverse-registration order"` --- ### Requirement: Retry policies The package SHALL export `RETRY_POLICIES.startFailed` (3 attempts, exponential backoff via `@suaveplan/retry`) and `RETRY_POLICIES.crashed` (default `crashRestartMax: 3`, `crashRestartWindowMs`, `crashRestartCooldownMs`). The `crashed` budget MUST reset after `crashRestartCooldownMs` of healthy operation. Exhausting any budget MUST surface `LIFECYCLE.RETRY_BUDGET_EXHAUSTED`. #### Scenario: start_failed retried 3 times then parked - **WHEN** the start hook throws 4 times in a row - **THEN** the 4th attempt does NOT retry; the plugin is parked AND `LIFECYCLE.RETRY_BUDGET_EXHAUSTED` is the failure code #### Scenario: crashed budget resets after cooldown - **WHEN** a plugin crashes twice within the window, then runs healthily for `crashRestartCooldownMs`, then crashes again - **THEN** the post-cooldown crash is treated as the first crash in a new budget window **Verified by:** `src/retry-policies/retry-policies.test.ts::"start_failed retried 3 times then parked"` AND `src/retry-policies/retry-policies.test.ts::"crashed budget resets after cooldown"` --- ### Requirement: requiresReinstall criteria The package SHALL export `requiresReinstall(prev: PluginManifest, next: PluginManifest): boolean` returning `true` IFF any of: `next.database.schemaVersion > prev.database.schemaVersion`; `next.scopes` adds names not in `prev.scopes`; `next.events.publishes` or `next.events.subscribes` add new topics; `next.jobs.cron` or `next.jobs.queued` add new names; `next.secrets` adds a new secret; `next.dependsOn` differs from `prev.dependsOn`. A downgrade (`next.version < prev.version`) MUST throw `LIFECYCLE.REINSTALL_REQUIRED_VERSION_DOWNGRADE`. #### Scenario: identical manifest returns false - **WHEN** `prev` and `next` are deep-equal - **THEN** `requiresReinstall(prev, next) === false` #### Scenario: schemaVersion bump returns true - **WHEN** `next.database.schemaVersion` is greater than `prev`'s - **THEN** the result is `true` #### Scenario: version downgrade rejected - **WHEN** `semver.lt(next.version, prev.version)` is true - **THEN** the call throws `LIFECYCLE.REINSTALL_REQUIRED_VERSION_DOWNGRADE` **Verified by:** `src/reinstall-criteria/reinstall-criteria.test.ts::"identical manifest returns false"` AND `src/reinstall-criteria/reinstall-criteria.test.ts::"schemaVersion bump returns true"` AND `src/reinstall-criteria/reinstall-criteria.test.ts::"version downgrade throws"` --- ### Requirement: Mutex contract The package SHALL export `PluginMutex` interface: `acquire(plugin: string): Promise`, `tryAcquire(plugin: string): Release | null`. At most one acquired release per plugin. Concurrent acquires queue FIFO. The interface is types-only; concrete impl lives in `api-host`. #### Scenario: every documented method present - **WHEN** `expectTypeOf()` is checked - **THEN** `acquire` and `tryAcquire` are declared #### Scenario: tryAcquire returns null when held - **WHEN** the conformance harness exercises a stub: acquire then `tryAcquire` - **THEN** the second call returns `null` **Verified by:** `src/mutex-contract/mutex-contract.spec-conformance.test.ts::"PluginMutex declares acquire tryAcquire"` AND `src/mutex-contract/mutex-contract.test.ts::"tryAcquire returns null when held"` --- ### Requirement: Error codes registry frozen The package SHALL export `LIFECYCLE_CODES` containing exactly: `ILLEGAL_TRANSITION`, `MUTEX_HELD`, `RETRY_BUDGET_EXHAUSTED`, `REINSTALL_REQUIRED_VERSION_DOWNGRADE`. #### Scenario: every code reachable - **WHEN** the test enumerates `LIFECYCLE_CODES` - **THEN** every code has a production throw site **Verified by:** `src/error-codes/error-codes.spec-conformance.test.ts::"every LIFECYCLE_CODES entry thrown by production path"` --- ### Requirement: Conformance harness for host driver The package SHALL export `runLifecycleConformance({ factory, label })`. The Stage-2 `api-host` driver MUST pass it. #### Scenario: harness covers states, transitions, side-effects, retry budgets, mutex, reinstall criteria - **WHEN** the harness runs against a stub driver - **THEN** test categories exist for each documented behavior **Verified by:** `src/testing/conformance.test.ts::"harness covers states transitions side-effects retry-budgets mutex reinstall-criteria"`