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,85 @@
# json-decode
## Purpose
Implements `deserialize`, the inverse of `serialize`. Given a tagged JSON string produced by the encoder, it reconstructs the original JavaScript value graph including all registered types (built-ins and custom), `undefined`, `NaN`, and `±Infinity`. Any `__t` tag that has no registered handler causes an `UnsupportedTypeError` to be thrown immediately rather than silently discarding data.
The decoder is intentionally strict: it is safer to fail loudly on an unrecognised tag than to silently drop values, which could cause subtle data corruption downstream.
## Features
- Full inverse of `serialize`: all built-in type tags are decoded back to their original JavaScript types
- `AbortSignal` support for cancelling mid-traversal on large payloads, checked every 256 nodes
- `UnsupportedTypeError` thrown (with the tag name in `context.tag`) for unrecognised `__t` values
- Recursive array and plain-object decoding
- Zero allocations for pass-through primitives (string, number, boolean, null)
## Usage: Basic Example
```typescript
import { deserialize, serialize } from "@suaveplan/serialize";
const original = { created: new Date(), tags: new Set(["x", "y"]) };
const json = serialize(original);
const restored = deserialize(json) as typeof original;
restored.created instanceof Date; // true
restored.tags instanceof Set; // true
```
## Usage: Advanced Example
```typescript
import { deserialize, UnsupportedTypeError } from "@suaveplan/serialize";
const controller = new AbortController();
// Cancel deserialization partway through:
setTimeout(() => controller.abort(new Error("timeout")), 10);
try {
const value = deserialize(largeJson, { signal: controller.signal });
processValue(value);
} catch (err) {
if (err instanceof UnsupportedTypeError) {
console.error("Unknown type tag:", err.context?.tag);
} else {
// likely the abort reason
throw err;
}
}
```
## API Reference
### `deserialize(json: string, opts?: DeserializeOptions): unknown`
Parse `json` and recursively reconstruct the JavaScript value it encodes.
**Parameters:**
- `json` — a tagged JSON string produced by `serialize`
- `opts.signal` — an `AbortSignal`; checked every 256 nodes
**Returns:** the reconstructed value.
**Throws:**
- `UnsupportedTypeError` when a `__t` tag has no registered decoder
- `SyntaxError` from `JSON.parse` if the input is not valid JSON
- The `AbortSignal`'s reason when `signal` is aborted
### `DeserializeOptions`
```typescript
interface DeserializeOptions {
signal?: AbortSignal;
}
```
## Implementation Notes
- The decoder calls `JSON.parse` once at the top level and then recursively traverses the parsed intermediate tree. This avoids parsing partial JSON strings and keeps the implementation simple.
- For tagged objects, the inner `v` payload is decoded recursively *before* being handed to `def.decode`. This means registered type decoders always receive already-decoded JavaScript values, not raw JSON intermediates.
- Signal checking is shared with a module-level counter that resets per `deserialize` call; it fires every 256 traversed nodes.
- The decoder does not validate the structure of `v` beyond calling `def.decode`; responsibility for validating the payload shape belongs to each registered type definition.

View File

@@ -0,0 +1,99 @@
# json-encode
## Purpose
Implements the `serialize` function that converts any JavaScript value to a tagged JSON string preserving type information beyond what `JSON.stringify` supports. The encoder traverses the value graph recursively, wraps non-primitive types in `{ "__t": tag, "v": ... }` envelopes handled by the type registry, and detects both cycles and excessive nesting depth before they cause runtime errors.
The tagged JSON format is self-describing: a receiver that imports `deserialize` from the same package can reconstruct the original value graph without out-of-band schema information.
## Features
- Handles all types registered in the global registry (Date, Map, Set, BigInt, TypedArrays, URL, RegExp, custom classes)
- Preserves `undefined` as `{"__t":"__undefined","v":null}` (normally silently dropped by `JSON.stringify`)
- Preserves `NaN` as `{"__t":"__NaN","v":null}` and `±Infinity` as `{"__t":"__Infinity","v":"+"/"−"}`
- Detects circular references via `WeakSet` and throws `CircularReferenceError` with the structural path
- Configurable maximum depth (default 100) via `maxDepth` option; throws `MaxDepthExceededError`
- Configurable maximum byte length via `maxSize` option; throws `MaxSizeExceededError`
- Honour `AbortSignal` cancellation; checked every 256 nodes for minimal overhead
- `seen` set cleared per-branch (re-entrant DAG nodes are allowed, only true cycles throw)
## Usage: Basic Example
```typescript
import { serialize } from "@suaveplan/serialize";
const obj = {
created: new Date("2024-01-01"),
tags: new Set(["a", "b"]),
count: BigInt(9007199254740993),
};
const json = serialize(obj);
// '{"created":{"__t":"__Date","v":"2024-01-01T00:00:00.000Z"},...}'
```
## Usage: Advanced Example
```typescript
import { serialize } from "@suaveplan/serialize";
const controller = new AbortController();
try {
// Enforce max 50KB output and max 20 levels of nesting
const json = serialize(hugeObject, {
maxDepth: 20,
maxSize: 50_000,
signal: controller.signal,
});
sendToNetwork(json);
} catch (err) {
if (err instanceof MaxSizeExceededError) {
console.error("Payload too large:", err.context?.size, "bytes");
} else if (err instanceof MaxDepthExceededError) {
console.error("Too deeply nested at depth:", err.context?.depth);
}
}
// Cancel mid-way through a large serialization:
controller.abort();
```
## API Reference
### `serialize(value: unknown, opts?: SerializeOptions): string`
Converts `value` to a tagged JSON string.
**Parameters:**
- `value` — any JavaScript value; functions and symbols are silently dropped (same as `JSON.stringify`)
- `opts.maxDepth` — maximum recursion depth before `MaxDepthExceededError` is thrown (default: `100`)
- `opts.maxSize` — maximum byte length of the resulting JSON string; checked after encoding (default: unlimited)
- `opts.signal` — an `AbortSignal`; checked every 256 nodes; throws the signal's reason if aborted
**Returns:** a JSON string in the tagged wire format.
**Throws:**
- `CircularReferenceError` when a cycle is detected
- `MaxDepthExceededError` when nesting exceeds `maxDepth`
- `MaxSizeExceededError` when output exceeds `maxSize` bytes
- The `AbortSignal`'s reason when `signal` is aborted
### `SerializeOptions`
```typescript
interface SerializeOptions {
maxDepth?: number; // default: 100
maxSize?: number; // default: unlimited
signal?: AbortSignal;
}
```
## Implementation Notes
- The cycle-detection `WeakSet` (`seen`) is managed with add-before/delete-after so that DAG structures (same object reachable via multiple paths) do not falsely trigger the cycle check. Only true back-edges to an ancestor in the current call stack throw.
- Signal checking happens every 256 nodes rather than every node to keep the hot path free of extra branch overhead.
- The `maxSize` check occurs **after** `JSON.stringify` on the fully encoded intermediate object, not during encoding. This trades one additional pass for implementation simplicity and avoids double-counting tag overhead.
- `undefined` at object property positions is omitted by `JSON.stringify` but handled explicitly here by converting it to a tagged sentinel before the `JSON.stringify` call.

View File

@@ -0,0 +1,75 @@
# msgpack-decode
## Purpose
Implements `decodeBinary`, the inverse of `encodeBinary`. Given a MessagePack binary buffer produced by the encoder, it reconstructs the original JavaScript value by first decoding the msgpack wire bytes to an intermediate plain-JS-object representation and then applying the same tag-based reconstruction logic used by `deserialize`.
The two-phase approach (msgpack decode → tag reconstruct) keeps the decoder code minimal and reuses the same registry lookup path as the JSON decoder.
## Features
- Full inverse of `encodeBinary`: all built-in and custom registered types are reconstructed
- `UnsupportedTypeError` thrown on unrecognised `__t` tags (same strict policy as JSON decoder)
- Reuses a single shared `Decoder` instance for zero allocation overhead across calls
- `DeserializeOptions` parameter accepted for API consistency with the JSON codec (signal not yet applied internally, as msgpack decode is synchronous and very fast)
## Usage: Basic Example
```typescript
import { decodeBinary, encodeBinary } from "@suaveplan/serialize";
const original = new Map([["key", new Date()]]);
const bytes = encodeBinary(original);
const restored = decodeBinary(bytes) as Map<string, Date>;
restored instanceof Map; // true
restored.get("key") instanceof Date; // true
```
## Usage: Advanced Example
```typescript
import { decodeBinary, UnsupportedTypeError } from "@suaveplan/serialize";
async function receiveMessage(bytes: Uint8Array) {
try {
const value = decodeBinary(bytes);
return value;
} catch (err) {
if (err instanceof UnsupportedTypeError) {
// The sender used a type tag unknown to this receiver.
// Either register the type or reject the message.
throw new Error(`Unknown type: ${err.context?.tag}`);
}
throw err;
}
}
```
## API Reference
### `decodeBinary(bytes: Uint8Array, opts?: DeserializeOptions): unknown`
Decode a MessagePack buffer to the original JavaScript value.
**Parameters:**
- `bytes` — a `Uint8Array` produced by `encodeBinary`
- `opts` — `DeserializeOptions` (accepted for API parity; signal not currently applied within this synchronous path)
**Returns:** the reconstructed JavaScript value.
**Throws:**
- `UnsupportedTypeError` when a `__t` tag has no registered decoder
- `RangeError` or `TypeError` from `@msgpack/msgpack` if the input bytes are malformed
### `DeserializeOptions`
See `json-decode.md`.
## Implementation Notes
- A single shared `Decoder` instance is reused to avoid per-call setup overhead; the decoder is stateless between calls.
- The `postDecode` function is structurally identical to the inner `decodeValue` used by `json-decode.ts`. Future refactoring could extract it to a shared utility; currently it is kept local to avoid premature abstraction.
- The `opts` parameter is accepted but currently unused because the msgpack decode step is synchronous and does not lend itself to mid-stream cancellation without a streaming API.

View File

@@ -0,0 +1,78 @@
# msgpack-encode
## Purpose
Implements `encodeBinary`, the MessagePack counterpart to `serialize`. It converts any JavaScript value to a compact binary `Uint8Array` using the same type-registry tag scheme as the JSON encoder. By pre-processing the input into plain msgpack-compatible intermediate values before handing off to `@msgpack/msgpack`, the codec avoids the complexity of a custom extension codec while still preserving all built-in and custom registered types.
Object keys are sorted canonically before encoding, so two structurally identical objects always produce bit-identical output regardless of property insertion order.
## Features
- Produces a compact binary wire format via `@msgpack/msgpack`
- Same type-registry integration as the JSON codec: all built-in and custom types round-trip faithfully
- Canonical key ordering for deterministic binary output
- `maxDepth`, `maxSize`, and `AbortSignal` options parallel to the JSON encoder
- Cycle detection via `WeakSet` — throws `CircularReferenceError` on cycles
- Throws `MaxDepthExceededError` and `MaxSizeExceededError` on limit violations
## Usage: Basic Example
```typescript
import { decodeBinary, encodeBinary } from "@suaveplan/serialize";
const payload = { created: new Date(), ids: new Set([1, 2, 3]) };
const bytes = encodeBinary(payload);
// bytes is a compact Uint8Array — smaller than JSON for numeric-heavy data
const restored = decodeBinary(bytes);
```
## Usage: Advanced Example
```typescript
import { encodeBinary } from "@suaveplan/serialize";
const controller = new AbortController();
const bytes = encodeBinary(largeDataset, {
maxDepth: 30,
maxSize: 1_000_000, // 1 MB
signal: controller.signal,
});
// Send over WebSocket, IndexedDB, etc.
socket.send(bytes);
```
## API Reference
### `encodeBinary(value: unknown, opts?: SerializeOptions): Uint8Array`
Encodes `value` to a MessagePack binary buffer.
**Parameters:**
- `value` — any JavaScript value; functions and symbols are silently dropped
- `opts.maxDepth` — maximum recursion depth (default: `100`)
- `opts.maxSize` — maximum byte length of the binary output; checked after encoding
- `opts.signal` — `AbortSignal`; checked once before encoding begins
**Returns:** a `Uint8Array` containing the MessagePack payload.
**Throws:**
- `CircularReferenceError` on cycles
- `MaxDepthExceededError` when nesting exceeds `maxDepth`
- `MaxSizeExceededError` when output exceeds `maxSize` bytes
- The `AbortSignal`'s reason if aborted
### `SerializeOptions`
See `json-encode.md` — the same options interface is shared.
## Implementation Notes
- A single shared `Encoder` instance is reused across calls to amortise initialisation overhead; the encoder is stateless between calls.
- The `preEncode` helper mirrors the logic in `json-encode.ts` but emits plain objects/arrays instead of a JSON string, avoiding a second parse step.
- `AbortSignal` is checked once at the call boundary rather than every 256 nodes because msgpack encoding is typically fast and does not support mid-stream interruption without a custom streaming approach.
- Canonical key sorting uses the default `Array.prototype.sort` comparator (lexicographic UTF-16 code units), matching the sort used by many canonical JSON implementations.

View File

@@ -0,0 +1,92 @@
# stream
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides `encodeStream` and `decodeStream` for streaming serialization over environments that produce or consume `ReadableStream<Uint8Array>`. Rather than requiring the entire payload to be buffered before transmission, `encodeStream` chunks the MessagePack binary output into 4-byte-length-prefixed frames of up to 4 KB each, enabling progressive delivery over WebSockets, HTTP streaming responses, or other byte-stream transports.
`decodeStream` is the inverse: it reads framed chunks from a `ReadableStream<Uint8Array>`, reassembles the payload, and emits a single decoded value via a `ReadableStream<unknown>`.
## Features
- Frame format: 4-byte big-endian payload length + up to 4 KB payload
- Streaming encode over `ReadableStream<Uint8Array>` using `encodeBinary` internally
- Streaming decode that handles partial frames across multiple read chunks
- `AbortSignal` support in both encode and decode paths
- Errors propagated via `controller.error()` so consumers can catch via stream pipelines
- Zero external deps beyond the msgpack codec and Web Streams API
## Usage: Basic Example
```typescript
import { decodeStream, encodeStream } from "@suaveplan/serialize";
const payload = { data: new Array(1000).fill("item") };
const stream = encodeStream(payload);
// In the same process or after transmission:
const decoded = decodeStream(stream);
const reader = decoded.getReader();
const { value } = await reader.read();
// value deep-equals payload
```
## Usage: Advanced Example
```typescript
import { decodeStream, encodeStream } from "@suaveplan/serialize";
// Abort if encoding takes too long
const controller = new AbortController();
setTimeout(() => controller.abort(new Error("encode timeout")), 5000);
const stream = encodeStream(hugeObject, { signal: controller.signal });
// Pipe to a fetch body or WebSocket
const response = new Response(stream, {
headers: { "Content-Type": "application/octet-stream" },
});
// On the receiving side:
const incoming: ReadableStream<Uint8Array> = response.body!;
const valueStream = decodeStream(incoming);
const reader = valueStream.getReader();
const { value } = await reader.read();
```
## API Reference
### `encodeStream(value: unknown, opts?: SerializeOptions & { signal?: AbortSignal }): ReadableStream<Uint8Array>`
Serialize `value` using `encodeBinary` and emit the result as framed 4 KB chunks.
**Parameters:**
- `value` — any serializable JavaScript value
- `opts` — same as `SerializeOptions` plus optional `signal`
**Returns:** a `ReadableStream<Uint8Array>` where each chunk is a complete length-prefixed frame.
**Throws (via stream error):** all errors from `encodeBinary` plus `AbortSignal` reason.
### `decodeStream(stream: ReadableStream<Uint8Array>, opts?: DeserializeOptions): ReadableStream<unknown>`
Reassemble frames from `stream` and emit the decoded value.
**Parameters:**
- `stream` — a framed stream produced by `encodeStream`
- `opts` — `DeserializeOptions` including optional `signal`
**Returns:** a `ReadableStream<unknown>` that emits exactly one decoded value then closes.
**Throws (via stream error):** all errors from `decodeBinary` plus `AbortSignal` reason.
## Implementation Notes
- Each frame is self-delimiting (4-byte length prefix), so the receiver never needs to know the total payload size in advance.
- `decodeStream` handles partial frames across multiple read iterations via an internal accumulator; frames that span chunk boundaries are correctly reassembled.
- If the encoded payload is empty (e.g. for `null` or `undefined`), a single zero-payload frame is emitted so the stream does not close silently without producing any data.
- The Web Streams API (`ReadableStream`) is used throughout, making the codec compatible with browser, Node.js (≥18), Bun, and Deno environments without polyfills.

View File

@@ -0,0 +1,57 @@
# wire.schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the Zod schemas that pin the **wire-format invariants** of the serialize codecs. Both the JSON codec and the MessagePack codec produce structurally identical intermediate values: a recursive tree of JSON-compatible primitives, arrays, plain records, and tagged envelopes (`{ "__t": "<tag>", "v": <encoded> }`). This module defends the public `deserialize` and `decodeBinary` boundaries against accidental or malicious inputs that would otherwise reach the type registry's `decode` functions in an unexpected shape — for example a number where an envelope is expected, or an envelope with `__t` set to a non-string value.
The schemas are exported as both reusable values (`WireNodeSchema`, `TaggedEnvelopeSchema`) and an ergonomic helper (`validateWireNode`) that wraps `safeParse` and exposes the parse outcome as a discriminated union so call sites can wrap rejection in a `MalformedWireError` with stable code metadata.
## Features
- Recursive `WireNodeSchema` covering primitives, arrays, tagged envelopes, and plain records
- Strict `TaggedEnvelopeSchema` that rejects extra keys
- `validateWireNode` helper returning `{ ok: true; value } | { ok: false; error }`
- Used by `deserialize` (JSON) and `decodeBinary` (MessagePack) at the public boundary
- Pure, side-effect free; tree-shakes when unused
## Usage: Basic Example
```ts
import { validateWireNode } from "@suaveplan/serialize";
const result = validateWireNode({ __t: "__Date", v: "2024-01-01T00:00:00.000Z" });
if (result.ok) {
// safe to feed to the type-registry decode pipeline
}
```
## Usage: Advanced Example
```ts
import { MalformedWireError, validateWireNode } from "@suaveplan/serialize";
function strictDeserialize(input: unknown): unknown {
const wire = validateWireNode(input);
if (!wire.ok) {
throw new MalformedWireError(
"input does not match the tagged-wire schema",
"root",
wire.error
);
}
return wire.value;
}
```
## API Reference
- `WireNodeSchema: z.ZodType<unknown>` — recursive schema for any wire-format node.
- `TaggedEnvelopeSchema: z.ZodObject<{ __t: ZodString, v: ZodUnknown }>` — strict envelope schema.
- `validateWireNode(value: unknown): { ok: true; value: unknown } | { ok: false; error: ZodError }` — non-throwing validator.
## Implementation Notes
`WireNodeSchema` is defined via `z.lazy()` to support recursion through arrays and records. The envelope schema uses `.strict()` so an envelope with a smuggled-in third key is rejected — this matches the `serialize`/`encodeBinary` output, which only ever emits `__t` + `v`. We intentionally exclude `BigInt`, `Map`, `Set`, and other JS-only constructs from the schema: the wire format never produces them at this layer (BigInts arrive as a `__BigInt` envelope with a string `v`), so admitting them would weaken the invariant.