Files
2026-05-27 23:29:53 +01:00

9.0 KiB

Tasks — @suaveplan/i18n-backend-chained (chained-fallback backend orchestrator)

Every checkbox MUST reflect work actually done in this change. Do not retroactively tick.

1. Spec authoring

  • 1.1 Read openspec/cross-cutting-designs/2026-05-05-i18n-modular-split.md end-to-end (especially §3, §6, §6.1)
  • 1.2 Read openspec/AGENTS.md Sections 0, 0a, 6a, 7, 9, 13
  • 1.3 Read source: packages/i18n/i18n/src/core/backend/chained/chained-backend.ts, chained-backend.test.ts, chained-backend.spec-conformance.test.ts, chained-backend.schemas.ts, chained-backend.md, plus packages/i18n/i18n/src/core/backend/backend.types.ts
  • 1.4 Confirm Wave 1b (@suaveplan/i18n-shared) registry includes I18N.CHAINED_BACKEND_ALL_FAILED so this package can throw it without a registry-change dependency
  • 1.5 Draft proposal.md from openspec/templates/proposal.md
  • 1.6 Fill ## Source mapping table with verified line ranges from monolith
  • 1.7 Draft specs/i18n-backend-chained/spec.md from openspec/templates/spec-delta.md
  • 1.8 Every requirement carries a **Verified by:** line naming concrete it(...) titles
  • 1.9 openspec change validate i18n-backend-chained exits with no errors

2. Package scaffold

  • 2.1 mkdir -p packages/i18n/i18n-backend-chained/src
  • 2.2 Author package.json — name @suaveplan/i18n-backend-chained, version 0.1.0, type module, sideEffects false. Deps: @suaveplan/i18n-shared workspace:^, @suaveplan/types workspace:^, @suaveplan/error workspace:^, @opentelemetry/api catalog:observability, tslib catalog:build, zod catalog:forms. devDeps: @suaveplan/testing workspace:^, @suaveplan/typescript-config workspace:^, @vitest/coverage-istanbul catalog:testing, vitest catalog:testing. No workspace dep on any other backend package.
  • 2.3 Author tsconfig.json extending @suaveplan/typescript-config/library.json
  • 2.4 Author vite.config.ts using createTestConfig() from @suaveplan/testing/config
  • 2.5 Author README.md (≥200 words, includes Modules table linking to each .md)
  • 2.6 Add package.json exports map with subpaths ., ./backend, ./factory, ./write-through, ./merge

3. Implementation

  • 3.1 Author src/backend.types.ts — port ChainedBackendOptions and MergeStrategy from monolith L15-L28 verbatim
  • 3.2 Author src/backend.schemas.ts — port ChainedBackendOptionsSchema Zod schema verbatim (11 lines); validates backends is a non-empty array
  • 3.3 Author src/write-through.ts — extract #writeThrough logic into a standalone writeThrough(backends, successIndex, locale, namespace, record): Promise<void> (best-effort; swallows errors)
  • 3.4 Author src/merge.ts — extract inline merge logic from readMulti into a standalone mergeReadMulti(perBackend, strategy): Record<string, TranslationRecord>
  • 3.5 Author src/observability.ts — inline equivalents of recordRead / recordWrite; export i18n.backend.chained.read_duration_ms, i18n.backend.chained.write_duration_ms histograms; counters read_total{outcome}, read_errors_total{code}, write_total{outcome}, fallthrough_total{from_backend,to_backend} (low-cardinality — backends identified by their .name)
  • 3.6 Author src/backend.ts — port ChainedBackend class from monolith L43-L230; rewire imports; use writeThrough and mergeReadMulti helpers; emit metrics including the new fallthrough_total counter on every BACKEND_LOCALE_NOT_AVAILABLE-driven continue; add optional signal?: AbortSignal parameter on read / readMulti / save and thread it into every child call; throw I18nError(I18N.CHAINED_BACKEND_ALL_FAILED) if backends.length === 0 is reached at runtime (defence-in-depth past the schema check)

4. Factory function

  • 4.1 Author src/factory.ts — chainedBackend(backendsOrOptions: readonly Backend[] | ChainedBackendOptions): ChainedBackend supporting both array shorthand and full options object (per monolith L233-L242)

5. Documentation (sibling .md per implementation file)

  • 5.1 src/backend.md — port from packages/i18n/i18n/src/core/backend/chained/chained-backend.md; ≥200 words; document fallthrough semantics, write-through cache pattern, BACKEND_LOCALE_NOT_AVAILABLE vs CHAINED_BACKEND_ALL_FAILED distinction
  • 5.2 src/factory.md — ≥200 words; basic + advanced examples (array shorthand vs options object)
  • 5.3 src/write-through.md — best-effort semantics; per-backend save-capability gating
  • 5.4 src/merge.md — first-wins vs last-wins examples; gap-filling behaviour
  • 5.5 src/observability.md — emitted metrics + units; fallthrough_total cardinality (= number of unique chain links observed)
  • 5.6 README.md ## Modules section links every .md

6. Tests (Vitest, runner-neutral imports per CLAUDE.md §11)

  • 6.1 src/backend.test.ts — port every test from packages/i18n/i18n/src/core/backend/chained/chained-backend.test.ts (495 lines); same it(...) titles for conformance traceability; cover fallthrough, propagation of BACKEND_READ_FAILED, write-through, readMulti merge strategies, save fan-out, lastModified first-defined, init/dispose lifecycle, capabilities union
  • 6.2 src/backend.spec-conformance.test.ts — port from monolith spec-conformance suite (102 lines); add new requirements (CHAINED_BACKEND_ALL_FAILED on empty backends, AbortSignal cancellation, fallthrough_total counter emission)
  • 6.3 src/factory.test.ts — array form returns ChainedBackend; options form returns ChainedBackend; default name === "chained"
  • 6.4 src/write-through.test.ts — writes only to backends with save; saves at indices 0..N-1; never writes to the success backend; failures swallowed (best-effort)
  • 6.5 src/merge.test.ts — first-wins picks earlier; last-wins picks later; gaps filled from later
  • 6.6 src/index.test.ts — every documented public export resolves through ., ./backend, ./factory, ./write-through, ./merge
  • 6.7 Add an AbortSignal cancellation test — pre-aborted signal causes read to throw I18nError(I18N.BACKEND_TIMEOUT); signal threaded into child reads (verify via stub backend that records its received signal)
  • 6.8 Add a CHAINED_BACKEND_ALL_FAILED test — bypass schema validation via as unknown as ChainedBackendOptions cast and pass backends: []; runtime throw on first read carries the new code
  • 6.9 Every it(...) carries { timeout: 5000 }
  • 6.10 All test imports go through @suaveplan/testing/runner (CLAUDE.md §11) — verified by grep -rnE "from ['\"](vitest|bun:test)['\"]" packages/i18n/i18n-backend-chained/src/ returning zero hits
  • 6.11 No // biome-ignore suppressions (CLAUDE.md §9)
  • 6.12 No export * in any non-root barrel (CLAUDE.md §10)

7. Pre-archive verification (5-gate triple)

  • 7.1 bun run check — Biome 0/0/0 across the monorepo
  • 7.2 bunx turbo lint typecheck test --filter=@suaveplan/i18n-backend-chained — green
  • 7.3 bunx turbo test:coverage --filter=@suaveplan/i18n-backend-chained — 100/100/100/100
  • 7.4 bun scripts/openspec-conformance.ts @suaveplan/i18n-backend-chained — exit 0
  • 7.5 bunx turbo build --filter=@suaveplan/i18n-backend-chained — green

8. Pre-archive 7-row checklist (AGENTS.md Section 13)

  • 8.1 Generics threaded — Backend[] correctly typed; MergeStrategy discriminator narrowed exhaustively
  • 8.2 Zod schemas wired at the public boundary — ChainedBackendOptionsSchema parses constructor input AND rejects empty backends array with INVALID_OPTIONS
  • 8.3 Every error code thrown by at least one production path: INVALID_OPTIONS (constructor), BACKEND_LOCALE_NOT_AVAILABLE (all children miss with non-empty chain), CHAINED_BACKEND_ALL_FAILED (defence-in-depth empty-chain throw), BACKEND_READ_FAILED (propagation from child), BACKEND_WRITE_FAILED (save fan-out failure), BACKEND_TIMEOUT (signal aborted)
  • 8.4 Strict-mode tested — exhaustive switch on MergeStrategy enforced via expectTypeOf
  • 8.5 OTel histograms emitted from production paths: i18n.backend.chained.read_duration_ms, i18n.backend.chained.write_duration_ms; counters read_total, read_errors_total, write_total, fallthrough_total{from_backend,to_backend}
  • 8.6 Lifecycle dedup — init is idempotent (re-calling forwards to children but does not duplicate state); dispose is idempotent and reverse-order
  • 8.7 No retroactive tick — every box reflects work done in this change

9. Manifest + ecosystem awareness

  • 9.1 Add @suaveplan/i18n-backend-chained entry to root CLAUDE.md package listing
  • 9.2 Run bun scripts/folder-reorg/generate-manifest.ts to refresh MANIFEST.md
  • 9.3 Append @suaveplan/i18n-backend-chained to STATUS.md Completed list

10. Archive

  • 10.1 Fold spec delta into openspec/specs/i18n-backend-chained/spec.md (NEW capability spec — first delta becomes canonical)
  • 10.2 Move openspec/changes/i18n-backend-chained/ → openspec/archive/<yyyy-mm-dd>-i18n-backend-chained/