Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
# `persistence`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`SagaPersistence` is the sole path through which the orchestrator
mutates the host's saga tables (`suaveplan_saga_instances` and
`suaveplan_saga_steps` from `api-host` migration 2). The class wraps
the host's `LedgerDb` view with prepared statements and funnels every
state transition through the `state/state-machine` guard so an
in-flight bug in the orchestrator cannot bypass the lifecycle
invariants — a forbidden transition raises
`SAGA.INVALID_STATE_TRANSITION` before the DB row is touched.
## Features
- Prepared-statement CRUD on `suaveplan_saga_instances` keyed by
`(id)` and `suaveplan_saga_steps` keyed by `(saga_id, step_index)`.
- Per-instance payload bag (`PersistedInstancePayload`) carrying the
saga name, tenant ids, declared steps, principal, and
`currentStepIndex` so recovery can resume after a host restart.
- Per-step payload bag (`PersistedStepPayload`) carrying the input,
output, last error, attempt counters, and compensation attempts.
- State transitions guarded — every `transitionInstance` /
`transitionStep` call runs through `assertInstanceTransition` /
`assertStepTransition`. Same-state transitions are accepted as
idempotent no-ops.
- Multi-axis filtering on `listInstances` (state, sagaName, tenantId,
pluginName) so the recovery runner can scope its replay to
`state IN (running, compensating)`.
- Defensive JSON parsing — any malformed `payload_json` /
`compensation_payload_json` row surfaces
`SAGA.PERSISTENCE_FAILED` with the row id in context.
## Usage: Basic
```ts
import { SagaPersistence } from "@suaveplan/api-saga/persistence/persistence";
const persistence = new SagaPersistence({ db: host.ledgerDb });
const instance = persistence.createInstance({
id: "s_1",
pluginName: "billing",
state: "running",
payload: {
sagaName: "checkout",
tenantId: "acme",
initiatorTenantId: "acme",
principalId: "u_42",
steps: [
{ plugin: "billing", action: "charge", compensate: "refund" },
],
input: { amount: 1000 },
currentStepIndex: 0,
},
});
persistence.transitionInstance(instance.id, "completed");
```
## Usage: Advanced
Recovery replay walks every non-terminal instance and resumes from
its persisted `currentStepIndex`. Step rows already in `completed`
are skipped; an `in_flight` row is re-emitted (the idempotency guard
deduplicates the underlying action call):
```ts
import { SagaPersistence } from "@suaveplan/api-saga";
const persistence = new SagaPersistence({ db });
const inflight = persistence.listInstances({ state: "running" });
for (const inst of inflight) {
const steps = persistence.listSteps(inst.id);
const next = steps.find((s) => s.state !== "completed");
if (next === undefined) {
persistence.transitionInstance(inst.id, "completed");
continue;
}
if (next.state === "in_flight") {
orchestrator.replayStep(inst, next);
} else {
orchestrator.dispatchStep(inst, next);
}
}
```
## API Reference
- `SagaPersistence({ db, now? })` — construct against an already
migrated `LedgerDb`.
- Instance: `createInstance`, `getInstance`, `requireInstance`,
`listInstances(filter)`, `transitionInstance(id, to)`,
`updateInstancePayload(id, payload)`, `deleteInstance(id)`.
- Step: `upsertStep`, `getStep`, `listSteps`, `transitionStep`,
`updateStepPayload`.
- Filter type `SagaInstanceListFilter` accepts any subset of
`{ state, sagaName, tenantId, pluginName }`.
## Implementation Notes
`api-host` migration 2 already ships the two tables — this package
does NOT own migrations; it consumes the host-owned schema. The
`payload_json` and `compensation_payload_json` columns are typed
`TEXT` in SQLite; we JSON-encode the matching TS types and validate
on read so a hand-edited row that drops a required field surfaces
loud rather than silently undefined-ing through the orchestrator.
Same-state transitions intentionally short-circuit before the DB
write so a replay pass over an already-correctly-stored saga is a
no-op rather than an unnecessary row touch — critical for the
recovery runner's "boot resume" path that may re-assert state for
many instances at once.