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,130 @@
# @suaveplan/i18n-backend-storage - Universal translation backend backed by any AbstractStorage from @suaveplan/storage. Three wire layouts: single-key-per-namespace (default), single-key-per-locale, flat-keys. Pluggable codec, AbortSignal cancellation, OpenTelemetry instrumentation under i18n.backend.storage.*. Zero runtime dependencies on @suaveplan/i18n core.
## Summary
Universal translation backend for the `@suaveplan/i18n` family,
backed by any `AbstractStorage` implementation from
`@suaveplan/storage`. Implements the full `Backend` contract
(`read`, `readMulti`, `save`, `lastModified`) over three wire
layouts: `single-key-per-namespace` (default),
`single-key-per-locale`, and `flat-keys`. Pluggable codec, optional
`AbortSignal` cancellation, OpenTelemetry instrumentation under the
`i18n.backend.storage.*` prefix. Zero runtime dependencies on
`@suaveplan/i18n` core.
<!-- begin:auto-docs -->
## Documentation tree
- [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_- [backend](./src/backend/backend.md) — <!-- begin:auto-docs --> _(no submodules)_- [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_ - [factory](./src/factory/factory.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_ - [observability](./src/observability/observability.md) — <!-- begin:auto-docs --> _(no submodules)_
## Why this exists
The Wave 5 modular split lifts each backend out of the
`@suaveplan/i18n` monolith into its own sibling so consumers can
install only the backends they need. The storage backend is the
canonical persistence layer: pair it with `MemoryStorage` for tests,
`OpfsStorage` / `IndexedDBStorage` for offline-first browser apps,
or `S3Storage` / `GarageStorage` / `NodeFsStorage` /
`SqliteStorage` for server-tier deployments. Splitting it out also
isolates `@suaveplan/storage` as a transitive dep — consumers using
only the HTTP or memory backends no longer pull it in.
## Install
```bash
bun add @suaveplan/i18n-backend-storage @suaveplan/storage
```
## Quick Start
```ts
import { storageBackend } from "@suaveplan/i18n-backend-storage";
import { MemoryStorage } from "@suaveplan/storage";
const backend = storageBackend({ storage: new MemoryStorage() });
await backend.save("en", "auth", { login: { title: "Sign in" } });
const record = await backend.read("en", "auth");
// → { login: { title: "Sign in" } }
```
## Modules
| Subpath | Description |
|---|---|
| `@suaveplan/i18n-backend-storage` | Root barrel — class + factory + types + schemas + observability |
| `@suaveplan/i18n-backend-storage/backend` | `StorageBackend` class + `defaultCodec` |
| `@suaveplan/i18n-backend-storage/factory` | `storageBackend` factory function only |
| `@suaveplan/i18n-backend-storage/schemas` | Zod boundary schemas |
| `@suaveplan/i18n-backend-storage/observability` | OTel instruments + in-memory state ring |
## Modules Documentation
- [backend.md](src/backend/backend.md) — `StorageBackend` class, layouts, codec
- [factory.md](src/factory/factory.md) — `storageBackend` factory function
- [observability.md](src/observability/observability.md) — OTel instruments + cardinality
## Capabilities
| Flag | Value | Notes |
|---|---|---|
| `canRead` | `true` | always |
| `canReadMulti` | `true` | per-layout dispatch |
| `canSave` | `true` | persists into the wrapped storage |
| `canTrackModification` | `true` | consults `getMetadata().modifiedAt` |
| `canWatch` | `false` | the storage contract has no change feed |
## Layouts
- `single-key-per-namespace` (default) — one key per `(locale, ns)`
at `<prefix>/<locale>/<namespace>.json`. Best for HTTP-CDN-style
storages.
- `single-key-per-locale` — one key per locale holding the whole
namespace map. Best when namespaces are small and read together.
- `flat-keys` — one key per leaf string at
`<prefix>:<locale>:<namespace>:<dotPath>`. Best for content-
addressed storages with cheap per-leaf delta shipping.
## Testing
The package's own tests use the shared `runBackendConformance`
harness from `@suaveplan/i18n-shared/testing` so the universal
`Backend` contract is exercised against a fresh storage backend
per assertion (against all three layouts):
```ts
import { runBackendConformance } from "@suaveplan/i18n-shared/testing";
import { StorageBackend } from "@suaveplan/i18n-backend-storage";
import { MemoryStorage } from "@suaveplan/storage";
runBackendConformance("StorageBackend", async () => {
const backend = new StorageBackend({ storage: new MemoryStorage() });
return {
backend,
async seed(locale, namespace, record) {
await backend.save(locale, namespace, record as never);
},
};
});
```
## Versioning
This package is part of the i18n modular-split rollout. Stays at
`0.1.0` until the slim core (Wave 6) cuts the family `1.0.0`
release. The metric names under `i18n.backend.storage.*` are part of
the public API contract and will not change without a major bump.
## Wave 5 Scope
This package is Wave 5b of the 14-package i18n modular split. It
ships pure additions only — `packages/i18n/i18n/src/core/backend/storage/`
retains its copy of the backend until Wave 6 deletes it. Both
copies coexist during Wave 5; downstream consumers continue to
import from the monolith and observe no behaviour change.

View File

@@ -0,0 +1,167 @@
# `StorageBackend`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`StorageBackend` adapts any `AbstractStorage` instance from
`@suaveplan/storage` into a translation `Backend` for the
`@suaveplan/i18n` family. This is the headline persistence
integration that lets translations live in S3, OPFS, IndexedDB,
SQLite, Garage, or any other storage `@suaveplan/storage` supports
— without each consumer integrating once per source. The user-
facing pitch is "translations in OPFS for offline" or "translations
on a content-addressed CDN" become one config line.
The backend has zero runtime dependencies on `@suaveplan/i18n` core.
It implements the full `Backend` contract from `@suaveplan/types`
(`read`, `readMulti`, `save`, `lastModified`) and emits
OpenTelemetry instruments under the `i18n.backend.storage.*` prefix
so production teams can dashboard latency and miss-rate per layout.
## Features
- Full 5-flag capability matrix: `canRead`, `canReadMulti`,
`canSave`, `canTrackModification` all `true`; `canWatch: false`.
- Three wire layouts: `single-key-per-namespace` (default),
`single-key-per-locale`, `flat-keys`.
- Pluggable codec — JSON by default; swap for MessagePack, CBOR, or
schema-validated JSON.
- Optional `signal?: AbortSignal` parameter on `read`, `readMulti`,
and `save`; pre-aborted signals throw
`I18nError(I18N.BACKEND_TIMEOUT)` without touching the underlying
storage.
- OTel histograms + counters emitted on every read/save with a
`layout` attribute so per-layout latency can be charted.
- Zod boundary validation: invalid options throw
`I18nError(I18N.INVALID_OPTIONS)` at construction time.
- `BACKEND_LOCALE_NOT_AVAILABLE` on miss (chained backends fall
through); `BACKEND_READ_FAILED` / `BACKEND_WRITE_FAILED` on
transport / codec errors (chained backends propagate without
fallthrough).
## Layouts
### `single-key-per-namespace` (default)
```text
i18n/en/auth.json → {"login":{"title":"Sign in"}}
i18n/en/common.json → {"cancel":"Cancel"}
```
One storage key per `(locale, namespace)`. Reads are exactly one
storage `get` per `read(locale, ns)` call. Best for HTTP-CDN-style
storages where each key is independently cacheable.
### `single-key-per-locale`
```text
i18n/en.json → {"auth":{...},"common":{...}}
```
One storage key per locale, holding the entire
`Record<namespace, TranslationRecord>` map. Best when namespaces are
small and read together. Saving a single namespace requires a
read-modify-write of the whole locale map.
### `flat-keys`
```text
i18n:en:auth:login.title → Sign in
i18n:en:common:cancel → Cancel
```
One storage key per leaf string. Best for content-addressed storages
where deltas are expensive at namespace granularity but cheap at
leaf granularity.
## Usage: Basic Example
```ts
import { StorageBackend } from "@suaveplan/i18n-backend-storage";
import { MemoryStorage } from "@suaveplan/storage";
const backend = new StorageBackend({ storage: new MemoryStorage() });
await backend.save("en", "auth", { login: { title: "Sign in" } });
const record = await backend.read("en", "auth");
// → { login: { title: "Sign in" } }
```
## Usage: Advanced Example — OPFS + custom binary codec
```ts
import { StorageBackend } from "@suaveplan/i18n-backend-storage";
import { OpfsStorage } from "@suaveplan/storage-browser-opfs";
import { encode, decode } from "@msgpack/msgpack";
const backend = new StorageBackend({
storage: new OpfsStorage({ rootDir: "i18n" }),
layout: "single-key-per-locale",
encode: (record) => encode(record),
decode: (bytes) => decode(bytes) as Record<string, unknown>,
});
// Cancellable reads play nicely with multi-backend orchestration.
const controller = new AbortController();
controller.abort();
await backend.read("en", "auth", controller.signal); // throws BACKEND_TIMEOUT
```
## API Reference
### `class StorageBackend implements Backend`
- `constructor(options: StorageBackendOptions)` — validates options
via `StorageBackendOptionsSchema`; throws
`I18nError(I18N.INVALID_OPTIONS)` on schema mismatch or missing
`storage`.
- `name: string` — backend label (default `"storage"`).
- `capabilities: BackendCapabilities` — frozen 5-flag matrix.
- `read(locale, namespace, signal?): Promise<TranslationRecord>` —
resolves with the stored record; throws
`I18nError(I18N.BACKEND_LOCALE_NOT_AVAILABLE)` on miss.
- `readMulti(locale, namespaces, signal?):
Promise<Record<string, TranslationRecord>>` — returns only the
namespaces that exist; missing namespaces are omitted.
- `save(locale, namespace, record, signal?): Promise<void>` —
persists the record; failure surfaces as
`BACKEND_WRITE_FAILED`.
- `lastModified(locale, namespace): Promise<number | undefined>` —
returns the underlying storage's `modifiedAt` field, or
`undefined` if metadata is unavailable.
### `defaultCodec: StorageBackendCodec`
JSON-based codec (`TextEncoder` / `JSON.stringify`). The decode path
rejects non-object payloads (strings, arrays, null) as
`BACKEND_READ_FAILED`.
## Implementation Notes
- The default JSON codec uses `TextEncoder` / `TextDecoder` and
`JSON.stringify` / `JSON.parse`. The decode path rejects non-
object payloads because translation records are always objects in
the contract; custom codecs MUST also reject non-objects or
document the relaxation explicitly.
- `flat-keys` reads via `AbstractStorage.list({ prefix })`. List is
required by every concrete storage, so the layout works against
all of them. Saves write each leaf with its own `set` call — for
backends that batch writes, prefer `single-key-per-namespace` to
amortise the batch.
- `lastModified` for `flat-keys` takes the maximum `modifiedAt`
across leaves. For other layouts it consults the namespace or
locale key directly. All paths swallow storage exceptions and
return `undefined` rather than throw, mirroring the contract that
`lastModified` is a hint, not a guarantee.
- `signal?.aborted` is checked at the entry of every async method.
The throw surfaces as `I18nError(I18N.BACKEND_TIMEOUT)`. The flag
is sampled exactly once at the top — once the underlying
`AbstractStorage` operation begins, abort propagation is the
storage's responsibility.
- OTel `recordRead` / `recordWrite` calls emit zero overhead unless
a `MeterProvider` is registered. Tests use the in-memory metrics
state ring exposed by `./observability` to assert payloads
without standing up the full SDK. The `layout` attribute is a
three-value low-cardinality enum.

View File

@@ -0,0 +1,94 @@
# `storageBackend(options)`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Factory wrapper around `new StorageBackend(options)` for callers
that prefer fluent builder-style composition over `new`. Returns a
fresh backend per call — no internal singleton, no shared state, no
caching. The factory exists primarily so chained-backend
configurations can be expressed declaratively without the visual
weight of repeated `new` keywords.
The factory is the recommended public entry point for routine use
because it composes naturally with the rest of the i18n family
(every backend ships a `<name>Backend` factory mirroring its class
constructor). Direct `new StorageBackend(...)` remains supported and
is exactly equivalent.
## Features
- Returns a fresh `StorageBackend` per call.
- Forwards every constructor option (`storage`, `layout`, `prefix`,
`name`, `encode`, `decode`) unchanged.
- Same Zod boundary validation as the class constructor — invalid
options throw `I18nError(I18N.INVALID_OPTIONS)`.
## Usage: Basic Example
```ts
import { storageBackend } from "@suaveplan/i18n-backend-storage";
import { MemoryStorage } from "@suaveplan/storage";
const backend = storageBackend({ storage: new MemoryStorage() });
await backend.save("en", "common", { hello: "Hello" });
const record = await backend.read("en", "common");
// → { hello: "Hello" }
```
## Usage: Advanced Example
The factory composes naturally with chained-backend configurations
where multiple storages (e.g. an OPFS write-through cache plus an
S3 origin) are wired declaratively.
```ts
import { storageBackend } from "@suaveplan/i18n-backend-storage";
import { MemoryStorage } from "@suaveplan/storage";
const cache = storageBackend({
storage: new MemoryStorage(),
layout: "single-key-per-locale",
name: "cache",
});
const origin = storageBackend({
storage: new MemoryStorage(),
layout: "flat-keys",
name: "origin",
prefix: "translations",
});
// Hand both to a ChainedBackend (Wave 5e).
// const chain = chainedBackend({ backends: [cache, origin] });
```
## API Reference
### `storageBackend(options: StorageBackendOptions): StorageBackend`
- `options.storage: AbstractStorage` — required.
- `options.layout?: StorageBackendLayout` — wire layout (default
`"single-key-per-namespace"`).
- `options.prefix?: string` — key prefix (default `"i18n"`).
- `options.name?: string` — backend label (default `"storage"`).
- `options.encode?: (record) => Uint8Array` — custom encoder.
- `options.decode?: (bytes) => TranslationRecord` — custom decoder.
- Returns: a fresh `StorageBackend` instance.
## Implementation Notes
- The factory is a one-liner: `return new StorageBackend(options)`.
It exists for ergonomic parity with the rest of the
`<name>Backend` factory pattern across the i18n backend family
and adds no behaviour beyond instantiation.
- The factory does NOT memoise — every call returns a distinct
backend instance. Callers needing shared state pass the same
backend reference to multiple call sites; do not call the
factory multiple times expecting the same store.
- For consumers who need polymorphism over
`BackendFactory<TOptions>` from `@suaveplan/types`, this factory
satisfies that signature.

View File

@@ -0,0 +1,132 @@
# `StorageBackend` Observability
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns the OpenTelemetry instrumentation for
`@suaveplan/i18n-backend-storage`. It ships per-call metric emitters
(`recordRead`, `recordWrite`) that the backend invokes around every
`read` / `save` operation, plus a small in-memory state ring so
spec-conformance tests can assert exact attribute payloads without
standing up a full SDK + InMemoryMetricExporter pipeline. Every
instrument is built lazily and is a no-op when no MeterProvider is
registered — consumers pay zero overhead unless they wire up the
SDK.
The metric naming follows the per-backend prefix convention agreed
in the Wave 5 design: `i18n.backend.storage.*`. Sibling backend
packages emit under their own prefixes so consumers can group
dashboard panels per backend without shared-namespace collisions.
Every counter and histogram carries a low-cardinality `layout`
attribute (`single-key-per-namespace` | `single-key-per-locale` |
`flat-keys`) so production teams can drill into per-layout latency
or miss-rate.
## Features
- Five instruments — two histograms, three counters — covering both
read and write paths.
- `layout` attribute on every aggregate so per-layout dashboards
work without parsing namespaces.
- In-memory state ring for test assertions (no exporter required).
- Lazy meter construction; idempotent
`getStorageBackendMetrics()`.
- Test-only `resetStorageBackendMetricsCache()` so suites can
rebuild the meter under a fresh provider.
## Usage: Basic Example
```ts
import {
getStorageBackendMetricsState,
resetStorageBackendMetricsState,
} from "@suaveplan/i18n-backend-storage/observability";
import { StorageBackend } from "@suaveplan/i18n-backend-storage";
import { MemoryStorage } from "@suaveplan/storage";
resetStorageBackendMetricsState();
const backend = new StorageBackend({ storage: new MemoryStorage() });
await backend.save("en", "auth", { ok: "Ok" });
await backend.read("en", "auth");
const state = getStorageBackendMetricsState();
expect(state.readTotal).toEqual([
{ attributes: { outcome: "hit", layout: "single-key-per-namespace" } },
]);
```
## Usage: Advanced Example
A read miss emits both an outcome counter AND a
`read_errors_total` counter labelled by error code. Both carry the
`layout` attribute so per-layout error rates are charted directly.
```ts
import {
getStorageBackendMetricsState,
resetStorageBackendMetricsState,
} from "@suaveplan/i18n-backend-storage/observability";
import { StorageBackend } from "@suaveplan/i18n-backend-storage";
import { MemoryStorage } from "@suaveplan/storage";
resetStorageBackendMetricsState();
const backend = new StorageBackend({
storage: new MemoryStorage(),
layout: "flat-keys",
});
await backend.read("en", "missing").catch(() => undefined);
const state = getStorageBackendMetricsState();
expect(state.readErrorsTotal).toEqual([
{
attributes: {
code: "I18N.BACKEND_LOCALE_NOT_AVAILABLE",
layout: "flat-keys",
},
},
]);
```
## API Reference
### Emitted instruments
| Instrument | Kind | Labels | Unit |
|---|---|---|---|
| `i18n.backend.storage.read_duration_ms` | Histogram | `locale`, `namespace`, `outcome`, `layout` | ms |
| `i18n.backend.storage.read_total` | Counter | `outcome` (hit \| miss \| error), `layout` | count |
| `i18n.backend.storage.read_errors_total` | Counter | `code` (I18N error code), `layout` | count |
| `i18n.backend.storage.write_duration_ms` | Histogram | `locale`, `namespace`, `outcome`, `layout` | ms |
| `i18n.backend.storage.write_total` | Counter | `outcome` (ok \| error), `layout` | count |
### Functions
- `getStorageBackendMetrics()` — returns cached instruments; builds
them on first call.
- `recordRead(locale, namespace, layout, outcome, durationMs, errorCode?)`
— emit a read sample.
- `recordWrite(locale, namespace, layout, outcome, durationMs)` —
emit a write sample.
- `getStorageBackendMetricsState()` — frozen-shape snapshot of every
emitted sample since last reset.
- `resetStorageBackendMetricsState()` — clear the in-memory state
ring (test fixture).
- `resetStorageBackendMetricsCache()` — drop the cached instruments
so a fresh `MeterProvider` can be wired (test fixture).
## Implementation Notes
- The state ring is intentionally cardinality-bounded by the
caller's test cases. It exists for assertion ergonomics, not for
production telemetry — production telemetry flows through the
registered OTel `MeterProvider`.
- Cardinality safety: aggregate counters use only `outcome` +
`layout` (each three or fewer values); per-error-code attribution
lives on a separate `read_errors_total` counter so consumers can
drop the high-cardinality counter without losing aggregate
visibility.
- Metric names are stable: every `i18n.backend.storage.*` name is
part of the package's public API contract and will not change
without a major bump.