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,111 @@
# `registry/`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The participant registry is the install-time admission control between
plugins and the 2PC coordinator. Plugins declare
`twoPhaseCommit.participates: true` in their manifest; the host
registers them with this registry; the registry refuses any writer
whose bound database engine cannot honour `PREPARE TRANSACTION`.
This is where the design doc §9.2 rule "SQLite and SurrealDB MUST NOT
participate as writers" is enforced. The capability check is injected
as a callback so the registry stays decoupled from
`@suaveplan/api-host`'s concrete `CapabilityRegistry`.
## Features
- `TwoPcParticipantRegistry.register(participant)` — refuses writers
without `db.preparable`; accepts read-only voters regardless of
engine.
- `get(id)` / `list()` / `size()` / `unregister(id)` — standard
registry surface.
- Injection seam: `PreparableCapabilityCheck` is a plain
`(id: string) => boolean` callback — zero coupling to api-host.
## Usage: Basic
```ts
import { TwoPcParticipantRegistry } from "@suaveplan/api-2pc/registry";
const isPreparable = (pluginId: string) =>
pluginId === "billing" || pluginId === "inventory";
const registry = new TwoPcParticipantRegistry(isPreparable);
registry.register({
id: "billing",
voterMode: "writer",
connection: { engine: "postgres", run: pg.unsafe.bind(pg) },
prepare: billingPrepareCallback,
});
```
## Usage: Advanced
Refusing a SQLite-backed plugin as a writer happens at register-time
— wrap the registration in a try/catch to surface the failure to the
plugin loader without crashing the host boot:
```ts
import { TWOPC_CODES, TwoPcError } from "@suaveplan/api-2pc/error-codes";
import { TwoPcParticipantRegistry } from "@suaveplan/api-2pc/registry";
function safeRegisterParticipants(
registry: TwoPcParticipantRegistry,
participants: ReadonlyArray<RegisteredTwoPcParticipant>
) {
const failures: { id: string; reason: string }[] = [];
for (const p of participants) {
try {
registry.register(p);
} catch (err) {
if (
err instanceof TwoPcError &&
err.twoPcCode === TWOPC_CODES.PARTICIPANT_NOT_PREPARABLE
) {
failures.push({ id: p.id, reason: err.message });
continue;
}
throw err;
}
}
return failures;
}
```
The same plugin can then re-register as a `read-only` voter — the
registry accepts read-only voters regardless of engine, because the
coordinator never sends them prepare/commit SQL.
## API Reference
- `type PreparableCapabilityCheck = (participantId: string) => boolean`
— capability resolver injected at construction.
- `class TwoPcParticipantRegistry`
- `constructor(isPreparable: PreparableCapabilityCheck)`
- `register(participant): void` — throws
`TWOPC_PARTICIPANT_NOT_PREPARABLE` on writer refusal.
- `get(id): RegisteredTwoPcParticipant | undefined`
- `list(): readonly RegisteredTwoPcParticipant[]`
- `size(): number`
- `unregister(id): boolean`
## Implementation Notes
The registry refuses a writer at `register`-time rather than at
`run2pc`-time. This is deliberate — admission failures should surface
during plugin install, not when the first 2PC transaction is attempted
in production. The capability check fires only for writers; read-only
voters are accepted unconditionally because the coordinator drops their
connection from phase-2 SQL traffic. The second-half guard against
read-only voters that try to write a payload lives in
`phases/prepare.ts` (raising `TWOPC_READ_ONLY_PARTICIPANT_CANNOT_WRITE`
when the participant returns `writeIntent: true`).
`unregister` exists for hot-reload paths: when a plugin is removed
from the running host, the registry drops its participant entry so
subsequent `run2pc` calls cannot accidentally route to it.