Initial Commit
This commit is contained in:
175
openspec/changes/enterprise-currency/proposal.md
Normal file
175
openspec/changes/enterprise-currency/proposal.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# OpenSpec Change: enterprise-currency
|
||||
|
||||
- **Change ID**: enterprise-currency
|
||||
- **Package**: `@suaveplan/currency`
|
||||
- **Category**: finance
|
||||
- **Target path**: `finance/currency/currency`
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-24
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/currency.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/currency` on the genesis branch from scratch at enterprise quality. The package is a currency conversion engine with pluggable `CurrencyProvider` interface, TTL-based `RateCache`, automatic BFS cross-rate triangulation (shortest-path over the cached-rate graph), inverse-rate calculation, per-provider circuit breaker, `AbortSignal` cancellation, batch convert, and a `setVirtualRate` live-update API. Returns typed `ConversionResult` with provenance (`providerName`, `triangulatedThrough`). Ships two reference providers: `InMemoryProvider` and `createStaticProvider`.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
Does not exist on genesis — fresh build.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/error` (`workspace:^`) | Typed `CURRENCY.*` error codes |
|
||||
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `EventBusLike` |
|
||||
| `@suaveplan/events` (`workspace:^`) | Virtual-rate change events |
|
||||
| `@suaveplan/retry` (`workspace:^`) | Circuit-breaker + backoff |
|
||||
| `@suaveplan/logger` (`workspace:^`) | Structured logs |
|
||||
| `zod` (`catalog:forms`) | Options validation |
|
||||
| `@opentelemetry/api` (`catalog:observability`) | Spans + counters |
|
||||
| `tslib` (`catalog:build`) | Runtime helpers |
|
||||
|
||||
Dev dep: `@suaveplan/money workspace:^` (for `MoneyLike` type in tests).
|
||||
|
||||
---
|
||||
|
||||
## Public API surface (from main branch enterprise spec)
|
||||
|
||||
```ts
|
||||
class CurrencyConverter {
|
||||
constructor(config: ConverterConfig)
|
||||
convert(from: string, to: string, amount: MoneyLike | number, options?: ConvertOptions): Promise<ConversionResult>
|
||||
convertAmount(from: string, to: string, amount: number, options?: ConvertOptions): Promise<ConversionResult>
|
||||
convertBatch(requests: Array<{from: string; to: string; amount: number}>): Promise<ConversionResult[]>
|
||||
setVirtualRate(symbol: string, rate: number): void
|
||||
invalidateCache(from?: string, to?: string): void
|
||||
}
|
||||
|
||||
class RateCache {
|
||||
get(from: string, to: string): ExchangeRate | undefined
|
||||
set(from: string, to: string, rate: ExchangeRate): void
|
||||
prune(): void
|
||||
invalidate(from?: string, to?: string): void
|
||||
}
|
||||
|
||||
function createStaticProvider(rates: Record<string, Record<string, number>>): CurrencyProvider
|
||||
class InMemoryProvider implements CurrencyProvider { setRates(rates: ExchangeRate[]): void }
|
||||
|
||||
interface ConversionResult {
|
||||
amount: number
|
||||
rate: number
|
||||
inverseRate: number
|
||||
rateAgeMs: number
|
||||
providerName: string
|
||||
triangulatedThrough: string[] | null
|
||||
}
|
||||
|
||||
interface ConverterConfig {
|
||||
providers: CurrencyProvider[]
|
||||
defaultBase?: string
|
||||
cacheTtlMs?: number
|
||||
maxRetries?: number
|
||||
logger?: LoggerLike
|
||||
}
|
||||
|
||||
interface CurrencyProvider {
|
||||
name: string
|
||||
supportsFiat: boolean
|
||||
supportsCrypto: boolean
|
||||
fetchRates(options: RateFetchOptions): Promise<RateFetchResult>
|
||||
isHealthy?(): boolean
|
||||
}
|
||||
```
|
||||
|
||||
### Error codes
|
||||
|
||||
```ts
|
||||
type CurrencyErrorCode =
|
||||
| 'CURRENCY_PROVIDER_UNAVAILABLE'
|
||||
| 'CURRENCY_UNSUPPORTED_PAIR'
|
||||
| 'CURRENCY_TRIANGULATION_IMPOSSIBLE'
|
||||
| 'CURRENCY_CONVERT_ABORTED'
|
||||
| 'CURRENCY_INVALID_CONFIG'
|
||||
|
||||
function registerCurrencyErrorCodes(): void
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## New additions for genesis
|
||||
|
||||
- **BFS triangulation** in `src/engine/triangulate.ts`
|
||||
- **Circuit breaker** per provider in `src/engine/circuit-breaker.ts` using `@suaveplan/retry`
|
||||
- **`convertBatch`** — coalesces required rate fetches
|
||||
- **`setVirtualRate` + change events** via `@suaveplan/events`
|
||||
- **`AbortSignal` on `convert`**
|
||||
- **Zod schemas** for `ConverterConfig`, `ConvertOptions`
|
||||
- **OTel** spans per convert, cache-hit counter, triangulation-path histogram
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
finance/currency/currency/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts
|
||||
error-codes.ts
|
||||
constants/index.ts
|
||||
types/
|
||||
currency.types.ts
|
||||
currency.types.md
|
||||
engine/
|
||||
converter/
|
||||
converter.ts
|
||||
converter.md
|
||||
converter.types.ts
|
||||
converter.test.ts
|
||||
index.ts
|
||||
rate-cache/
|
||||
rate-cache.ts
|
||||
rate-cache.md
|
||||
rate-cache.test.ts
|
||||
index.ts
|
||||
triangulate/
|
||||
triangulate.ts
|
||||
triangulate.md
|
||||
triangulate.test.ts
|
||||
index.ts
|
||||
circuit-breaker/
|
||||
circuit-breaker.ts
|
||||
circuit-breaker.md
|
||||
circuit-breaker.test.ts
|
||||
index.ts
|
||||
provider/
|
||||
in-memory-provider/
|
||||
in-memory-provider.ts
|
||||
in-memory-provider.md
|
||||
in-memory-provider.test.ts
|
||||
index.ts
|
||||
static-provider/
|
||||
static-provider.ts
|
||||
static-provider.md
|
||||
static-provider.test.ts
|
||||
index.ts
|
||||
tests/
|
||||
integration.test.ts
|
||||
property.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
Downstream: `@suaveplan/currency-providers` (implements `CurrencyProvider`), `@suaveplan/money` (integration for `MoneyLike`).
|
||||
|
||||
**Prerequisites on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/retry`, `@suaveplan/logger`.
|
||||
94
openspec/changes/enterprise-currency/tasks.md
Normal file
94
openspec/changes/enterprise-currency/tasks.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# 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<T>` — 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/`.
|
||||
Reference in New Issue
Block a user