Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
# `@suaveplan/i18n-backend-memory` — in-memory translation backend
**Package**: `@suaveplan/i18n-backend-memory` (NEW)
**Status**: PROPOSED
**Type**: feature (new universal-tier package; verbatim port from monolith)
**Date**: 2026-05-05
**Wave**: 5
**Design doc**: `openspec/cross-cutting-designs/2026-05-05-i18n-modular-split.md`
**Path**: `packages/i18n/i18n-backend-memory/`
## Why
The Wave 5 backend split lifts each `Backend` implementation from the `@suaveplan/i18n` monolith into its own sibling package so consumers can install only the backends they need (matching the `crypto-*` / `drive-*` / `network-plugin-*` granularity established elsewhere in the monorepo). The in-memory backend is the simplest and most universal of the five — backed by `Map<locale, Map<namespace, TranslationRecord>>`, it ships the full `Backend` contract (`read` / `readMulti` / `save` / `lastModified`), is the canonical seed source for tests, and is the standard cache layer for `ChainedBackend`. Because it has zero external runtime deps, it is the cleanest candidate to land first in the Wave 5 batch.
The `Backend` interface itself moves to `@suaveplan/types/i18n` in Wave 1 (zero-runtime types extension). Every backend in this wave implements `Backend` from that location — `@suaveplan/i18n-backend-memory` does NOT depend on `@suaveplan/i18n` core. The runtime atoms (`I18nError`, `I18N_CODES`) come from `@suaveplan/i18n-shared` (Wave 1b).
## What changes
### New package: `@suaveplan/i18n-backend-memory`
- ADDED: `packages/i18n/i18n-backend-memory/` package scaffold (package.json, tsconfig.json, vite.config.ts, README.md).
- ADDED: `src/backend.ts` — `MemoryBackend` class implementing `Backend` from `@suaveplan/types/i18n`. Source: `packages/i18n/i18n/src/core/backend/memory/memory-backend.ts` L36-L133. Backed by `Map<locale, Map<namespace, TranslationRecord>>` plus a `Map<string, number>` modification ledger keyed by `${locale} ${namespace}`. Capabilities: `canRead: true`, `canReadMulti: true`, `canSave: true`, `canTrackModification: true`, `canWatch: false`.
- ADDED: `src/backend.types.ts` — `MemorySeed = Record<string, Record<string, TranslationRecord>>` and `MemoryBackendOptions` ({ `seed?`, `name?`, `now?` }). Source: monolith L11-L26.
- ADDED: `src/backend.schemas.ts` — Zod schema `MemoryBackendOptionsSchema` validating the constructor options at the public boundary.
- ADDED: `src/factory.ts` — `memoryBackend(options?: MemoryBackendOptions): MemoryBackend` factory function. Source: monolith L136-L138.
- ADDED: `src/observability.ts` — emits `i18n.backend.memory.read_duration_ms` (histogram), `i18n.backend.memory.read_total{outcome}` (counter), `i18n.backend.memory.read_errors_total{code}` (counter), `i18n.backend.memory.write_duration_ms` (histogram), `i18n.backend.memory.write_total{outcome}` (counter). The histogram pattern follows the existing `recordRead` / `recordWrite` helpers in the monolith (`packages/i18n/i18n/src/observability/backend-metrics.ts`) but is inlined here so the leaf does not depend on the monolith.
- ADDED: `src/backend.test.ts`, `src/backend.spec-conformance.test.ts`, `src/factory.test.ts`, `src/index.test.ts` — Vitest suites verbatim-ported from monolith `memory-backend.test.ts` plus new spec-conformance suites.
- ADDED: `src/backend.md`, `src/factory.md` — co-located docs; root `README.md` links them.
- ADDED: `src/index.ts` — explicit named re-exports (`MemoryBackend`, `memoryBackend`, type `MemoryBackendOptions`, type `MemorySeed`).
### Subpath exports
- `.` — root barrel (`MemoryBackend` class + `memoryBackend` factory + types)
- `./backend` — `MemoryBackend` class only
- `./factory` — `memoryBackend` factory function only
### Dependencies
- `@suaveplan/i18n-shared workspace:^` — for `I18N_CODES`, `I18nError`
- `@suaveplan/types workspace:^` — for `Backend`, `BackendCapabilities`, `TranslationRecord` from `@suaveplan/types/i18n`
- `@suaveplan/error workspace:^` — for `defineCodes`-derived consumer-side helpers (if needed by tests)
- `@opentelemetry/api catalog:observability` — for histogram + counter emission
- `tslib catalog:build`
- `zod catalog:forms` — for `MemoryBackendOptionsSchema`
- **No** dep on `@suaveplan/i18n` core. **No** dep on any other backend package.
### Tier
Universal. Runs in browser AND server with no platform-specific imports. The `Map`-backed store and `Date.now` fallback work identically across runtimes.
## Out of scope
- **Removing the in-memory backend from `packages/i18n/i18n/src/core/backend/memory/`** — happens in Wave 6 (`i18n-core-slim`). For Wave 5, both copies coexist; the monolith remains green.
- **Persistent variants of the in-memory store** — `WeakMap` / TTL-eviction / LRU bounds are explicit non-goals; consumers who need eviction wire `MemoryBackend` behind a `ChainedBackend` cache layer or use `StorageBackend` against `MemoryStorage`.
- **Async seed loading** — the constructor `seed` is synchronous; consumers who load seeds asynchronously call `save()` post-construction.
- **Cross-instance event broadcasting** — the in-memory backend is per-instance by design; consumers who need broadcast wire the `EventBus` from `@suaveplan/events` themselves.
## Source mapping
| Requirement | Source file | Source lines | Notes |
|---|---|---|---|
| `MemoryBackend` class + capabilities matrix | `packages/i18n/i18n/src/core/backend/memory/memory-backend.ts` | L36-L60 | Verbatim port; rewire imports to `@suaveplan/i18n-shared` and `@suaveplan/types/i18n` |
| `MemoryBackend.read` (hit + miss paths, `BACKEND_LOCALE_NOT_AVAILABLE` on miss) | same file | L62-L84 | Verbatim port |
| `MemoryBackend.readMulti` | same file | L86-L100 | Verbatim port |
| `MemoryBackend.save` | same file | L102-L109 | Verbatim port |
| `MemoryBackend.lastModified` | same file | L111-L116 | Verbatim port |
| `MemoryBackend.clear` test-only helper | same file | L119-L122 | Verbatim port |
| `MemorySeed`, `MemoryBackendOptions` types | same file | L11-L26 | Verbatim port to `backend.types.ts` |
| `memoryBackend` factory | same file | L136-L138 | Lifted into `factory.ts` |
| `recordRead` / `recordWrite` instrumentation pattern | `packages/i18n/i18n/src/observability/backend-metrics.ts` | full file | Reimplemented inline in `src/observability.ts`; same metric names with `i18n.backend.memory.` prefix |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| Subpath exports `./backend` and `./factory` | Tree-shaking — consumers who construct via the factory should not pull the full `Backend` class transitively; consumers who subclass should not pull factory wrappers |
| `MemoryBackendOptionsSchema` Zod schema | Boundary validation — every backend in Wave 5 validates options at the public boundary, matching the pattern in `HttpBackend` (already does this) |
| `AbortSignal` parameter on `read` / `readMulti` / `save` | Cancellation parity across backends — every Wave 5 backend accepts an optional `AbortSignal` so chained / multi-backend orchestration can propagate cancellation |
## Verification approach
Each requirement carries a `**Verified by:**` line in the spec delta. Concrete test files:
- `packages/i18n/i18n-backend-memory/src/backend.test.ts` — full `Backend` contract (read / readMulti / save / lastModified) round-trip, hit + miss paths, capabilities matrix, custom `name` / `now` injection
- `packages/i18n/i18n-backend-memory/src/backend.spec-conformance.test.ts` — every spec requirement mapped 1:1 to an `it(...)` title for traceability
- `packages/i18n/i18n-backend-memory/src/factory.test.ts` — `memoryBackend()` returns `MemoryBackend` instance; passes seed through; defaults `name` to `"memory"`
- `packages/i18n/i18n-backend-memory/src/index.test.ts` — every documented public export resolves through every documented subpath
## Impact
- New universal-tier package — joins the i18n family under `packages/i18n/`.
- No downstream consumer changes needed in Wave 5 — the slim core (Wave 6) re-exports `MemoryBackend` and `memoryBackend` from the new package, preserving every existing `import { MemoryBackend } from "@suaveplan/i18n"` call site.
- `MANIFEST.md` regenerated via `bun scripts/folder-reorg/generate-manifest.ts` after the package lands.
- `CLAUDE.md` package listing updated with a one-line entry under the i18n cluster.

View File

@@ -0,0 +1,200 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec: i18n-backend-memory (i18n-backend-memory)
**Package**: `@suaveplan/i18n-backend-memory`
**Status**: PROPOSED — first delta; becomes canonical spec on archive.
> Every requirement below carries a `**Verified by:**` line at authoring time. (See `openspec/AGENTS.md` Section 13.)
## ADDED Requirements
### Requirement: MemoryBackend implements the Backend interface
The package SHALL export a `MemoryBackend` class that implements the `Backend` interface from `@suaveplan/types/i18n` (universal-tier, browser + server). The class SHALL declare a static `capabilities` matrix of `{ canRead: true, canReadMulti: true, canSave: true, canTrackModification: true, canWatch: false }` and SHALL accept an optional `MemoryBackendOptions` constructor argument carrying `{ seed?, name?, now? }`. Default `name` SHALL equal `"memory"`. Default `now` SHALL equal `Date.now`.
#### Scenario: capabilities matrix matches the contract
- **WHEN** `new MemoryBackend().capabilities` is read
- **THEN** the result equals `{ canRead: true, canReadMulti: true, canSave: true, canTrackModification: true, canWatch: false }`
#### Scenario: default name is "memory"
- **WHEN** `new MemoryBackend().name` is read
- **THEN** the value equals `"memory"`
#### Scenario: custom name override
- **WHEN** `new MemoryBackend({ name: "cache" }).name` is read
- **THEN** the value equals `"cache"`
**Verified by:** `packages/i18n/i18n-backend-memory/src/backend.test.ts::"capabilities matrix matches the contract"` AND `packages/i18n/i18n-backend-memory/src/backend.test.ts::"default name is memory"` AND `packages/i18n/i18n-backend-memory/src/backend.test.ts::"custom name override"`
---
### Requirement: read returns the seeded record on hit
The `read(locale, namespace, signal?)` method SHALL return the `TranslationRecord` previously seeded for the `(locale, namespace)` pair. Lookup SHALL be O(1) via the underlying `Map<locale, Map<namespace, TranslationRecord>>` data structure.
#### Scenario: read hit returns seeded record
- **GIVEN** a `MemoryBackend` constructed with `{ seed: { en: { common: { hello: "Hello" } } } }`
- **WHEN** `backend.read("en", "common")` is awaited
- **THEN** the result equals `{ hello: "Hello" }`
**Verified by:** `packages/i18n/i18n-backend-memory/src/backend.test.ts::"read hit returns seeded record"`
---
### Requirement: read miss throws BACKEND_LOCALE_NOT_AVAILABLE
The `read` method SHALL throw `I18nError` with code `I18N.BACKEND_LOCALE_NOT_AVAILABLE` when the `(locale, namespace)` pair has no record. The error context SHALL include `{ locale, namespace, backend }` so chained-backend orchestration can log the missed lookup.
#### Scenario: read miss throws BACKEND_LOCALE_NOT_AVAILABLE
- **GIVEN** an empty `MemoryBackend`
- **WHEN** `backend.read("en", "common")` is awaited
- **THEN** the call throws `I18nError` with code `"I18N.BACKEND_LOCALE_NOT_AVAILABLE"`
**Verified by:** `packages/i18n/i18n-backend-memory/src/backend.test.ts::"read miss throws BACKEND_LOCALE_NOT_AVAILABLE"`
---
### Requirement: readMulti returns partial records without throwing
The `readMulti(locale, namespaces, signal?)` method SHALL return a `Record<namespace, TranslationRecord>` map containing only the namespaces that exist for the locale. Missing namespaces SHALL be omitted from the map (NOT undefined values, NOT thrown errors). When the locale is entirely absent, the method SHALL return `{}`.
#### Scenario: readMulti omits missing namespaces
- **GIVEN** a `MemoryBackend` seeded with `{ en: { common: {...}, errors: {...} } }`
- **WHEN** `backend.readMulti("en", ["common", "missing"])` is awaited
- **THEN** the result is `{ common: {...} }` and does NOT contain a `missing` key
#### Scenario: readMulti returns empty for absent locale
- **WHEN** `backend.readMulti("xx", ["any"])` is awaited
- **THEN** the result is `{}`
**Verified by:** `packages/i18n/i18n-backend-memory/src/backend.test.ts::"readMulti omits missing namespaces"` AND `packages/i18n/i18n-backend-memory/src/backend.test.ts::"readMulti returns empty for absent locale"`
---
### Requirement: save round-trip with deterministic lastModified
The `save(locale, namespace, record, signal?)` method SHALL persist the record into the in-memory store and SHALL update the modification ledger to the value of the injected `now()` function. A subsequent `lastModified(locale, namespace)` call SHALL return that exact timestamp.
#### Scenario: save then read round-trips
- **WHEN** `backend.save("en", "common", { hello: "Hi" })` is awaited and then `backend.read("en", "common")` is awaited
- **THEN** the read result equals `{ hello: "Hi" }`
#### Scenario: lastModified returns the injected timestamp
- **GIVEN** a `MemoryBackend` constructed with `{ now: () => 1234 }`
- **WHEN** `backend.save("en", "common", { x: "y" })` is awaited and then `backend.lastModified("en", "common")` is awaited
- **THEN** the result equals `1234`
**Verified by:** `packages/i18n/i18n-backend-memory/src/backend.test.ts::"save then read round-trips"` AND `packages/i18n/i18n-backend-memory/src/backend.test.ts::"lastModified returns the injected timestamp"`
---
### Requirement: AbortSignal cancellation throws BACKEND_TIMEOUT
Every async method (`read`, `readMulti`, `save`) SHALL accept an optional `signal?: AbortSignal` parameter. When the signal is already aborted at call time, the method SHALL throw `I18nError` with code `I18N.BACKEND_TIMEOUT` and SHALL NOT mutate state.
#### Scenario: pre-aborted signal aborts read
- **GIVEN** an `AbortController` whose signal is already aborted
- **WHEN** `backend.read("en", "common", { signal })` is awaited
- **THEN** the call throws `I18nError` with code `"I18N.BACKEND_TIMEOUT"`
**Verified by:** `packages/i18n/i18n-backend-memory/src/backend.test.ts::"pre-aborted signal aborts read with BACKEND_TIMEOUT"`
---
### Requirement: memoryBackend factory function
The package SHALL export a `memoryBackend(options?: MemoryBackendOptions): MemoryBackend` factory function that returns `new MemoryBackend(options)`. The factory exists so consumers can compose backends fluently without `new`.
#### Scenario: factory returns MemoryBackend instance
- **WHEN** `memoryBackend()` is called
- **THEN** the result satisfies `result instanceof MemoryBackend`
#### Scenario: factory passes seed through
- **WHEN** `memoryBackend({ seed: { en: { common: { x: "y" } } } }).read("en", "common")` is awaited
- **THEN** the result equals `{ x: "y" }`
**Verified by:** `packages/i18n/i18n-backend-memory/src/factory.test.ts::"factory returns MemoryBackend instance"` AND `packages/i18n/i18n-backend-memory/src/factory.test.ts::"factory passes seed through"`
---
### Requirement: Boundary validation via Zod
The constructor SHALL validate `MemoryBackendOptions` via `MemoryBackendOptionsSchema` (Zod) and SHALL throw `I18nError(I18N.INVALID_OPTIONS)` on invalid input.
#### Scenario: invalid options throw INVALID_OPTIONS
- **WHEN** `new MemoryBackend({ seed: "not an object" as unknown as MemorySeed })` is called
- **THEN** the call throws `I18nError` with code `"I18N.INVALID_OPTIONS"`
**Verified by:** `packages/i18n/i18n-backend-memory/src/backend.test.ts::"invalid options throw INVALID_OPTIONS"`
---
### Requirement: OpenTelemetry instrumentation
The backend SHALL emit `i18n.backend.memory.read_duration_ms` (histogram, milliseconds), `i18n.backend.memory.read_total{outcome="hit"|"miss"|"error"}` (counter), `i18n.backend.memory.read_errors_total{code}` (counter), `i18n.backend.memory.write_duration_ms` (histogram), and `i18n.backend.memory.write_total{outcome="ok"|"error"}` (counter). The histograms SHALL be observed via `performance.now()` deltas around each call.
#### Scenario: read emits read_total counter on hit
- **WHEN** `backend.read("en", "common")` succeeds
- **THEN** `i18n.backend.memory.read_total` increments with `outcome="hit"`
#### Scenario: read emits read_total counter on miss
- **WHEN** `backend.read("en", "missing")` throws `BACKEND_LOCALE_NOT_AVAILABLE`
- **THEN** `i18n.backend.memory.read_total` increments with `outcome="miss"`
#### Scenario: save emits write_total counter
- **WHEN** `backend.save("en", "common", {...})` succeeds
- **THEN** `i18n.backend.memory.write_total` increments with `outcome="ok"`
**Verified by:** `packages/i18n/i18n-backend-memory/src/backend.spec-conformance.test.ts::"read emits read_total counter on hit"` AND `packages/i18n/i18n-backend-memory/src/backend.spec-conformance.test.ts::"read emits read_total counter on miss"` AND `packages/i18n/i18n-backend-memory/src/backend.spec-conformance.test.ts::"save emits write_total counter"`
---
### Requirement: Subpath exports
The package SHALL declare subpath exports so consumers can import only the surfaces they need:
- `@suaveplan/i18n-backend-memory` — root barrel (class + factory + types)
- `@suaveplan/i18n-backend-memory/backend` — `MemoryBackend` class only
- `@suaveplan/i18n-backend-memory/factory` — `memoryBackend` factory function only
#### Scenario: every subpath resolves
- **WHEN** every documented subpath is imported in a consumer test fixture
- **THEN** every import resolves with no `Module not found` errors
**Verified by:** `packages/i18n/i18n-backend-memory/src/index.test.ts::"every documented subpath resolves"`
---
### Requirement: Side-effect-free package
The package SHALL declare `"sideEffects": false` in `package.json` so bundlers can tree-shake unused exports, and SHALL NOT depend on `@suaveplan/i18n` core nor on any other backend package.
#### Scenario: package.json declares sideEffects false
- **WHEN** `package.json` is parsed
- **THEN** the `sideEffects` field equals `false`
#### Scenario: package.json does not depend on @suaveplan/i18n core
- **WHEN** `package.json` `dependencies` is parsed
- **THEN** the result does NOT contain `@suaveplan/i18n`
**Verified by:** `packages/i18n/i18n-backend-memory/src/index.test.ts::"package.json declares sideEffects false"` AND `packages/i18n/i18n-backend-memory/src/index.test.ts::"package.json does not depend on i18n core"`

View File

@@ -0,0 +1,83 @@
# Tasks — `@suaveplan/i18n-backend-memory` (in-memory translation backend)
> Every checkbox MUST reflect work actually done in this change. Do not retroactively tick.
## 1. Spec authoring
- [x] 1.1 Read `openspec/cross-cutting-designs/2026-05-05-i18n-modular-split.md` end-to-end (especially §3, §6, §6.1)
- [x] 1.2 Read `openspec/AGENTS.md` Sections 0, 0a, 6a, 7, 9, 13
- [x] 1.3 Read source: `packages/i18n/i18n/src/core/backend/memory/memory-backend.ts`, `memory-backend.test.ts`, `memory-backend.md`, plus `packages/i18n/i18n/src/core/backend/backend.types.ts` (interface contract that moves to `@suaveplan/types/i18n` in Wave 1)
- [x] 1.4 Draft `proposal.md` from `openspec/templates/proposal.md`
- [x] 1.5 Fill `## Source mapping` table with verified line ranges from monolith
- [x] 1.6 Draft `specs/i18n-backend-memory/spec.md` from `openspec/templates/spec-delta.md`
- [x] 1.7 Every requirement carries a `**Verified by:**` line naming concrete `it(...)` titles
- [ ] 1.8 `openspec change validate i18n-backend-memory` exits with no errors (openspec CLI not installed in this env — skipped; manual review confirms format compliance)
## 2. Package scaffold
- [x] 2.1 `mkdir -p packages/i18n/i18n-backend-memory/src`
- [x] 2.2 Author `package.json`
- [x] 2.3 Author `tsconfig.lib.json` extending `@suaveplan/typescript-config/library.json`
- [x] 2.4 Author `vite.config.ts` and `vitest.config.ts`
- [x] 2.5 Author `README.md` (≥200 words, includes Modules table linking to each `.md`)
- [x] 2.6 Add `package.json` `exports` map with subpaths `.`, `./backend`, `./factory`, `./schemas`, `./observability`
## 3. Implementation
- [x] 3.1 Author `src/backend.types.ts` — port `MemorySeed` and `MemoryBackendOptions`
- [x] 3.2 Author `src/backend.schemas.ts` — Zod `MemoryBackendOptionsSchema`
- [x] 3.3 Author `src/observability.ts` — inline `recordRead` / `recordWrite` with per-backend metric prefixes
- [x] 3.4 Author `src/backend.ts` — port `MemoryBackend` class; rewire imports; emit metrics; add optional `signal?: AbortSignal` parameter on `read` / `readMulti` / `save` and throw `I18nError(I18N.BACKEND_TIMEOUT)` when signal is aborted
## 4. Factory function
- [x] 4.1 Author `src/factory.ts` — `memoryBackend(options?): MemoryBackend`
## 5. Documentation (sibling `.md` per implementation file)
- [x] 5.1 `src/backend.md`
- [x] 5.2 `src/factory.md`
- [x] 5.3 `src/observability.md`
- [x] 5.4 N/A — no `backend.types.md` (per CLAUDE.md, types files inside a feature dir need no `.md`)
- [x] 5.5 `README.md` `## Modules` section links every `.md`
## 6. Tests (Vitest, runner-neutral imports per CLAUDE.md §11)
- [x] 6.1 `src/backend.test.ts` — runs `runBackendConformance` from `@suaveplan/i18n-shared/testing` and adds `MemoryBackend` specifics
- [x] 6.2 Spec-conformance assertions woven into `backend.test.ts` (one `it(...)` per spec scenario)
- [x] 6.3 `src/factory.test.ts`
- [x] 6.4 `src/index.test.ts` — every documented public export resolves through `.`, `./backend`, `./factory`, `./schemas`, `./observability` subpaths
- [x] 6.5 AbortSignal cancellation tests for `read`, `readMulti`, `save`
- [x] 6.6 Every `it(...)` carries `{ timeout: 5000 }`
- [x] 6.7 All test imports go through `@suaveplan/testing/runner` — `grep -rnE "from ['\"](vitest|bun:test)['\"]" packages/i18n/i18n-backend-memory/src/` returns zero hits
- [x] 6.8 No `// biome-ignore` suppressions
- [x] 6.9 No `export *` in any non-root barrel
## 7. Pre-archive verification (5-gate triple)
- [x] 7.1 `bun run check` — Biome 0/0/0 across the monorepo
- [x] 7.2 `bunx turbo lint typecheck test --filter=@suaveplan/i18n-backend-memory` — green
- [x] 7.3 `bunx turbo test:coverage --filter=@suaveplan/i18n-backend-memory` — 100/100/100/100
- [ ] 7.4 `bun scripts/openspec-conformance.ts @suaveplan/i18n-backend-memory` — script not present in this repo state; manual spec review performed
- [x] 7.5 `bunx turbo build --filter=@suaveplan/i18n-backend-memory` — green
## 8. Pre-archive 7-row checklist (AGENTS.md Section 13)
- [x] 8.1 Generics threaded — `MemoryBackend` implements `Backend` from `@suaveplan/types`
- [x] 8.2 Zod schemas wired at the public boundary — `MemoryBackendOptionsSchema` parses constructor input
- [x] 8.3 Every error code thrown by at least one production path: `BACKEND_LOCALE_NOT_AVAILABLE` (read miss), `BACKEND_TIMEOUT` (AbortSignal aborted), `INVALID_OPTIONS` (Zod parse failure)
- [x] 8.4 Strict-mode tested — capability matrix exact-shape match via `toEqual`
- [x] 8.5 OTel histograms emitted from production paths
- [x] 8.6 Lifecycle dedup — N/A (in-memory backend has no async init/dispose)
- [x] 8.7 No retroactive tick — every box reflects work done in this change
## 9. Manifest + ecosystem awareness
- [x] 9.1 Add `@suaveplan/i18n-backend-memory` entry to root `CLAUDE.md` package listing under the i18n cluster
- [x] 9.2 Run `bun scripts/folder-reorg/generate-manifest.ts` to refresh `MANIFEST.md`
- [x] 9.3 Append `@suaveplan/i18n-backend-memory` to `STATUS.md` Completed list
## 10. Archive
- [x] 10.1 Fold spec delta into `openspec/specs/i18n-backend-memory/spec.md` (NEW capability spec — first delta becomes canonical)
- [x] 10.2 Move `openspec/changes/i18n-backend-memory/` → `openspec/archive/2026-05-09-i18n-backend-memory/`