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,82 @@
# `registry`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `PluginRegistry` is the **session-restore lookup table** that maps a serialised `pluginType` string (a plugin's `metadata.id`) to a fresh-instance factory. `PluginChain.restore(snapshot, registry)` calls into it once per slot in the snapshot to rehydrate the original chain composition. The registry is intentionally external to the chain so consumers control which plugins are available for restore: omit a factory and any snapshot referencing that `pluginType` will be rejected with `AUDIO_FX.CHAIN_SLOT_NOT_FOUND`, which is the right shape for sandboxed projects, plugin-blacklist policies, and CI fixtures that need to fail loud on missing dependencies.
The registry deliberately holds factories, not singletons. Audio plugins own per-instance DSP state (delay lines, filter histories, voice pools); two chains restoring the same `pluginType` MUST receive independent plugin instances so disposing one chain never affects another.
## Features
- `register(pluginType, factory)` stores a synchronous factory. Re-registering the same `pluginType` overwrites the previous factory — useful for tests that inject doubles without rebuilding a global registry.
- `has(pluginType)` cheap presence check that does not invoke the factory.
- `create(pluginType)` invokes the factory and returns a fresh `AudioFxPlugin`. Throws `AUDIO_FX.CHAIN_SLOT_NOT_FOUND` with `context.pluginType` when no factory is registered; the chain restore loop relies on this exact error code so callers can pattern-match on the missing-type case.
- `list()` returns registered `pluginType`s in **insertion order** so UI pickers that render a "create plugin" list have a deterministic sequence. This matters for snapshot diff stability when consumers iterate the registry to seed defaults.
## Usage: Basic Example
```ts
import { PluginRegistry } from "@suaveplan/audio-fx-plugin-base/registry";
import { EQPlugin } from "@suaveplan/audio-fx-plugin-eq";
import { CompressorPlugin } from "@suaveplan/audio-fx-plugin-compressor";
const registry = new PluginRegistry();
registry.register("eq", () => new EQPlugin());
registry.register("compressor", () => new CompressorPlugin());
const eq = registry.create("eq"); // fresh instance
```
## Usage: Advanced Example
```ts
import { PluginRegistry } from "@suaveplan/audio-fx-plugin-base/registry";
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
// Sandboxed project: only ship a curated subset of plugins to the
// host's restore registry so user-submitted snapshots can only
// instantiate vetted plugins.
const sandboxRegistry = new PluginRegistry();
for (const id of allowlist) {
sandboxRegistry.register(id, plugins[id]);
}
// Untrusted snapshot incoming. Restore will reject any pluginType
// not in the sandbox allowlist with AUDIO_FX.CHAIN_SLOT_NOT_FOUND
// before any plugin is instantiated.
const chain = new PluginChain(
{ id: snapshot.id, channels: snapshot.channels, sampleRate: snapshot.sampleRate, blockSize: snapshot.blockSize },
new DefaultBusRegistry(),
);
try {
await chain.restore(snapshot, sandboxRegistry);
} catch (error) {
if ((error as AudioFxError).code === "AUDIO_FX.CHAIN_SLOT_NOT_FOUND") {
reportToUser(`Snapshot references plugin "${error.context.pluginType}" which is not allowed on this server.`);
}
}
```
## API Reference
- `PluginRegistry` (class)
- `register(pluginType: string, factory: PluginFactory): void` — stores a factory; overwrites existing entries.
- `has(pluginType: string): boolean` — presence check.
- `create(pluginType: string): AudioFxPlugin` — invokes the factory; throws `AUDIO_FX.CHAIN_SLOT_NOT_FOUND` (`context.pluginType`) when no factory is registered.
- `list(): ReadonlyArray<string>` — registered `pluginType`s in insertion order.
- `PluginFactory` (type) — `() => AudioFxPlugin`. Synchronous. Must return a fresh instance per call.
## Implementation Notes
The registry stores plain references in a `Map<string, PluginFactory>`; iteration order is insertion order per the ECMA-262 spec, which `list()` relies on. The implementation deliberately avoids any caching: a registry shared across many chains would otherwise hand out shared instances that get disposed by the first chain to tear down. Letting the consumer decide caching is the right shape — most callers want fresh instances; the rare caller that wants pooling can implement pooling inside their factory.
The error code on missing factory is `CHAIN_SLOT_NOT_FOUND` rather than a bespoke `PLUGIN_TYPE_NOT_REGISTERED` because the restore code path is the primary consumer and treating "no slot" and "no plugin type to fill the slot" identically keeps the error vocabulary small. The `context.pluginType` payload disambiguates the two cases for callers that need to act on them differently.
Factories are synchronous because plugin construction is synchronous in this package — async resource loading (impulse responses, sample files) happens in `setup()`, not the constructor. If a future plugin needed async construction, the right shape is a registered factory that performs the sync construction and an `await plugin.setup(...)` afterwards; the registry contract does not need to change.
Re-registration overwriting is intentional for tests and for plugin hot-swap scenarios. Consumers who want strict registration semantics can wrap the registry and reject duplicates at the caller boundary.