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,124 @@
# `createInMemoryEventsBus`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createInMemoryEventsBus` is the factory entry point of
`@suaveplan/api-events-in-memory`. It returns a fresh
{@link InMemoryEventsBus} whose `publish` calls are recorded in a
per-topic ring of payloads and whose subscriber delivery is delegated
to a private `@suaveplan/events` `EventBus`. Tests can both assert
"this event was published with this payload" (via `published(topic)`)
and assert "this subscriber observed the side-effect" (via the
subscriber callback) without any mocks or test doubles — the real
production bus runs underneath.
## Features
- Recording layer wraps every `publish` / `publishAsync` / `emit` call;
the payload is appended to the topic's history BEFORE dispatch so
history is still complete even if a subscriber throws.
- Strict-throw history-overflow policy via the cap-bounded
{@link History}. `published(topic)`'s "every publish since
`reset()`" guarantee survives long-running test suites.
- Subscriber surface (`on` / `once` / `off`) mirrors
`@suaveplan/events`' `EventBus`; the more powerful
`EventBus.emitAsync` semantics are exposed through `publishAsync`
(sequential await over registered listeners; same delivery order;
same error-capture model).
- `emit(topic, payload)` alias satisfies the structural
`CapabilityEventBusLike` contract from
`@suaveplan/api-types/capabilities` so the bus can be handed to API
plugins as their `events` capability without an additional adapter.
- `dispose()` clears history AND tears down every subscriber so test
isolation is straightforward — call once per test, no leaks.
- `createInMemoryEventBus` (no "s") is a stable alias matching the
prompt's `./adapter` subpath; both spellings will continue to exist.
## Usage: Basic
```ts
import { createInMemoryEventsBus } from "@suaveplan/api-events-in-memory/bus";
const bus = createInMemoryEventsBus();
const seen: unknown[] = [];
bus.on("billing.invoice.created", (payload) => seen.push(payload));
bus.publish("billing.invoice.created", { invoiceId: "inv_1", amount: 100 });
console.log(bus.published("billing.invoice.created"));
// [{ invoiceId: "inv_1", amount: 100 }]
console.log(seen);
// [{ invoiceId: "inv_1", amount: 100 }]
bus.reset();
console.log(bus.published("billing.invoice.created")); // []
```
## Usage: Advanced
Pin the per-topic cap when the test publishes in a tight loop, then
recover by `reset()` between batches:
```ts
import { createInMemoryEventsBus } from "@suaveplan/api-events-in-memory/bus";
import {
EVENTS_IN_MEMORY_CODES,
EventsInMemoryError,
} from "@suaveplan/api-events-in-memory/error-codes";
const bus = createInMemoryEventsBus({ maxHistoryPerTopic: 3 });
async function fanOut(count: number) {
for (let i = 0; i < count; i += 1) {
await bus.publishAsync("metrics.tick", { i });
}
}
try {
await fanOut(5);
} catch (error) {
if (
error instanceof EventsInMemoryError &&
error.code === EVENTS_IN_MEMORY_CODES.HISTORY_OVERFLOW
) {
bus.reset();
await fanOut(2);
} else {
throw error;
}
}
console.log(bus.published("metrics.tick").length); // 2
bus.dispose();
```
## API Reference
- `createInMemoryEventsBus(options?: InMemoryEventsBusOptions): InMemoryEventsBus` — factory.
- `createInMemoryEventBus` — alias, same return value.
- `InMemoryEventsBus.publish(topic, payload): void` — sync publish; appends history first.
- `InMemoryEventsBus.publishAsync(topic, payload): Promise<void>` — async publish.
- `InMemoryEventsBus.emit(topic, payload): Promise<void>` — `CapabilityEventBusLike` alias.
- `InMemoryEventsBus.on(topic, listener): Unsubscribe` — register a subscriber.
- `InMemoryEventsBus.once(topic, listener): Unsubscribe` — one-shot subscriber.
- `InMemoryEventsBus.off(topic, listener): void` — remove a subscriber.
- `InMemoryEventsBus.published(topic): readonly unknown[]` — snapshot of history.
- `InMemoryEventsBus.topics(): readonly string[]` — sorted topic names.
- `InMemoryEventsBus.reset(): void` — clear history; keep subscribers.
- `InMemoryEventsBus.dispose(): void` — clear history AND subscribers.
## Implementation Notes
The underlying `EventBus` is parameterised with the open `EventMap`
shape (`Record<string, unknown>`) so the in-memory adapter can carry
any topic name without forcing the test to declare a typed map up
front. Recording happens BEFORE dispatch because the contract that
matters for tests is "this publish was attempted" — history that
drops on subscriber failure would silently hide test bugs. `emit` and
`publishAsync` share the same body; `emit` exists only so the bus
satisfies the structural `CapabilityEventBusLike` interface from
`@suaveplan/api-types`.

View File

@@ -0,0 +1,97 @@
# `EVENTS_IN_MEMORY_CODES` — Error-code registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The error-code registry pins the two failure modes that the in-memory
events bus exposes to test code. Together they let test authors
distinguish "you bombarded the bus past the configured ring-buffer cap"
(`HISTORY_OVERFLOW`) from "you handed the factory malformed options"
(`INVALID_OPTIONS`). Both are raised as instances of
{@link EventsInMemoryError}, which extends `SuaveplanError<EventsInMemoryErrorCode>`
so `instanceof` narrows the `code` field.
The registry is built with `defineCodes` from `@suaveplan/error` so the
codes are typed as a const-string union; mistyped branches fail
TypeScript checks and the unused-code lint gate at `bun run check`.
## Features
- Stable `EVENTS_IN_MEMORY.*` namespace — codes never change without an
OpenSpec change so downstream tests can pin them.
- `EventsInMemoryError` extends `SuaveplanError`, giving cause-chain
traversal, Zod-validated wire format, and OTel span-event adapters
for free.
- `eventsInMemoryCode` is set on the public surface as a typed alias of
the base-class `code` field; legacy code paths that read
`error.eventsInMemoryCode` directly still narrow correctly.
- Two-code surface keeps the failure-mode budget small and auditable:
one for bounded-queue overflow, one for the input-validation
boundary.
## Usage: Basic
```ts
import {
EVENTS_IN_MEMORY_CODES,
EventsInMemoryError,
} from "@suaveplan/api-events-in-memory/error-codes";
try {
bus.publish("billing.invoice.created", { amount: 100 });
} catch (error) {
if (
error instanceof EventsInMemoryError &&
error.code === EVENTS_IN_MEMORY_CODES.HISTORY_OVERFLOW
) {
console.warn("history capped — call reset() between cases");
} else {
throw error;
}
}
```
## Usage: Advanced
Both codes carry `context` for forensic logs:
```ts
import {
EVENTS_IN_MEMORY_CODES,
EventsInMemoryError,
} from "@suaveplan/api-events-in-memory/error-codes";
try {
bus.publish("x", payload);
} catch (error) {
if (error instanceof EventsInMemoryError) {
const ctx = error.context as
| { readonly topic: string; readonly limit: number }
| undefined;
console.error("[events-in-memory]", error.code, ctx);
}
throw error;
}
```
For schema-failure paths `error.cause` carries the raw `z.ZodError`,
preserving issue-level granularity for forensic dumps without leaking
the underlying Zod runtime through the package's public surface.
## API Reference
- `EVENTS_IN_MEMORY_CODES` — frozen `{ HISTORY_OVERFLOW, INVALID_OPTIONS }` mapping.
- `EventsInMemoryErrorCode` — union of `"EVENTS_IN_MEMORY.HISTORY_OVERFLOW" | "EVENTS_IN_MEMORY.INVALID_OPTIONS"`.
- `EventsInMemoryError extends SuaveplanError<EventsInMemoryErrorCode>` — public constructor `(code, message, options?)`; exposes typed `eventsInMemoryCode` mirror of `code`.
## Implementation Notes
`defineCodes` prefixes every entry with the namespace (`EVENTS_IN_MEMORY.`)
so cross-package collisions are impossible. The class duplicates `code`
as `eventsInMemoryCode` because TypeScript's "narrowing by literal
field" works more reliably on the package-local alias than on the
generic base-class field when the same `SuaveplanError` is caught from
multiple packages in the same `try` block.

View File

@@ -0,0 +1,97 @@
# `History`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`History` is the per-topic append-only payload store that backs
`bus.published(topic)`. Each `publish` invocation appends to the
matching topic's list; `published(topic)` snapshots that list (a copy,
so external mutation can't corrupt internal state); `reset()` empties
every topic at once. The class lives in its own module because it
carries the overflow policy independently of the bus wrapper — tests
can pin overflow behaviour without booting an `EventBus`.
## Features
- Per-topic FIFO arrays — payloads are returned in publish order. No
re-ordering, no de-duplication, no transformation.
- Strict-throw overflow policy: once a topic has `maxPerTopic` entries
the next `append` raises `EVENTS_IN_MEMORY.HISTORY_OVERFLOW` with the
topic name and cap in `context`. The alternative ("drop oldest")
would silently break `published(topic)`'s contract that "every
publish since `reset()` is queryable", so the package picks the
louder failure mode.
- `read(topic)` returns `readonly unknown[]` — empty array when the
topic has never been published, never `undefined`. Callers don't
branch.
- `reset()` is `O(topics)` — fast enough to call between every test
case without measurable overhead.
- `size()` + `topics()` diagnostics ease debugging "why is this test
flaky?" sessions without leaking the internal `Map`.
## Usage: Basic
```ts
import { History } from "@suaveplan/api-events-in-memory/history";
const history = new History(100);
history.append("billing.invoice.created", { invoiceId: "inv_1" });
history.append("billing.invoice.created", { invoiceId: "inv_2" });
console.log(history.read("billing.invoice.created"));
// [{ invoiceId: "inv_1" }, { invoiceId: "inv_2" }]
```
## Usage: Advanced
Pin overflow behaviour explicitly:
```ts
import { History } from "@suaveplan/api-events-in-memory/history";
import {
EVENTS_IN_MEMORY_CODES,
EventsInMemoryError,
} from "@suaveplan/api-events-in-memory/error-codes";
const history = new History(2);
history.append("x", 1);
history.append("x", 2);
try {
history.append("x", 3);
} catch (error) {
if (
error instanceof EventsInMemoryError &&
error.code === EVENTS_IN_MEMORY_CODES.HISTORY_OVERFLOW
) {
const ctx = error.context as
| { readonly topic: string; readonly limit: number }
| undefined;
console.warn(`topic ${ctx?.topic} hit cap of ${ctx?.limit}`);
}
}
// reset() clears every topic at once
history.reset();
console.log(history.read("x")); // []
```
## API Reference
- `new History(maxPerTopic: number)` — construct an empty store.
- `append(topic, payload): void` — push payload; throws `HISTORY_OVERFLOW` at the cap.
- `read(topic): readonly unknown[]` — snapshot copy in publish order.
- `reset(): void` — drop every topic's history.
- `size(): number` — total payload count across topics (diagnostic).
- `topics(): readonly string[]` — sorted topic names (diagnostic).
## Implementation Notes
`read()` always returns a snapshot (`list.slice()`) so callers can
freeze the array, sort it, or hand it to assertion libraries without
mutating internal state. The throw-on-overflow policy is the design
doc's choice; the alternative drop-oldest could be added behind an
option in a later OpenSpec change, but doing so today would weaken the
`published(topic)` contract for every existing consumer.

View File

@@ -0,0 +1,80 @@
# `InMemoryEventsBusOptions`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The options module is the single boundary at which user-supplied
configuration enters the in-memory events bus. It owns the Zod schema,
the default for `maxHistoryPerTopic`, and the
`resolveInMemoryEventsBusOptions` helper that turns a partial caller
input into a fully-resolved internal options object. Keeping resolution
out of the factory avoids re-checking defaults on every internal call
and keeps the public Zod-validation surface in one place.
## Features
- Strict schema (`.strict()`) — typos in the options object surface as
`EVENTS_IN_MEMORY.INVALID_OPTIONS` rather than being silently
dropped. This is the difference between "the cap I set was ignored"
and "the cap key was misspelled" — the test author needs to know.
- Default `maxHistoryPerTopic = 1000` matches the design doc's
"guard against runaway tests" guidance — high enough for realistic
test suites, low enough that an accidental `while (true) publish(...)`
fails loudly.
- Public Zod-failures are repackaged as `EventsInMemoryError`. Callers
catch the package's own error type; `error.cause` still carries the
raw `z.ZodError` for forensic logs.
- `ResolvedInMemoryEventsBusOptions` exposes a concrete (non-optional)
shape downstream code can branch on without re-applying defaults.
## Usage: Basic
```ts
import { resolveInMemoryEventsBusOptions } from "@suaveplan/api-events-in-memory/options";
const resolved = resolveInMemoryEventsBusOptions(undefined);
console.log(resolved.maxHistoryPerTopic); // 1000
```
## Usage: Advanced
Combine a parsed JSON config blob with the schema directly when the
caller wants to surface validation issues at file-read time rather than
at bus-construction time:
```ts
import {
inMemoryEventsBusOptionsSchema,
resolveInMemoryEventsBusOptions,
} from "@suaveplan/api-events-in-memory/options";
const fromFile: unknown = JSON.parse(readFileSync("./bus.json", "utf8"));
const result = inMemoryEventsBusOptionsSchema.safeParse(fromFile);
if (!result.success) {
throw new Error(`bus.json failed validation: ${result.error.message}`);
}
const resolved = resolveInMemoryEventsBusOptions(result.data);
```
`maxHistoryPerTopic` must be a positive integer; `0` and negatives are
rejected so callers can't accidentally disable history entirely.
## API Reference
- `DEFAULT_MAX_HISTORY_PER_TOPIC: 1000` — applied when callers omit the field.
- `InMemoryEventsBusOptions` — interface with `maxHistoryPerTopic?: number`.
- `inMemoryEventsBusOptionsSchema` — `z.ZodObject` with `.strict()`.
- `ResolvedInMemoryEventsBusOptions` — internal resolved shape (`maxHistoryPerTopic: number`).
- `resolveInMemoryEventsBusOptions(raw?)` — parses, applies defaults, raises `EventsInMemoryError(INVALID_OPTIONS)` on failure.
## Implementation Notes
The schema rejects extra keys with `.strict()` because the alternative
("permissive, ignore unknown") empirically masks typos for months
before anyone notices. The repackaging-to-`EventsInMemoryError` step is
the boundary at which the package commits to its own error namespace —
once a Zod error has crossed `resolveInMemoryEventsBusOptions`, no
caller should ever see a raw `z.ZodError` again.

View File

@@ -0,0 +1,95 @@
# `createInMemoryEventsBusHarness`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The testing harness packages an {@link InMemoryEventsBus} with a
`waitFor` helper for tests that publish asynchronously and need to
assert on history once the publish has settled. It is the standard
shape downstream plugin tests will import to avoid hand-rolling a
poll-with-timeout loop for every assertion. Importing
`@suaveplan/api-events-in-memory/testing` returns a tightly-scoped
surface that won't leak production-only construction details into test
code.
## Features
- Fresh bus per harness — call `createInMemoryEventsBusHarness()` once
per test, `dispose()` at teardown; no shared state across cases.
- `waitFor(predicate, { timeoutMs, intervalMs })` polls until the
predicate holds or the timeout elapses. The default timeout (1 s)
and interval (5 ms) work for the overwhelming majority of unit
tests; override only when integrating with slower infra.
- Fast path: if the predicate already holds at call time the function
resolves on the next microtask without sleeping.
- Reject-on-timeout includes the requested `timeoutMs` in the message
so test failures are easy to triage.
- All options forwarded verbatim to {@link createInMemoryEventsBus}, so
the harness inherits the strict-throw history-overflow policy and
the Zod-validated options boundary.
## Usage: Basic
```ts
import { createInMemoryEventsBusHarness } from "@suaveplan/api-events-in-memory/testing";
const harness = createInMemoryEventsBusHarness();
const { bus, waitFor } = harness;
bus.on("user.signup", async () => {
await Promise.resolve();
bus.publish("welcome.email.queued", { to: "u@example.com" });
});
await bus.publishAsync("user.signup", { userId: "u1" });
await waitFor((b) => b.published("welcome.email.queued").length === 1);
console.log(bus.published("welcome.email.queued"));
harness.dispose();
```
## Usage: Advanced
Tighten the deadline for a fast unit test, or extend it for an
integration test that sits behind a real database:
```ts
import { createInMemoryEventsBusHarness } from "@suaveplan/api-events-in-memory/testing";
const harness = createInMemoryEventsBusHarness({ maxHistoryPerTopic: 50 });
try {
await harness.waitFor(
(bus) => bus.published("payment.captured").length >= 3,
{ timeoutMs: 5000, intervalMs: 25 }
);
} catch (error) {
console.error("payment.captured fan-out stalled:", error);
throw error;
} finally {
harness.dispose();
}
```
`dispose()` is idempotent — calling it twice in `afterEach` after a
mid-test failure is safe.
## API Reference
- `createInMemoryEventsBusHarness(options?): InMemoryEventsBusHarness` — factory.
- `InMemoryEventsBusHarness.bus: InMemoryEventsBus` — the wrapped bus.
- `InMemoryEventsBusHarness.waitFor(predicate, options?): Promise<void>` — poll-with-timeout.
- `InMemoryEventsBusHarness.dispose(): void` — tear down the bus.
- `WaitForPredicate = (bus: InMemoryEventsBus) => boolean`.
- `WaitForOptions = { timeoutMs?: number; intervalMs?: number }`.
## Implementation Notes
`waitFor` uses `setTimeout` rather than `setImmediate`/`queueMicrotask`
because real tests need to interleave with timers managed by the
runtime; busy-looping on microtasks starves them. The harness owns
`dispose` rather than asking the caller to call `bus.dispose()`
directly so that future versions can add additional resources
(meter providers, span recorders) without breaking the test surface.