# Tasks: enterprise-currency **Change ID**: enterprise-currency **Package**: `@suaveplan/currency` **Target path**: `finance/currency/currency` **Prerequisites on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/retry`, `@suaveplan/logger` --- ## 0. Pre-flight (non-negotiable) - [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each) - [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against. - [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest. ## 1. Scaffold - [ ] Create directory tree per `proposal.md`. - [ ] `package.json` at `0.1.0`, `"sideEffects": false`, exports `"."` + engine + provider subpaths. - [ ] Deps: workspace + catalog as listed. - [ ] DevDeps: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `fast-check catalog:testing`, `@suaveplan/testing workspace:^`, `@suaveplan/money workspace:^`. - [ ] `tsconfig.json` extending `library.json`; `vitest.config.ts` with `createTestConfig()`. - [ ] Scripts: `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check`. ## 2. Types and errors - [ ] Define all interfaces and Zod schemas (`ConverterConfigSchema`, `ConvertOptionsSchema`) in `src/types/currency.types.ts`. - [ ] Write `currency.types.md` (≥200 words, both examples, API reference). - [ ] `src/error-codes.ts`: five codes; `registerCurrencyErrorCodes()`; eager call. ## 3. Rate cache - [ ] Implement `RateCache` in `src/engine/rate-cache/rate-cache.ts`: - `get(from, to)` / `set(from, to, rate)` / `prune()` / `invalidate(from?, to?)`. - TTL check on `get`; prune on set when cache exceeds 1000 entries. - OTel counter `currency.cache_hits_total` / `currency.cache_misses_total`. - [ ] Write `rate-cache.test.ts`: TTL expiry, prune, invalidate, hit/miss tracking. - [ ] Write `rate-cache.md` (≥200 words). ## 4. Triangulation - [ ] Implement BFS triangulation in `src/engine/triangulate/triangulate.ts`: - `findPath(cache, from, to): string[] | null` — BFS over cached pairs to find shortest path; max 3 hops. - `triangulateRate(cache, path): number` — multiply rates along path. - [ ] Write `triangulate.test.ts`: direct found; one-hop; two-hop; no path returns null; cycle avoidance. - [ ] Write `triangulate.md` (≥200 words). ## 5. Circuit breaker - [ ] Implement per-provider `ProviderCircuitBreaker` in `src/engine/circuit-breaker/circuit-breaker.ts`: - States: closed → open (after N consecutive failures) → half-open (after cooldown) → closed. - `wrap(fn): Promise` — skips call when open; re-tries in half-open. - Uses `@suaveplan/retry` for backoff calculation. - [ ] Write `circuit-breaker.test.ts`: consecutive failures open circuit; cooldown half-open; success resets. - [ ] Write `circuit-breaker.md` (≥200 words). ## 6. Providers - [ ] Implement `InMemoryProvider` and `createStaticProvider` in respective feature dirs. - [ ] Write unit tests + `.md` for each. ## 7. CurrencyConverter - [ ] Implement `CurrencyConverter` in `src/engine/converter/converter.ts`: - Constructor validates config via `ConverterConfigSchema`; calls `registerCurrencyErrorCodes()` eagerly. - `convert(from, to, amount, options?)`: 1. Check cache; if hit, return without provider call. 2. For each provider (skip open circuit): call `fetchRates({ symbols: [to], base: from })`; on success cache result. 3. If no direct rate found: BFS triangulate from cache. 4. If still not found: throw `CURRENCY_TRIANGULATION_IMPOSSIBLE`. 5. Honour `AbortSignal` between each provider attempt. 6. Return `ConversionResult` with `providerName`, `triangulatedThrough`. 7. OTel span `currency.convert`; counter `currency.converts_total` with `{direct, triangulated}`. - `convertBatch(requests)`: group by provider calls, execute, map results. - `setVirtualRate(symbol, rate)`: update cache; emit `currency:rate:updated` event via `@suaveplan/events`. - `invalidateCache(from?, to?)`: delegate to `RateCache`. - [ ] Write `converter.types.ts`. - [ ] Write `converter.test.ts` (≤500 lines): direct convert; cache hit; triangulation; AbortSignal; circuit breaker open; virtual rate; batch; all providers fail throws. - [ ] Write `converter.md` (≥200 words, simple + advanced, API reference). ## 8. Integration + property tests - [ ] `tests/integration.test.ts`: three-provider fallback chain; circuit breaker in play; virtual rate update. - [ ] `tests/property.test.ts` (fast-check): `convert(from, to)` then `convert(to, from)` yields approximately inverse; `convertBatch` results equal individual converts. ## 9. Docs + verify + publish - [ ] All `.md` per layout; root `README.md` with quick-start, provider guide, BFS triangulation explanation, API table. - [ ] `bunx turbo lint --filter=@suaveplan/currency` — zero errors. - [ ] `bunx turbo typecheck --filter=@suaveplan/currency` — zero errors. - [ ] `bunx turbo test --filter=@suaveplan/currency` — all pass. - [ ] `bunx turbo test:coverage --filter=@suaveplan/currency` — 100%. - [ ] `bun scripts/publish.ts --package @suaveplan/currency`. - [ ] Update `CLAUDE.md`; archive to `openspec/archive/2026-04-24-enterprise-currency/`.