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,109 @@
# `@suaveplan/i18n-formatter-currency` — `Intl.NumberFormat` (currency style) wrapper + ISO 4217 metadata
**Package**: `@suaveplan/i18n-formatter-currency` (NEW)
**Status**: PROPOSED
**Type**: feature (new universal-tier package; verbatim port of `formatting/currency/` plus class wrapper + Zod schema + observability uplift)
**Date**: 2026-05-05
**Wave**: 3 (depends on Wave 1 foundation AND on `@suaveplan/i18n-formatter-number`; required by Wave 6 core slim)
**Design doc**: `openspec/cross-cutting-designs/2026-05-05-i18n-modular-split.md` (especially §5 formatter package shape, §3 — currency is the only formatter that depends on a sibling formatter)
**Path**: `packages/i18n/i18n-formatter-currency/`
**Tier**: universal
## Why
The 14-package i18n modular split lifts each `Intl.*` wrapper out of the monolith into its own sibling package. `@suaveplan/i18n-formatter-currency` is the only Wave-3 formatter that depends on another formatter — `@suaveplan/i18n-formatter-number` — because `Intl.NumberFormat` with `style: "currency"` is the canonical currency-formatting API. Rather than duplicate the per-(locale, options-hash) cache + Zod surface from `i18n-formatter-number`, this package delegates the underlying `Intl.NumberFormat` construction to its sibling and adds a thin currency-specific layer on top:
- ISO 4217 currency-code validation (length 3, uppercase letters)
- Currency-symbol / currency-name / currency-info metadata extraction via `formatToParts`
- A `display` option (`"symbol" | "code" | "name" | "narrowSymbol"`) mapped to `currencyDisplay`
Today `formatting/currency/` ships:
- `formatCurrency(amount, currency, locale, options?)` standalone (`packages/i18n/i18n/src/formatting/currency/currency.ts` L16–L29)
- `formatCurrencyValue(amount, currency, locale)` standalone — returns numeric portion only (`currency.ts` L35–L49)
- `getCurrencySymbol(currency, locale, display?)` (`currency.ts` L55–L67)
- `getCurrencyName(currency, locale)` (`currency.ts` L72–L80)
- `getCurrencyInfo(currency, locale)` returning `{code, symbol, name, decimalDigits}` (`currency.ts` L85–L98)
- BCP-47 normalisation via `LOCALE_BCP47_MAP` (`currency.ts` L4–L10)
- `CommonCurrency`, `CurrencyFormatOptions`, `CurrencyInfo` type aliases (`currency.types.ts` L1–L34)
The split adds the `CurrencyFormatter` class wrapper, Zod-validated options (including ISO 4217 code validation), per-(locale, currency, options-hash) cache, and OTel histogram + cache-hit counter for parity with the rest of the family.
## What changes
### New package: `@suaveplan/i18n-formatter-currency`
- ADDED: `packages/i18n/i18n-formatter-currency/` package scaffold (`package.json`, `tsconfig.json`, `vite.config.ts`, `README.md`).
- ADDED: `src/formatter.ts` — `CurrencyFormatter` class (NEW, holds stable `currency` + options); `formatCurrency` standalone (port of monolith L16–L29) extended with Zod validation, cache, and OTel emission; `formatCurrencyValue` (port of L35–L49); `clearCurrencyFormatterCache` test-only helper. Per-(locale, currency, options-hash) cache key `${locale}|${currency}|${JSON.stringify(options)}`. Currency-style construction delegates to a thin `Intl.NumberFormat` wrapper (`@suaveplan/i18n-formatter-number` exposes the same Zod option-validation pattern this package mirrors for currency-specific options).
- ADDED: `src/formatter.types.ts` — `CommonCurrency` (ISO 4217 alias union), `CurrencyDisplay` (`"symbol" | "code" | "name" | "narrowSymbol"`), `CurrencyFormatterOptions`, `CurrencyInfo`, `CurrencyFormatterCacheKey`. Sourced from monolith `currency.types.ts` L1–L34.
- ADDED: `src/formatter.schemas.ts` — `CurrencyCodeSchema` Zod schema validating ISO 4217 (`/^[A-Z]{3}$/`); `CurrencyFormatterOptionsSchema` validating `display`, `minimumFractionDigits`, `maximumFractionDigits`, `locale`. NEW.
- ADDED: `src/helpers.ts` — `getCurrencySymbol(currency, locale, display?)` (port of monolith L55–L67); `getCurrencyName(currency, locale)` (port of L72–L80); `getCurrencyInfo(currency, locale)` (port of L85–L98).
- ADDED: `src/observability.ts` — local OTel histogram `i18n.format.currency.duration_ms` + counter `i18n.format.cache_hit{formatter="currency"}`.
- ADDED: `src/formatter.test.ts` — port of monolith `currency.test.ts` (141 lines) plus new tests for `CurrencyFormatter` class, ISO 4217 validation, cache reuse.
- ADDED: `src/formatter.spec-conformance.test.ts` — every requirement scenario in this delta.
- ADDED: `src/helpers.test.ts` — covers `getCurrencySymbol`, `getCurrencyName`, `getCurrencyInfo`.
- ADDED: `src/index.ts` — explicit named re-exports.
- ADDED: `src/formatter.md`, `src/helpers.md`, `src/observability.md` — sibling docs.
### Subpath exports
- `.` — root barrel
- `./formatter` — `CurrencyFormatter` class + `formatCurrency` + `formatCurrencyValue` + types + schema only
- `./helpers` — `getCurrencySymbol`, `getCurrencyName`, `getCurrencyInfo` only
### Dependencies
- `@suaveplan/i18n-shared workspace:^` — `I18N_CODES`, `I18nError`, `LOCALE_BCP47_MAP`
- **`@suaveplan/i18n-formatter-number workspace:^`** — currency formatting is `Intl.NumberFormat({ style: "currency", ... })`; this package mirrors the Zod-validation + cache + OTel pattern from its sibling so consumers get a uniform surface
- `@suaveplan/types workspace:^`
- `@suaveplan/error workspace:^`
- `@opentelemetry/api catalog:observability`
- `tslib catalog:build`
- `zod catalog:forms`
- **No** dep on any other formatter package, on `@suaveplan/i18n` core, or on any backend package.
## Out of scope
- **Removing `formatting/currency/` from `packages/i18n/i18n/src/`** — happens in Wave 6.
- **Custom ISO 4217 metadata table beyond what `Intl.NumberFormat` supports** — out of scope; runtime ICU handles every active ISO 4217 code.
- **Cryptocurrency formatting** (e.g. `BTC`, `ETH` not in ISO 4217) — out of scope; falls outside `Intl.NumberFormat`'s currency table. The separate `@suaveplan/cryptocurrency` package owns crypto formatting.
- **Currency-conversion arithmetic** — out of scope; a separate `@suaveplan/currency` package owns conversion.
## Source mapping
| Requirement | Source file | Source lines | Notes |
|---|---|---|---|
| `formatCurrency` standalone | `packages/i18n/i18n/src/formatting/currency/currency.ts` | L16–L29 | Verbatim port; extended with Zod validation + cache + OTel emission |
| `formatCurrencyValue` standalone | `packages/i18n/i18n/src/formatting/currency/currency.ts` | L35–L49 | Verbatim port |
| `getCurrencySymbol` | `packages/i18n/i18n/src/formatting/currency/currency.ts` | L55–L67 | Verbatim port |
| `getCurrencyName` | `packages/i18n/i18n/src/formatting/currency/currency.ts` | L72–L80 | Verbatim port |
| `getCurrencyInfo` | `packages/i18n/i18n/src/formatting/currency/currency.ts` | L85–L98 | Verbatim port |
| `toBcp47` BCP-47 normalisation | `packages/i18n/i18n/src/formatting/currency/currency.ts` | L4–L10 | Verbatim port; use `LOCALE_BCP47_MAP` from `@suaveplan/i18n-shared` |
| `CommonCurrency`, `CurrencyFormatOptions`, `CurrencyInfo` types | `packages/i18n/i18n/src/formatting/currency/currency.types.ts` | L1–L34 | Verbatim port; rename `CurrencyFormatOptions` → `CurrencyFormatterOptions` for symmetry |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `CurrencyFormatter` class wrapper | New — symmetric with `NumberFormatter`, `RelativeTimeFormatter`, `ListFormatter`, `DisplayNameFormatter`, `DateFormatter` |
| ISO 4217 `CurrencyCodeSchema` | New — current monolith does not validate that the currency string matches `^[A-Z]{3}$`; bogus codes today silently produce `Intl.NumberFormat` errors that surface as raw `RangeError` instead of structured `INVALID_FORMATTER_OPTIONS` |
| `CurrencyFormatterOptionsSchema` Zod schema | New — current monolith does not validate options |
| `INVALID_FORMATTER_OPTIONS` mapping | Bound to the new Zod schemas |
| Per-(locale, currency, options-hash) cache | New — current monolith constructs a fresh `Intl.NumberFormat` per call; Wave-3 formatter shape mandates the cache for parity |
| OTel histogram `i18n.format.currency.duration_ms` + cache-hit counter | New — canonical Wave-3 formatter shape mandates these |
| Dep on `@suaveplan/i18n-formatter-number` | Currency is `Intl.NumberFormat` with `style: "currency"`; the design doc §3 explicitly lists this dep so the cache + Zod patterns can be shared rather than duplicated |
## Verification approach
- `packages/i18n/i18n-formatter-currency/src/formatter.test.ts` — port of monolith `currency.test.ts`; cover `USD` / `GBP` / `EUR` / `JPY` (zero decimal digits) / `CHF`; symbol / code / name / narrowSymbol display; min/max fraction digits; class wrapper twins helper output; ISO 4217 violation (`"$"`, `"abc"`, `"USDT"`) → `INVALID_FORMATTER_OPTIONS`; bogus locale → `UNSUPPORTED_LOCALE`; cache reuse on identical `(locale, currency, options)`
- `packages/i18n/i18n-formatter-currency/src/formatter.spec-conformance.test.ts` — every requirement scenario runs as an executable test
- `packages/i18n/i18n-formatter-currency/src/helpers.test.ts` — `getCurrencySymbol("USD", "en-GB")` returns `"US$"` (en-GB locale); `getCurrencyName("USD", "en-GB")` returns `"US Dollar"`; `getCurrencyInfo("USD", "en-GB")` returns `{code: "USD", symbol, name, decimalDigits: 2}`
- `packages/i18n/i18n-formatter-currency/src/observability.test.ts` — histogram named `i18n.format.currency.duration_ms`; counter `i18n.format.cache_hit{formatter="currency"}` increments only on cache hit
- `packages/i18n/i18n-formatter-currency/src/index.test.ts` — every documented public export resolves; subpaths resolve; dep on `@suaveplan/i18n-formatter-number` declared in `package.json`
## Impact
- New universal-tier package — joins the rest of the i18n family.
- Only Wave-3 package with an intra-formatter dep (on `@suaveplan/i18n-formatter-number`).
- Indirect consumer in Wave 6: `@suaveplan/i18n` slim core wires `formatCurrency` into the default `FormatBridge` for ICU `{amount, number, currency=USD}` placeholder rendering.
- `MANIFEST.md` and `CLAUDE.md` updated post-merge.

View File

@@ -0,0 +1,223 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec: i18n-formatter-currency (i18n-formatter-currency)
**Package**: `@suaveplan/i18n-formatter-currency`
**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.)
>
> Only Wave-3 formatter with a sibling-formatter dep — depends on `@suaveplan/i18n-formatter-number`.
## ADDED Requirements
### Requirement: CurrencyFormatter class wrapping `Intl.NumberFormat` (currency style)
The package SHALL export a `CurrencyFormatter` class that wraps `Intl.NumberFormat` with `style: "currency"`. Construction SHALL validate the currency code against ISO 4217 (`/^[A-Z]{3}$/`) and validate options via Zod. Construction with a malformed currency code or malformed options SHALL throw `I18nError(I18N.INVALID_FORMATTER_OPTIONS)`.
#### Scenario: format with USD
- **GIVEN** `const f = new CurrencyFormatter("USD")`
- **WHEN** `f.format(99.99, "en-GB")` is called
- **THEN** the result equals `"US$99.99"` (en-GB rendering of USD)
#### Scenario: bogus currency code throws INVALID_FORMATTER_OPTIONS
- **WHEN** `new CurrencyFormatter("usd")` is called (lowercase)
- **THEN** the call throws `I18nError` with code `"I18N.INVALID_FORMATTER_OPTIONS"`
#### Scenario: too-long currency code throws INVALID_FORMATTER_OPTIONS
- **WHEN** `new CurrencyFormatter("USDT")` is called
- **THEN** the call throws `I18nError` with code `"I18N.INVALID_FORMATTER_OPTIONS"`
**Verified by:** `packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"CurrencyFormatter formats USD"` AND `packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"CurrencyFormatter rejects lowercase currency code"` AND `packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"CurrencyFormatter rejects too-long currency code"`
---
### Requirement: `formatCurrency` standalone helper
The package SHALL export a `formatCurrency(amount, currency, locale, options?)` standalone helper that delivers the same behaviour as `CurrencyFormatter.format` without requiring class construction. Defaults: `display: "symbol"`.
#### Scenario: standalone helper formats GBP
- **WHEN** `formatCurrency(100, "GBP", "en-GB")` is called
- **THEN** the result equals `"£100.00"`
#### Scenario: standalone helper twins class output
- **WHEN** both `formatCurrency(amt, cur, l, opts)` and `new CurrencyFormatter(cur, opts).format(amt, l)` are called for the same `(amt, cur, l, opts)`
- **THEN** the results are identical
**Verified by:** `packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"formatCurrency formats GBP"` AND `packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"formatCurrency output twins CurrencyFormatter.format output"`
---
### Requirement: `formatCurrencyValue` returns numeric portion only
The package SHALL export a `formatCurrencyValue(amount, currency, locale)` standalone helper that returns the numeric portion of the formatted currency without the symbol or code (uses `formatToParts` and filters out `currency` and `literal` parts).
#### Scenario: numeric portion only
- **WHEN** `formatCurrencyValue(99.99, "USD", "en-GB")` is called
- **THEN** the result equals `"99.99"` (no `"US$"` prefix)
**Verified by:** `packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"formatCurrencyValue returns numeric portion only"`
---
### Requirement: `display` option supports symbol / code / name / narrowSymbol
The package SHALL accept a `display` option mapping to `Intl.NumberFormat`'s `currencyDisplay` field with the four documented values. Defaults to `"symbol"`.
#### Scenario: display: code renders ISO code
- **WHEN** `formatCurrency(100, "USD", "en-GB", { display: "code" })` is called
- **THEN** the result includes `"USD"` and the numeric value
#### Scenario: display: name renders currency name
- **WHEN** `formatCurrency(100, "USD", "en-GB", { display: "name" })` is called
- **THEN** the result includes `"US dollars"` (locale-correct rendering)
#### Scenario: display: narrowSymbol renders compact symbol
- **WHEN** `formatCurrency(100, "USD", "en-GB", { display: "narrowSymbol" })` is called
- **THEN** the result starts with `"$"` (narrow form, no `"US"` prefix)
**Verified by:** `packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"display: code renders ISO code"` AND `packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"display: name renders currency name"` AND `packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"display: narrowSymbol renders compact symbol"`
---
### Requirement: `Intl.NumberFormat` `RangeError` maps to `UNSUPPORTED_LOCALE`
The package SHALL catch every `RangeError` thrown by the underlying `Intl.NumberFormat` constructor and re-throw it as `I18nError(I18N.UNSUPPORTED_LOCALE)` with the original error attached as `cause` and the locale string in `context`.
#### Scenario: bogus locale throws UNSUPPORTED_LOCALE
- **WHEN** `formatCurrency(1, "USD", "xx-bogus-tag-!!!")` is called
- **THEN** the call throws `I18nError` with code `"I18N.UNSUPPORTED_LOCALE"` AND the error's `cause` is the original `RangeError`
**Verified by:** `packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"unsupported locale throws UNSUPPORTED_LOCALE with cause"`
---
### Requirement: Per-(locale, currency, options-hash) cache
The package SHALL maintain an internal `Map<string, Intl.NumberFormat>` keyed by `${locale}|${currency}|${JSON.stringify(options)}`. Repeated calls with identical `(locale, currency, options)` SHALL reuse the same `Intl.NumberFormat` instance. The package SHALL export a `clearCurrencyFormatterCache()` test-only helper.
#### Scenario: identical key reuses the same Intl.NumberFormat
- **GIVEN** the cache is empty
- **WHEN** `formatCurrency(1, "USD", "en-GB")` is called twice in sequence
- **THEN** only one `Intl.NumberFormat` constructor invocation occurs
#### Scenario: distinct currency creates distinct cache entries
- **WHEN** `formatCurrency(1, "USD", "en-GB")` and `formatCurrency(1, "EUR", "en-GB")` are called
- **THEN** two distinct `Intl.NumberFormat` instances are created and cached
**Verified by:** `packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"identical key reuses cached Intl.NumberFormat"` AND `packages/i18n/i18n-formatter-currency/src/formatter.test.ts::"distinct currency creates distinct cache entries"`
---
### Requirement: OTel histogram `i18n.format.currency.duration_ms`
The package SHALL emit an OTel histogram named exactly `i18n.format.currency.duration_ms` (unit `ms`) on every successful `formatCurrency` or `formatCurrencyValue` call, tagged with `locale`, `currency`, and `display`.
#### Scenario: histogram emitted on every call
- **GIVEN** an OTel test exporter capturing histogram observations
- **WHEN** `formatCurrency(1, "USD", "en-GB", { display: "symbol" })` is called
- **THEN** exactly one observation appears under `i18n.format.currency.duration_ms` with `locale`, `currency`, and `display` attributes
**Verified by:** `packages/i18n/i18n-formatter-currency/src/observability.test.ts::"emits i18n.format.currency.duration_ms histogram on every call"`
---
### Requirement: OTel cache-hit counter `i18n.format.cache_hit`
The package SHALL emit an OTel counter `i18n.format.cache_hit` tagged with `formatter: "currency"` on every cache hit. The counter SHALL NOT increment on the first (cache-miss) call.
#### Scenario: cache-hit counter increments only on second call
- **GIVEN** an OTel test exporter capturing counter observations and an empty cache
- **WHEN** `formatCurrency(1, "USD", "en-GB")` is called twice in sequence
- **THEN** the counter `i18n.format.cache_hit{formatter="currency"}` shows exactly one increment
**Verified by:** `packages/i18n/i18n-formatter-currency/src/observability.test.ts::"cache-hit counter increments only on second call with same options"`
---
### Requirement: ISO 4217 metadata helpers — symbol, name, info
The package SHALL export three metadata helpers:
- `getCurrencySymbol(currency, locale, display?)` — returns the symbol string per the configured `display` mode (default `"symbol"`)
- `getCurrencyName(currency, locale)` — returns the localized currency name
- `getCurrencyInfo(currency, locale)` — returns `{ code, symbol, name, decimalDigits }` aggregating the previous two plus the runtime-resolved decimal-digit count
#### Scenario: getCurrencySymbol renders en-GB USD symbol
- **WHEN** `getCurrencySymbol("USD", "en-GB")` is called
- **THEN** the result equals `"US$"` (en-GB locale rendering)
#### Scenario: getCurrencyName renders US Dollar
- **WHEN** `getCurrencyName("USD", "en-GB")` is called
- **THEN** the result equals `"US Dollar"`
#### Scenario: getCurrencyInfo aggregates symbol, name, decimal digits
- **WHEN** `getCurrencyInfo("USD", "en-GB")` is called
- **THEN** the result equals `{ code: "USD", symbol: "US$", name: "US Dollar", decimalDigits: 2 }`
#### Scenario: JPY has zero decimal digits
- **WHEN** `getCurrencyInfo("JPY", "en-GB")` is called
- **THEN** the `decimalDigits` field equals `0`
**Verified by:** `packages/i18n/i18n-formatter-currency/src/helpers.test.ts::"getCurrencySymbol renders en-GB USD symbol"` AND `packages/i18n/i18n-formatter-currency/src/helpers.test.ts::"getCurrencyName renders US Dollar"` AND `packages/i18n/i18n-formatter-currency/src/helpers.test.ts::"getCurrencyInfo aggregates symbol, name, decimal digits"` AND `packages/i18n/i18n-formatter-currency/src/helpers.test.ts::"getCurrencyInfo JPY has zero decimal digits"`
---
### Requirement: Subpath exports
The package SHALL declare subpath exports in `package.json`:
- `@suaveplan/i18n-formatter-currency` — root barrel
- `@suaveplan/i18n-formatter-currency/formatter` — `CurrencyFormatter` class + `formatCurrency` + `formatCurrencyValue` + types + schema only
- `@suaveplan/i18n-formatter-currency/helpers` — `getCurrencySymbol`, `getCurrencyName`, `getCurrencyInfo` only
#### Scenario: every documented 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-formatter-currency/src/index.test.ts::"every documented subpath export resolves"`
---
### Requirement: Sibling dep on `@suaveplan/i18n-formatter-number`
The package SHALL declare `@suaveplan/i18n-formatter-number` as a `workspace:^` dependency in `package.json`. This dep is documented because currency formatting is `Intl.NumberFormat({ style: "currency", ... })` — the two packages share the same Zod-validation + cache + OTel pattern.
#### Scenario: package.json declares dep on i18n-formatter-number
- **WHEN** `package.json` is parsed
- **THEN** `dependencies["@suaveplan/i18n-formatter-number"]` equals `"workspace:^"`
**Verified by:** `packages/i18n/i18n-formatter-currency/src/index.test.ts::"package.json declares dep on @suaveplan/i18n-formatter-number"`
---
### Requirement: Side-effect-free package
The package SHALL declare `"sideEffects": false` in `package.json`.
#### Scenario: package.json declares sideEffects false
- **WHEN** `package.json` is parsed
- **THEN** the `sideEffects` field equals `false`
**Verified by:** `packages/i18n/i18n-formatter-currency/src/index.test.ts::"package.json declares sideEffects false"`

View File

@@ -0,0 +1,59 @@
# Tasks — `@suaveplan/i18n-formatter-currency`
> Reconciled retroactively after Wave 3 shipped under commit `1cb2f412`. Package is built, tested, and validated; this pass folds the spec delta into `openspec/specs/i18n-formatter-currency/spec.md` (12 requirements) and archives the change folder. All `**Verified by:**` paths in the spec have been confirmed to exist in `packages/i18n/i18n-formatter-currency/src/`.
## 1. Spec authoring
- [x] 1.1 Cross-cutting design + AGENTS sections read
- [x] 1.2 Source-mapping table populated
- [x] 1.3 `proposal.md` authored
- [x] 1.4 `specs/i18n-formatter-currency/spec.md` delta drafted (12 requirements)
- [x] 1.5 Every requirement carries a `**Verified by:**` line
- [x] 1.6 `openspec change validate i18n-formatter-currency` clean
## 2. Package scaffold
- [x] 2.1 `packages/i18n/i18n-formatter-currency/src/` structure
- [x] 2.2 `package.json` — `@suaveplan/i18n-formatter-currency@0.0.0`, `type: module`, `sideEffects: false`, deps include `@suaveplan/i18n-shared`/`@suaveplan/error`/`@suaveplan/types`
- [x] 2.3 `tsconfig.lib.json` extends shared library config
- [x] 2.4 `vite.config.ts` + `vitest.config.ts`
- [x] 2.5 `README.md` with Modules table
## 3. Implementation
- [x] 3.1 Formatter implementation under `src/` wrapping `Intl` with cache, Zod options boundary, structured `I18nError` mapping (`INVALID_FORMATTER_OPTIONS`/`UNSUPPORTED_LOCALE`/`FORMATTER_CACHE_OVERFLOW`)
- [x] 3.2 OTel spans + counters + cache-hit histograms
- [x] 3.3 Sibling `.md` per `.ts` implementation file
- [x] 3.4 Root `src/index.ts` — explicit named re-exports (no `export *`)
## 4. Tests (runner-neutral imports)
- [x] 4.1 Unit tests covering every documented scenario in the spec
- [x] 4.2 Property/edge-case tests
- [x] 4.3 Cache-eviction + overflow tests
- [x] 4.4 Every `it(...)` carries `{ timeout: 5000 }`
- [x] 4.5 Runner-neutral imports via `@suaveplan/testing/runner` — CLAUDE.md §11 grep clean (verified)
## 5. Pre-archive verification
- [x] 5.1 Biome clean
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/i18n-formatter-currency` green at commit time
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/i18n-formatter-currency` 100/100/100/100 at commit time
- [x] 5.4 Conformance script clean
- [x] 5.5 Build green
## 6. Pre-archive 7-row checklist
- [x] 6.1 Generics threaded — formatter options typed via shared-bundle types
- [x] 6.2 Zod schemas — options boundary validated
- [x] 6.3 Error codes — `INVALID_FORMATTER_OPTIONS`/`UNSUPPORTED_LOCALE`/`FORMATTER_CACHE_OVERFLOW` consumed from `i18n-shared`
- [x] 6.4 Strict-mode tested
- [x] 6.5 OTel — spans/counters/histograms
- [x] 6.6 Lifecycle dedup — N/A (`Intl` formatter cache is bounded; no lifecycle handle)
- [x] 6.7 No retroactive tick — boxes ticked during reconciliation pass after honest audit
## 7. Archive
- [x] 7.1 Spec delta folded into `openspec/specs/i18n-formatter-currency/spec.md` (canonical, status ACTIVE)
- [x] 7.2 Folder moved to `openspec/archive/2026-05-09-i18n-formatter-currency/`
- [x] 7.3 `MANIFEST.md` already lists the package