Files
2026-05-27 23:29:53 +01:00

6.1 KiB
Raw Permalink Blame History

@suaveplan/api-lifecycle — plugin lifecycle FSM definition

Package: @suaveplan/api-lifecycle Status: PROPOSED Type: new-package Stage: 1 (Contracts) — see umbrella openspec/changes/api-plugin-foundation/

Why

Design §4 specifies the plugin lifecycle as a finite-state machine with 13 states (uninstalled, installing, installed, starting, started, rolled_back, crashed, stopping, stopped, uninstalling, install_failed, start_failed, uninstall_failed) and a documented set of transitions. The host driver, the admin API status renderer, the hot-reload primitive, and operator tooling all need to agree on which transitions are legal — and a graph that lives in only one place. This package owns the FSM definition: the states (as a discriminated union), the transition function, the side-effect descriptors per transition (so the host can wire them), and the per-plugin mutex contract that guarantees at-most-one transition at a time. Reuses @suaveplan/fsm for the state-machine primitive — this package is the FSM definition, not a new FSM runtime.

Per design §4: "The FSM has a per-plugin mutex — at most one transition at a time. Concurrent triggers queue."

What changes

Net-new package. Pure definition: states + transitions + side-effect descriptors + retry policies. No concrete plugin code; that lives in api-host (Stage 2).

  • ADDED: ./states — PluginState discriminated union (re-imported from @suaveplan/api-types for runtime introspection); state-shape descriptors.
  • ADDED: ./transitions — PluginTransitions: the legal (from, to, trigger) set as a frozen table. Trigger names: install, installCompleted, installFailed, start, startCompleted, startFailed, rollbackTriggered, stop, stopCompleted, uninstall, uninstallCompleted, uninstallFailed, crashDetected, crashRestart, clearRollback, retryInstall, retryStart, forceUninstall, repair.
  • ADDED: ./fsm-factory — createPluginFsm({ initial, sideEffects, mutex }) returns an FSM bound to the genesis @suaveplan/fsm runtime, initialised with PluginTransitions and the side-effect bus from design §4's transition table.
  • ADDED: ./side-effects — descriptors for each transition's side effects (per §4 table): installing → installed emits plugin.installed, completes OTel span; stopping → stopped force-cancels stragglers, disposes capabilities reverse-registration order; etc. Descriptors are data; the host runtime executes them.
  • ADDED: ./retry-policies — start_failed auto-retry × 3 with @suaveplan/retry exponential backoff; crashed auto-restart × crashRestartMax (default 3); restart budget reset after crashRestartCooldownMs of healthy operation. Default values exported.
  • ADDED: ./mutex-contract — PluginMutex interface: at most one transition per plugin in flight; concurrent triggers queue (FIFO).
  • ADDED: ./reinstall-criteria — requiresReinstall(prev, next): boolean returning true when the new manifest bumps database.schemaVersion, declares new scopes/events/jobs/secrets, or changes dependsOn (per §4 reload semantics).
  • ADDED: ./error-codes — frozen LIFECYCLE_CODES: ILLEGAL_TRANSITION, MUTEX_HELD, RETRY_BUDGET_EXHAUSTED, REINSTALL_REQUIRED_VERSION_DOWNGRADE.
  • ADDED: ./testing — runLifecycleConformance(factory) reusable harness.

Out of scope

  • The host's lifecycle DRIVER (the code that actually CALLS install/start/stop hooks and reads side-effect descriptors) — lives in api-host (Stage 2).
  • Plugin ledger persistence — api-host.
  • Worker-tier crashed state restart implementation — api-host + api-worker-host (Stage 3).

Source mapping

Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.

Requirement Design doc section
State machine (13 states) §4 (FSM ASCII diagram, lines 409-473)
Transition side-effects table §4 (table, lines 475-484)
Reinstall criteria (schemaVersion bumped, new scopes/events/jobs/secrets, dependsOn changed) §4 (line 486)
start_failed auto-retry × 3 + rolled_back path §4 (failure recovery section, lines 491-493)
crashed auto-restart × crashRestartMax, cooldown §4 (line 493)
Per-plugin mutex; concurrent triggers queue §4 (final paragraph)
VERSION_DOWNGRADE failure §4 (line 486)

Requirements with no source line

Requirement Justification
Trigger naming (installCompleted etc.) Mechanically derived from the §4 table; pinned here so the host and admin renderers agree.
runLifecycleConformance harness Stage-5 deliverable (§13.4).

Verification approach

  • src/states/states.test.ts — PluginState union matches design §4 exactly (no missing / extra states).
  • src/transitions/transitions.test.ts — exhaustive transition table: every documented (from, to) pair is legal; an undocumented pair throws LIFECYCLE.ILLEGAL_TRANSITION.
  • src/fsm-factory/fsm-factory.test.ts — createPluginFsm wires @suaveplan/fsm correctly; transition emits the right side-effect descriptors.
  • src/side-effects/side-effects.test.ts — every documented side effect is in the table for the right transition (installed → starting mounts routes, etc.).
  • src/retry-policies/retry-policies.test.ts — start_failed retried up to 3 times then parked; crashed restarted up to crashRestartMax; budget reset after cooldown.
  • src/mutex-contract/mutex-contract.test.ts — concurrent triggers queue FIFO; second trigger waits until first releases mutex.
  • src/reinstall-criteria/reinstall-criteria.test.ts — every documented trigger (schemaVersion, new scope/event/job/secret, dependsOn change) yields true; identical manifest yields false; version downgrade yields REINSTALL_REQUIRED_VERSION_DOWNGRADE.
  • tests/dep-graph.test.ts — deps: @suaveplan/api-types, @suaveplan/fsm, @suaveplan/retry, @suaveplan/error, @suaveplan/types.

Migration impact

None — additive only.

Impact

Affected dependents

  • (none) — [x] no action needed (reason: net-new; first consumer is api-host in Stage 2)