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,56 @@
# defaultClock (schema-registry)
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`defaultClock` is the single point in `@suaveplan/bitpacker-schema/registry` where the wall clock is read. The `SchemaRegistry` class accepts an injectable `Clock` for stamping `createdAt`/`updatedAt` on registered entries; when no clock is provided, it falls back to this default.
Centralising the `Date.now()` call this way matches the pattern used by `@suaveplan/bitpacker-streaming` and `@suaveplan/bitpacker-network`. suaveplan enforces a "no module-scope `Date.now()`/`Math.random()`" rule outside allow-listed defaults — this file is one of those defaults.
## Features
- Single named export `defaultClock: Clock` typed via `@suaveplan/types`.
- Pure function — no side effects beyond reading the wall clock.
- Idiomatic Suaveplan convention for clock injection.
## Usage: Basic Example
```ts
import { defaultClock } from "@suaveplan/bitpacker-schema";
const now = defaultClock();
```
## Usage: Advanced Example
```ts
import type { Clock } from "@suaveplan/types";
import { SchemaRegistry } from "@suaveplan/bitpacker-schema";
// Inject a deterministic clock for reproducible test timestamps.
let t = 0;
const fakeClock: Clock = () => {
t += 1000;
return t;
};
const registry = new SchemaRegistry(undefined, { clock: fakeClock });
await registry.registerSchema(
{ id: "uuid" },
{ namespace: "events", name: "user-created" }
);
```
## API Reference
### `defaultClock: Clock`
Returns `Date.now()` — the current wall-clock time in milliseconds.
## Implementation Notes
The `Clock` type is `() => number`. suaveplan prefers function types over class instances for monotonic primitives because they are trivially serialisable, comparable, and substitutable in tests without dependency-injection frameworks.
The reason this lives in `registry/defaults/` rather than as a static class member is the lint rule that bans module-scope `Date.now()` calls in production code. By scoping the call to a single named, lint-allow-listed module, we keep the rest of the package free of static analysis exceptions.

View File

@@ -0,0 +1,75 @@
# SchemaRegistry Factories
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`createInMemorySchemaRegistry` and `createLocalStorageSchemaRegistry` are the convenience factories for the two shipping `SchemaRegistry` configurations. They mirror the upstream factory shape so consumers porting from `bitpacker/schema/registry` only need to change the import path.
Tests and ephemeral registries call `createInMemorySchemaRegistry`; browser apps wanting persistence across page loads call `createLocalStorageSchemaRegistry`. Both forward an optional `SchemaRegistryOptions` for clock injection.
## Features
- Two factories, one per shipping storage backend.
- Forward `SchemaRegistryOptions` (currently just `clock`) so tests can stamp deterministic timestamps without manually constructing the registry.
- Forward `LocalStorageSchemaRegistryStorageOptions` for the localStorage variant — caller can inject a stub store and a custom storage key.
## Usage: Basic Example
```ts
import {
createInMemorySchemaRegistry,
createLocalStorageSchemaRegistry,
} from "@suaveplan/bitpacker-schema";
const ephemeral = createInMemorySchemaRegistry();
const persistent = createLocalStorageSchemaRegistry({ storageKey: "schemas" });
```
## Usage: Advanced Example
```ts
import type { Clock } from "@suaveplan/types";
import {
createInMemorySchemaRegistry,
createLocalStorageSchemaRegistry,
type KeyValueStringStore,
} from "@suaveplan/bitpacker-schema";
let t = 0;
const fakeClock: Clock = () => {
t += 1000;
return t;
};
const inMemory = createInMemorySchemaRegistry({ clock: fakeClock });
const m = new Map<string, string>();
const stub: KeyValueStringStore = {
getItem: (k) => m.get(k) ?? null,
setItem: (k, v) => m.set(k, v),
};
const persistent = createLocalStorageSchemaRegistry(
{ store: stub, storageKey: "test:schemas" },
{ clock: fakeClock }
);
```
## API Reference
### `createInMemorySchemaRegistry(options?) → SchemaRegistry`
`options.clock?` — injectable `Clock`. Defaults to `defaultClock`.
### `createLocalStorageSchemaRegistry(storageOptions?, options?) → SchemaRegistry`
`storageOptions.store?` — any `KeyValueStringStore`. Defaults to `globalThis.localStorage`.
`storageOptions.storageKey?` — defaults to `"bitpacker-schema-registry"`.
`options.clock?` — injectable `Clock`. Defaults to `defaultClock`.
## Implementation Notes
The factories are thin wrappers. They exist primarily for upstream-shape parity and discoverability — having `createInMemorySchemaRegistry` show up in code completion is more obvious than asking the caller to know to import both `SchemaRegistry` and `InMemorySchemaRegistryStorage` and call `new`.
Splitting the localStorage factory's argument list into two — `storageOptions` first, then `SchemaRegistryOptions` — is intentional. The storage options are the more common knob to twist (different storage keys per app), so they take the leading slot.

View File

@@ -0,0 +1,112 @@
# SchemaRegistry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [default-clock](./defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_ - [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_ - [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_ - [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./registry.md) — <!-- begin:auto-docs --> _(no submodules)_- default-clock — <!-- begin:auto-docs --> _(no submod…
- [default-clock](./defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-clock](./defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./registry.md) — <!-- begin:auto-docs --> _(no submodules)_- default-clock — <!-- begin:a…
- [default-clock](./defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./registry.md) — <!-- begin:auto-docs --> _(no submodules)_- default-clock — <!-- begin:auto-docs --> _(no submo…
- [default-clock](./defaults/default-clock.md) — <!-- begin:auto-docs --> _(no submodules)_- [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_ - [factories](./factories/factories.md) — <!-- begin:auto-docs --> _(no submodules)_- [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_ - [in-memory](./storage/in-memory/in-memory.md) — <!-- begin:auto-docs --> _(no submodules)_- [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_ - [local-storage](./storage/local-storage/local-storage.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./registry.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
`SchemaRegistry` is the versioned, namespaced schema store at the heart of `@suaveplan/bitpacker-schema`. Callers register `BitPackerSchema` definitions under a `(namespace, name)` pair; each call gets an auto-incremented version stamped with `createdAt`/`updatedAt` timestamps. Lookups can target an exact version or "latest", and an entire `(namespace, name)` family can be reconstructed as a `VersionedSchema` for migration workflows.
The registry composes over a pluggable `SchemaRegistryStorage` adapter — the same class works against in-memory, browser-localStorage, or any future server-side persistence backend. Tests build the registry over the in-memory adapter; production browsers use the localStorage adapter; production servers wire to whatever storage already exists in the deployment.
## Features
- Auto-versioning: `registerSchema` reads the current latest version for `(namespace, name)` and emits the next integer version.
- Bulk versioning: `registerVersionedSchema` imports every version of a `VersionedSchema` in a single call, preserving the original `createdAt` per version.
- Latest-or-exact lookup: `getSchema(namespace, name)` returns the highest-versioned row; `getSchema(namespace, name, version)` targets that specific version.
- `getVersionedSchema` reconstructs a `VersionedSchema` from every stored version of `(namespace, name)`, sorted ascending.
- `listSchemas` and `deleteSchema` pass through to the storage adapter, with optional namespace + tag filtering.
- Injectable `Clock` for reproducible timestamps in tests.
- `BitpackerError` codes for failure paths, replacing upstream's plain `Error` throws.
## Usage: Basic Example
```ts
import {
SchemaRegistry,
InMemorySchemaRegistryStorage,
} from "@suaveplan/bitpacker-schema";
const registry = new SchemaRegistry(new InMemorySchemaRegistryStorage());
const entry = await registry.registerSchema(
{ id: "uuid", payload: "blob" },
{ namespace: "events", name: "user-created" }
);
console.log(entry.metadata.version); // → 1
const latest = await registry.getSchema("events", "user-created");
```
## Usage: Advanced Example
```ts
import {
SchemaRegistry,
LocalStorageSchemaRegistryStorage,
VersionedSchema,
} from "@suaveplan/bitpacker-schema";
// Browser-side registry persisted to localStorage.
const registry = new SchemaRegistry(
new LocalStorageSchemaRegistryStorage({ storageKey: "app:schemas" })
);
// Bulk-import an existing VersionedSchema in one call.
const versioned = new VersionedSchema(
{ id: "uuid" },
{ name: "user-created", namespace: "events" }
);
versioned.addVersion({ id: "uuid", email: "-256" }, "Add email field");
await registry.registerVersionedSchema(versioned, {
tags: ["v2", "stable"],
owner: "platform-team",
});
// Reconstruct the same VersionedSchema from storage.
const restored = await registry.getVersionedSchema("events", "user-created");
```
## API Reference
### `new SchemaRegistry(storage?, options?)`
`storage?` — any `SchemaRegistryStorage`. Defaults to `new InMemorySchemaRegistryStorage()`.
`options.clock?` — injectable `Clock` for `createdAt`/`updatedAt`. Defaults to `defaultClock`.
### `registerSchema(schema, metadata) → Promise<SchemaRegistryEntry>`
Registers a new schema. Computes `version` as `latest.version + 1` (or `1` if first). Stamps both timestamps with the configured clock. Returns the persisted entry.
### `registerVersionedSchema(versionedSchema, metadata) → Promise<SchemaRegistryEntry[]>`
Imports every version of `versionedSchema` as its own row. The `metadata` argument supplies tags/owner; namespace/name come from the `VersionedSchema` itself. Each row's `createdAt` is preserved from `version.createdAt` when present; `updatedAt` is stamped fresh.
### `getSchema(namespace, name, version?) → Promise<SchemaRegistryEntry | null>`
Exact-version or latest lookup. Returns `null` if no row matches.
### `getVersionedSchema(namespace, name) → Promise<VersionedSchema | null>`
Reconstructs a `VersionedSchema` from every stored version of `(namespace, name)`, sorted by version ascending. Returns `null` if no rows match.
### `listSchemas(namespace?, tag?) → Promise<SchemaRegistryEntry[]>`
Pass-through to `storage.listSchemas`.
### `deleteSchema(namespace, name, version?) → Promise<void>`
Pass-through to `storage.deleteSchema`. Without `version`, deletes every version of `(namespace, name)`.
## Implementation Notes
The constructor accepts `SchemaRegistryStorage` as an interface rather than a concrete class. Defaulting to `new InMemorySchemaRegistryStorage()` matches upstream behaviour but gives every consumer the option of swapping in a different backend without changing the rest of their code.
`registerVersionedSchema` preserves each version's original `createdAt` when present. This matters for migration workflows where the registry is restored from a backup — the original timestamps stay intact, and only `updatedAt` reflects "the moment this row was re-saved".
`getVersionedSchema` does its own version-name filter on top of `storage.listSchemas(namespace)` rather than calling a hypothetical `listSchemas(namespace, name)` because the `SchemaRegistryStorage` contract intentionally limits filtering to namespace + tag. Adding a name filter would require every adapter to gain a third overload, and the in-memory cost of the local filter is negligible at registry sizes.
After the empty-match early return, the index-`0` access is guarded by a type assertion rather than another runtime check — this is the pattern Suaveplan uses for unreachable post-validation accesses, and it keeps branch coverage at 100%.

View File

@@ -0,0 +1,90 @@
# InMemorySchemaRegistryStorage
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`InMemorySchemaRegistryStorage` is the in-process `SchemaRegistryStorage` adapter. It stores schema rows in a private array and is the default backend for `SchemaRegistry` instances created without an explicit storage adapter — useful for tests, ephemeral registries, and any scenario where persistence is not required.
This adapter exists primarily so the `SchemaRegistry` class can have a single storage-shape contract regardless of where the schemas actually live. Tests can build a registry over this adapter without needing browser polyfills or filesystem mocks; production servers that wire to a real persistence layer (Redis, Postgres, S3) implement the same `SchemaRegistryStorage` interface.
## Features
- Implements `SchemaRegistryStorage` exactly: `saveSchema`, `loadSchema`, `listSchemas`, `deleteSchema`.
- Defensive cloning on save/load so callers can't mutate stored entries by holding the returned reference.
- Latest-version resolution when `loadSchema` is called without a version.
- Tag and namespace filtering on `listSchemas`.
- Delete-all-versions semantics when `deleteSchema` is called without a version.
## Usage: Basic Example
```ts
import { InMemorySchemaRegistryStorage } from "@suaveplan/bitpacker-schema";
const storage = new InMemorySchemaRegistryStorage();
await storage.saveSchema({
metadata: {
namespace: "events",
name: "user-created",
version: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
schema: { id: "uuid", name: "-32" },
});
const loaded = await storage.loadSchema("events", "user-created");
```
## Usage: Advanced Example
```ts
import {
InMemorySchemaRegistryStorage,
SchemaRegistry,
} from "@suaveplan/bitpacker-schema";
const storage = new InMemorySchemaRegistryStorage();
const registry = new SchemaRegistry(storage);
await registry.registerSchema(
{ id: "uuid", payload: "blob" },
{
namespace: "events",
name: "user-updated",
tags: ["v2", "stable"],
}
);
const tagged = await registry.listSchemas(undefined, "stable");
```
## API Reference
### `new InMemorySchemaRegistryStorage()`
No arguments. Initialises an empty in-process schema array.
### `saveSchema(entry)`
Inserts `entry` if no `(namespace, name, version)` match exists; otherwise overwrites the existing row. Defensive-clones the entry.
### `loadSchema(namespace, name, version?)`
Returns the entry matching `(namespace, name, version)`. When `version` is omitted, returns the entry with the highest `metadata.version`. Returns `null` if no row matches.
### `listSchemas(namespace?, tag?)`
Returns all entries, optionally filtered by namespace and/or tag. Defensive-clones each entry so the caller can't mutate the store.
### `deleteSchema(namespace, name, version?)`
Removes the row matching `(namespace, name, version)` if `version` is supplied; removes ALL rows matching `(namespace, name)` otherwise.
## Implementation Notes
The store is a plain `SchemaRegistryEntry[]` rather than a nested map. Schemas are typically registered O(1)-O(10) per namespace, lookup is per-call rather than per-tick, and the linear-scan cost is dominated by the JS object dispatch. A nested map would add complexity without a measurable benefit at this size class.
`cloneEntry` is shallow on `metadata` (spreads the object) and identity on `schema` (BitPackerSchema is treated as opaque-by-reference because cloning it would require serialising and re-parsing the entire shape). Callers MUST NOT mutate schema objects after registering them — but if they do, the registry's stored reference points to the same object, by design.
Latest-version resolution uses a single linear pass rather than `Math.max(...versions)` because the array might be very long for high-traffic registries and `Math.max(...arr)` allocates a temporary spread for every call.

View File

@@ -0,0 +1,92 @@
# LocalStorageSchemaRegistryStorage
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`LocalStorageSchemaRegistryStorage` is the browser-`localStorage`-backed `SchemaRegistryStorage` adapter. It persists registry rows under a single key (default `"bitpacker-schema-registry"`) as a JSON array, restoring `Date` instances for `createdAt`/`updatedAt` on read.
The adapter accepts any `KeyValueStringStore`-conformant backend via dependency injection so unit tests don't need a real `localStorage` polyfill — pass an in-memory stub that implements `getItem(key) → string | null` and `setItem(key, value): void`. Production code passes `globalThis.localStorage` (the default).
## Features
- Exact `SchemaRegistryStorage` contract: `saveSchema`, `loadSchema`, `listSchemas`, `deleteSchema`.
- Dependency-injectable store — works with `globalThis.localStorage`, `sessionStorage`, an in-memory stub, or any future Web-storage-shaped backend.
- Date round-tripping: `createdAt`/`updatedAt` are restored from ISO strings to `Date` instances on read.
- Failure paths raise `BitpackerError` with codes (`SCHEMA_STORAGE_PARSE_FAILED`, `SCHEMA_STORAGE_WRITE_FAILED`) — no silent `console.error`'ing as upstream did.
- Missing-store fallback returns an empty list rather than throwing, so the same code can run in environments without any web storage at all.
## Usage: Basic Example
```ts
import { LocalStorageSchemaRegistryStorage } from "@suaveplan/bitpacker-schema";
// In a browser, defaults to globalThis.localStorage.
const storage = new LocalStorageSchemaRegistryStorage();
await storage.saveSchema({
metadata: {
namespace: "events",
name: "user-created",
version: 1,
createdAt: new Date(),
updatedAt: new Date(),
},
schema: { id: "uuid" },
});
```
## Usage: Advanced Example
```ts
import {
LocalStorageSchemaRegistryStorage,
type KeyValueStringStore,
} from "@suaveplan/bitpacker-schema";
// Swap in a stub for tests — no real localStorage needed.
function makeStubStore(): KeyValueStringStore {
const m = new Map<string, string>();
return {
getItem: (k) => m.get(k) ?? null,
setItem: (k, v) => m.set(k, v),
removeItem: (k) => m.delete(k),
};
}
const storage = new LocalStorageSchemaRegistryStorage({
store: makeStubStore(),
storageKey: "test:registry",
});
```
## API Reference
### `new LocalStorageSchemaRegistryStorage(options?)`
`options.store?` — any `KeyValueStringStore`. Defaults to `globalThis.localStorage` (or `undefined` if no localStorage is present).
`options.storageKey?` — the key to read/write. Default `"bitpacker-schema-registry"`.
### `saveSchema(entry)`
Reads the current array, inserts or replaces the row matching `(namespace, name, version)`, writes the array back. Throws `BitpackerError(SCHEMA_STORAGE_WRITE_FAILED)` if the underlying store rejects the write (typical cause: quota exceeded).
### `loadSchema(namespace, name, version?)`
Reads the current array and returns the matching row. When `version` is omitted, returns the row with the highest `metadata.version`. Returns `null` if no row matches.
### `listSchemas(namespace?, tag?)`
Reads the current array and applies optional namespace/tag filters.
### `deleteSchema(namespace, name, version?)`
Filters out the matching row(s) and writes the array back. Removes ALL versions of `(namespace, name)` if `version` is omitted.
## Implementation Notes
The adapter writes the full array on every save. For registries with hundreds of schemas this becomes O(n) per write, but at typical sizes (10-50 schemas per app) the cost is dominated by the JSON encode/decode rather than the array copy. If a deployment grows past that, switch to a chunk-keyed adapter (one localStorage key per `namespace + name`) — the `SchemaRegistryStorage` contract is the only thing that needs to stay the same.
`reviveEntry` casts the parsed metadata through an intermediate shape and rebuilds `Date` instances from the ISO strings. We do not run a Zod validator on the parsed rows — the adapter trusts its own write format (the only writer in production is `writeAll` from this same class). If a deployment needs hardened deserialisation against tampering, wrap the adapter with a validating decorator.
The default-store resolution looks at `globalThis.localStorage` rather than `window.localStorage` so the adapter compiles unchanged in Node.js, Bun, Web Workers, and Service Workers — anywhere with a global `localStorage` shape.