Initial Commit
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# Design: @suaveplan/currency-provider-virtual
|
||||
|
||||
> Non-normative. The normative requirements live in
|
||||
> `./specs/currency-provider-virtual/spec.md`. The cross-cutting
|
||||
> foundation-with-sibling-plugins architecture is documented once in
|
||||
> `openspec/archive/2026-05-17-slim-currency-providers/design.md` — this
|
||||
> document covers only what is specific to the virtual sibling.
|
||||
|
||||
## Shape
|
||||
|
||||
```text
|
||||
@suaveplan/currency-provider-virtual
|
||||
└── src/
|
||||
├── index.ts # explicit named barrel
|
||||
├── error-codes/
|
||||
│ ├── error-codes.ts # re-exports UNKNOWN_CURRENCY helper
|
||||
│ └── error-codes.md
|
||||
└── provider/
|
||||
├── provider.ts # VirtualCurrencyProvider class
|
||||
├── provider.types.ts # rate-table & options types
|
||||
├── provider.test.ts # all behaviour incl. UNKNOWN_CURRENCY
|
||||
└── provider.md
|
||||
```
|
||||
|
||||
## Rate-table storage
|
||||
|
||||
Rates are stored as a nested `Map<from, Map<to, number>>`. Pair lookup is
|
||||
O(1); enumeration for `fetchRates` is O(n) over the inner map of the
|
||||
requested base. Storing the table as nested `Map`s rather than a flat
|
||||
`Map<"${from}->${to}", number>` keeps `listCurrencies` cheap (just the outer
|
||||
key set) and avoids string-concatenation collisions when currency codes
|
||||
contain hyphens (`USDC-GOLD`, `GAME-1-COIN`).
|
||||
|
||||
Insertion replaces an existing rate silently — `setRate` is upsert, mirroring
|
||||
the upstream behaviour. This is intentional: virtual currencies are
|
||||
operator-controlled and "the operator changed the rate" is the normal flow,
|
||||
not an error.
|
||||
|
||||
## Triangulation is deliberately absent
|
||||
|
||||
The upstream provider derived `rates[base]` for non-default bases by inverting
|
||||
the stored rate (`1 / baseVc.rate`) and computing cross-rates
|
||||
(`otherRate / baseVc.rate`). Genesis pushes that into `CurrencyConverter`:
|
||||
the provider returns the rates it has, the converter triangulates via the
|
||||
cache. This keeps two responsibilities separate and avoids the upstream's
|
||||
implicit assumption that every virtual currency converts through a single
|
||||
shared `EUR` base. Callers who want bidirectional rates pre-populate them
|
||||
explicitly — usually trivial in a game-economy seeder.
|
||||
|
||||
## Clock injection
|
||||
|
||||
`Clock = () => number` from `@suaveplan/types` (ms since epoch). The
|
||||
constructor accepts an optional `clock` (default `() => Date.now()`); every
|
||||
`fetchRates` result stamps `timestamp = clock()`. Tests pass a `FakeClock`
|
||||
adapter (`() => fakeClock.now()`) for deterministic timestamps without
|
||||
monkey-patching `Date`.
|
||||
|
||||
## Error handling
|
||||
|
||||
`fetchRates({ base })` consults the rate table for `base`. If `base` is not
|
||||
present, the provider throws
|
||||
`CurrencyProviderError(CURRENCY_PROVIDER.UNKNOWN_CURRENCY)` with
|
||||
`providerName: "virtual-currency"`. This is the only error path: `setRate`
|
||||
upserts, `removeRate` is idempotent (returns `false` for missing pairs),
|
||||
`clear` always succeeds. `UNKNOWN_CURRENCY` was added to the slim-core
|
||||
registry on the original 2026-05-17 land and is the canonical typed error
|
||||
for "asked about a currency the provider doesn't know."
|
||||
|
||||
## Universal-tier purity
|
||||
|
||||
- No `node:` / `bun:` / DOM imports anywhere in `src/`.
|
||||
- No `fetch`, no network, no `FetchLike` consumption — this is the entire
|
||||
point of the package.
|
||||
- `sideEffects: false` — constructing the provider never mutates global
|
||||
state. (Contrast with the upstream form which called
|
||||
`@suaveplan/money` `registerCurrency` from the constructor, a hidden side
|
||||
effect that broke tree-shaking.)
|
||||
@@ -0,0 +1,102 @@
|
||||
# Add @suaveplan/currency-provider-virtual
|
||||
|
||||
**Package**: `@suaveplan/currency-provider-virtual`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new package (sibling of `@suaveplan/currency-providers` slim core)
|
||||
|
||||
- **Design**: ./design.md
|
||||
- **Spec delta**: ./specs/currency-provider-virtual/spec.md
|
||||
|
||||
## Why
|
||||
|
||||
The 2026-05-17 `slim-currency-providers` refactor split the original 9-provider
|
||||
bundle into a slim core (`@suaveplan/currency-providers`) plus nine sibling
|
||||
`@suaveplan/currency-provider-<name>` packages. Of the nine, eight wrap remote
|
||||
HTTP APIs (ECB, Fixer, OXR, Binance, CoinGecko, CoinMarketCap, CryptoCompare,
|
||||
Kraken); the ninth — **virtual** — serves operator-defined virtual / game
|
||||
currencies whose rates are programmable in memory and never round-trip a
|
||||
network.
|
||||
|
||||
This change lands the virtual sibling. It is the only sibling that does NOT
|
||||
consume `httpRequest` / `FetchLike` from the slim core (it has no upstream to
|
||||
fetch) and that DOES set `supportsVirtual: true` on the
|
||||
`CurrencyProvider` contract.
|
||||
|
||||
## What changes
|
||||
|
||||
### ADDED
|
||||
|
||||
- New package `@suaveplan/currency-provider-virtual` at
|
||||
`packages/finance/currency-provider-virtual/` (universal-tier, `0.1.0`).
|
||||
- `VirtualCurrencyProvider` — implements `CurrencyProvider` from
|
||||
`@suaveplan/currency`. Constructor takes an initial rate table and an
|
||||
optional `Clock` (default `Date.now`) used for `fetchRates` timestamps. No
|
||||
HTTP, no API key, no `FetchLike`.
|
||||
- Programmable rate-table API: `setRate(from, to, rate)`,
|
||||
`removeRate(from, to): boolean`, `clear()`, plus the read helpers
|
||||
`hasRate(from, to)`, `getRate(from, to)`, `listCurrencies(): readonly string[]`.
|
||||
- `fetchRates(options?)` — returns every rate denominated against the
|
||||
requested `base` (default the provider's `defaultBase`), filtered by the
|
||||
optional `targets` set, with `timestamp` derived from the injected `Clock`.
|
||||
- Raises `CurrencyProviderError(UNKNOWN_CURRENCY)` (registered in
|
||||
`@suaveplan/currency-providers`) when `fetchRates` is asked for a base that
|
||||
is not present in the rate table — there is no triangulation, the registry
|
||||
can be empty.
|
||||
|
||||
### KEPT (inherited)
|
||||
|
||||
- `CurrencyProvider` contract from `@suaveplan/currency` (unchanged).
|
||||
- `CurrencyProviderError` + `CURRENCY_PROVIDER_CODES.UNKNOWN_CURRENCY` from
|
||||
`@suaveplan/currency-providers` (already present in the 8-code slim-core
|
||||
registry).
|
||||
- Universal-tier purity (zero browser / Node APIs in `src/`), explicit named
|
||||
barrels, `@suaveplan/testing/runner` imports, 100/100/100/100 istanbul
|
||||
coverage.
|
||||
|
||||
## Source mapping
|
||||
|
||||
The upstream reference shape lived in
|
||||
`../suaveplan/packages/finance/currency-providers/src/virtual/virtual.ts`
|
||||
(pre-slim, now removed). That upstream shape coupled the provider to
|
||||
`@suaveplan/money` (`registerCurrency`) and stored rates as a record of
|
||||
`VirtualCurrencyConfig { rate, name, symbol, exponent?, issuerId? }` keyed by
|
||||
currency code, assuming a single shared base.
|
||||
|
||||
| Capability | Upstream | Migrated | Notes |
|
||||
|---|---|---|---|
|
||||
| Programmable per-currency rate table | ✅ | ✅ | Surface generalised — `setRate(from, to, rate)` lets callers store arbitrary directed pairs (game internal rates, partial graphs) rather than only "one base, many targets". |
|
||||
| `fetchRates({ base, targets })` | ✅ | ✅ | |
|
||||
| `supportsVirtual: true` flag | ✅ | ✅ | |
|
||||
| Triangulation from any virtual base | ✅ | 🚫 | Scoped out: the slim-core / `CurrencyConverter` already handles triangulation via the rate cache; duplicating it here would split the algorithm across two packages. Callers that need triangulated rates either pre-populate both directions with `setRate` or wire the provider through `CurrencyConverter` which triangulates from the cached `RateFetchResult`. |
|
||||
| Auto-registering currencies via `@suaveplan/money` `registerCurrency` | ✅ | 🚫 | Scoped out: the slim refactor explicitly dropped the `@suaveplan/money` dep so the slim core stays universal and zero-side-effect. Callers that want money-registry integration call `registerCurrency` themselves before constructing the provider — keeps this package side-effect-free per `sideEffects: false`. |
|
||||
| `Clock`-injected `timestamp` for deterministic tests | ⬆️ | ⬆️ | Enhanced: upstream hard-coded `Date.now()`. We inject a `Clock` so `FakeClock`-driven tests get deterministic timestamps without monkey-patching `Date`. |
|
||||
| `UNKNOWN_CURRENCY` typed error | ⬆️ | ⬆️ | Enhanced: upstream threw plain `Error("VirtualCurrencyProvider: no rates for base UNKNOWN")`. We throw `CurrencyProviderError(CURRENCY_PROVIDER.UNKNOWN_CURRENCY)` with `providerName` populated. |
|
||||
| `defineCurrencyProvider`-friendly | ⬆️ | ⬆️ | Enhanced: the provider exposes a stable class with the programmable methods AND can be wrapped via `defineCurrencyProvider` if a consumer prefers the object-literal idiom. |
|
||||
|
||||
The enterprise audit reference is
|
||||
`../suaveplan/enterprise/currency-providers.md`; the virtual provider is one
|
||||
row in that audit and carried no specific gaps beyond the registry-typed-error
|
||||
gap, which is closed here.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
Run `bunx turbo ls --filter=...@suaveplan/currency-provider-virtual` to enumerate.
|
||||
|
||||
- No current dependents (verified by `bunx turbo ls --filter=...@suaveplan/currency-provider-virtual`).
|
||||
|
||||
The package is a leaf sibling — the future commerce / gaming stack downstream
|
||||
will consume it directly, but those packages are still pending migration.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Triangulation across the rate table (see Parity Checklist row).
|
||||
- `@suaveplan/money` `registerCurrency` integration (see Parity Checklist row).
|
||||
- Persistence — the rate table is purely in-memory by design (this provider's
|
||||
whole purpose). Callers that need persistence layer a storage adapter on top
|
||||
and reload the table at construction.
|
||||
- `AbortSignal` plumbing on `fetchRates` — the operation is synchronous over
|
||||
an in-memory `Map`; an injected `signal` would have nothing to abort. The
|
||||
optional `signal` from `RateFetchOptions` is accepted by the type but
|
||||
unused.
|
||||
@@ -0,0 +1,196 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: currency-provider-virtual (add-currency-provider-virtual)
|
||||
|
||||
**Package**: `@suaveplan/currency-provider-virtual`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
> No exceptions. This is a fresh package — only `## ADDED Requirements` apply.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: VirtualCurrencyProvider implements CurrencyProvider with virtual support
|
||||
|
||||
The package SHALL export a `VirtualCurrencyProvider` class that implements
|
||||
the `CurrencyProvider` contract from `@suaveplan/currency`. The provider
|
||||
SHALL expose `name === "virtual-currency"`, `supportsCrypto === false`,
|
||||
`supportsFiat === false`, and `supportsVirtual === true`. The provider
|
||||
SHALL NOT consume `FetchLike`, SHALL NOT issue HTTP requests, and SHALL
|
||||
NOT depend on `@suaveplan/money` — it is a pure in-memory rate source.
|
||||
|
||||
#### Scenario: surfaces the expected identity flags
|
||||
|
||||
- **WHEN** a `VirtualCurrencyProvider` is constructed
|
||||
- **THEN** `provider.name === "virtual-currency"`,
|
||||
`provider.supportsCrypto === false`,
|
||||
`provider.supportsFiat === false`,
|
||||
`provider.supportsVirtual === true`
|
||||
|
||||
#### Scenario: package has no FetchLike consumption
|
||||
|
||||
- **WHEN** `grep -rn "FetchLike\|globalThis.fetch\|fetch(" src/` is run
|
||||
- **THEN** the output contains zero matches (no HTTP code paths exist)
|
||||
|
||||
**Verified by:** `src/provider/provider.test.ts::"surfaces the expected identity flags"` AND `src/provider/provider.test.ts::"has no FetchLike or fetch references in production source"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Programmable rate-table API
|
||||
|
||||
The provider SHALL expose a programmable rate table via `setRate(from,
|
||||
to, rate)`, `removeRate(from, to): boolean`, `clear(): void`,
|
||||
`hasRate(from, to): boolean`, `getRate(from, to): number | undefined`,
|
||||
and `listCurrencies(): readonly string[]`. `setRate` SHALL be upsert —
|
||||
calling it twice for the same `(from, to)` pair replaces the previous
|
||||
rate without throwing. `removeRate` SHALL return `true` when a pair was
|
||||
removed and `false` otherwise, and SHALL NOT throw on unknown pairs.
|
||||
`clear` SHALL empty the entire rate table. `listCurrencies` SHALL return
|
||||
a sorted snapshot (lexicographic) of every currency that appears as a
|
||||
`from` in the table.
|
||||
|
||||
#### Scenario: setRate upserts rates
|
||||
|
||||
- **WHEN** `setRate("USD", "GOLD", 100)` is called and then
|
||||
`setRate("USD", "GOLD", 200)` is called
|
||||
- **THEN** `getRate("USD", "GOLD") === 200` (the second call replaced
|
||||
the first without throwing)
|
||||
|
||||
#### Scenario: removeRate is idempotent
|
||||
|
||||
- **WHEN** `setRate("USD", "GOLD", 100)` is followed by two calls to
|
||||
`removeRate("USD", "GOLD")`
|
||||
- **THEN** the first call returns `true`, the second returns `false`,
|
||||
and `hasRate("USD", "GOLD") === false`
|
||||
|
||||
#### Scenario: clear empties the table
|
||||
|
||||
- **WHEN** several rates are set and `clear()` is called
|
||||
- **THEN** `listCurrencies()` returns `[]` and `hasRate(...)` returns
|
||||
`false` for every previously-known pair
|
||||
|
||||
#### Scenario: hasRate and getRate report pair state
|
||||
|
||||
- **WHEN** `setRate("USD", "GOLD", 100)` is called
|
||||
- **THEN** `hasRate("USD", "GOLD") === true`,
|
||||
`hasRate("USD", "GEMS") === false`,
|
||||
`getRate("USD", "GOLD") === 100`,
|
||||
`getRate("USD", "MISSING") === undefined`
|
||||
|
||||
#### Scenario: listCurrencies returns a sorted snapshot
|
||||
|
||||
- **WHEN** rates are set with `from` values `"USD"`, `"GOLD"`, `"EUR"`
|
||||
in any order
|
||||
- **THEN** `listCurrencies()` returns `["EUR", "GOLD", "USD"]`
|
||||
(lexicographic order, regardless of insertion order)
|
||||
|
||||
**Verified by:** `src/provider/provider.test.ts::"setRate upserts an existing pair without throwing"` AND `src/provider/provider.test.ts::"removeRate returns true on first call and false on the second"` AND `src/provider/provider.test.ts::"clear empties the rate table"` AND `src/provider/provider.test.ts::"hasRate and getRate report pair state correctly"` AND `src/provider/provider.test.ts::"listCurrencies returns a lexicographic snapshot"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: fetchRates returns the rates for the requested base
|
||||
|
||||
The provider SHALL implement `fetchRates(options?: RateFetchOptions):
|
||||
Promise<RateFetchResult>`. The returned `result.base` SHALL be
|
||||
`options?.base` when provided, otherwise the provider's configured
|
||||
`defaultBase`. The returned `result.rates` SHALL contain exactly the
|
||||
rates stored under that base in the rate table, optionally filtered by
|
||||
`options?.targets` (an inclusion list — only matching targets appear).
|
||||
`result.timestamp` SHALL be the value returned by the injected `Clock`
|
||||
at the moment of the call. The provider SHALL NOT triangulate — it
|
||||
returns only the rates explicitly stored under the requested base.
|
||||
|
||||
#### Scenario: returns all rates for the default base
|
||||
|
||||
- **WHEN** the provider is constructed with `defaultBase: "USD"` and
|
||||
rates `setRate("USD", "GOLD", 100)`, `setRate("USD", "GEMS", 500)`,
|
||||
and `fetchRates()` is called
|
||||
- **THEN** the promise resolves to a result with `base: "USD"`,
|
||||
`rates: { GOLD: 100, GEMS: 500 }`
|
||||
|
||||
#### Scenario: filters by targets
|
||||
|
||||
- **WHEN** the provider holds rates for `GOLD`, `GEMS`, and `SILVER`
|
||||
under `"USD"`, and `fetchRates({ base: "USD", targets: ["GOLD"] })`
|
||||
is called
|
||||
- **THEN** the resolved `rates` contains exactly `{ GOLD: 100 }` (the
|
||||
other targets are absent)
|
||||
|
||||
#### Scenario: honours an explicit non-default base
|
||||
|
||||
- **WHEN** `fetchRates({ base: "GOLD" })` is called against a provider
|
||||
that has rates set with `setRate("GOLD", "GEMS", 5)` and
|
||||
`setRate("GOLD", "USD", 0.01)`
|
||||
- **THEN** the resolved `result.base === "GOLD"` and the resolved
|
||||
`rates` contains exactly `{ GEMS: 5, USD: 0.01 }`
|
||||
|
||||
#### Scenario: returns an empty rate map when the base has no targets
|
||||
|
||||
- **WHEN** `setRate("USD", "GOLD", 100)` is called and
|
||||
`fetchRates({ base: "USD", targets: ["MISSING"] })` is called
|
||||
- **THEN** the resolved `rates` is `{}` (no `targets` matched, the call
|
||||
does not throw — `MISSING` simply isn't in the inclusion list)
|
||||
|
||||
**Verified by:** `src/provider/provider.test.ts::"returns all rates for the default base"` AND `src/provider/provider.test.ts::"filters returned rates by the targets inclusion list"` AND `src/provider/provider.test.ts::"honours an explicit non-default base"` AND `src/provider/provider.test.ts::"returns an empty rate map when the targets filter excludes every stored target"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: UNKNOWN_CURRENCY raised for missing base
|
||||
|
||||
The provider SHALL throw
|
||||
`CurrencyProviderError(CURRENCY_PROVIDER.UNKNOWN_CURRENCY)` when
|
||||
`fetchRates({ base })` is called for a `base` that is not present in
|
||||
the rate table (and the table is therefore unable to answer for that
|
||||
base). The thrown error SHALL carry `providerName === "virtual-currency"`.
|
||||
The provider SHALL NOT throw a raw `Error`, and SHALL NOT triangulate
|
||||
to satisfy an unknown base — the registry can be empty, and an unknown
|
||||
base is a definitive "no answer" path.
|
||||
|
||||
#### Scenario: throws CurrencyProviderError(UNKNOWN_CURRENCY) when base is missing
|
||||
|
||||
- **WHEN** a fresh `VirtualCurrencyProvider` (empty table) has
|
||||
`fetchRates({ base: "NOPE" })` called on it
|
||||
- **THEN** the promise rejects with a `CurrencyProviderError` whose
|
||||
`code === "CURRENCY_PROVIDER.UNKNOWN_CURRENCY"` and whose
|
||||
`providerName === "virtual-currency"`
|
||||
|
||||
#### Scenario: throws UNKNOWN_CURRENCY when the default base is empty
|
||||
|
||||
- **WHEN** the provider was constructed with `defaultBase: "USD"` and
|
||||
no `USD`-rooted rates have ever been set, and `fetchRates()` (no
|
||||
options) is called
|
||||
- **THEN** the promise rejects with `CurrencyProviderError` whose
|
||||
`code === "CURRENCY_PROVIDER.UNKNOWN_CURRENCY"`
|
||||
|
||||
**Verified by:** `src/provider/provider.test.ts::"throws CurrencyProviderError(UNKNOWN_CURRENCY) when the requested base is missing"` AND `src/provider/provider.test.ts::"throws UNKNOWN_CURRENCY when the default base has no stored rates"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Clock injection for deterministic timestamps
|
||||
|
||||
The provider constructor SHALL accept an optional `clock: Clock`
|
||||
(`@suaveplan/types`'s `Clock = () => number`) and SHALL use it to
|
||||
populate `RateFetchResult.timestamp` on every `fetchRates` call. When
|
||||
no `clock` is provided the provider SHALL default to
|
||||
`() => Date.now()`. The provider SHALL call the clock at the moment of
|
||||
the `fetchRates` invocation, not at construction.
|
||||
|
||||
#### Scenario: FakeClock-driven timestamps are deterministic across calls
|
||||
|
||||
- **WHEN** the provider is constructed with `clock: () => fakeClock.now()`
|
||||
where `fakeClock` is a `FakeClock({ startTime: 1_700_000_000_000 })`,
|
||||
and `fetchRates()` is called twice with `fakeClock.advance(1000)`
|
||||
between them
|
||||
- **THEN** the first result's `timestamp === 1_700_000_000_000` and the
|
||||
second result's `timestamp === 1_700_000_001_000`
|
||||
|
||||
#### Scenario: default clock falls back to Date.now
|
||||
|
||||
- **WHEN** the provider is constructed without a `clock` and
|
||||
`fetchRates()` is called
|
||||
- **THEN** the returned `timestamp` is a finite number whose value is
|
||||
within ±1000 ms of `Date.now()` measured immediately around the call
|
||||
|
||||
**Verified by:** `src/provider/provider.test.ts::"FakeClock-driven timestamps are deterministic across calls"` AND `src/provider/provider.test.ts::"defaults to Date.now when no clock is injected"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
@@ -0,0 +1,81 @@
|
||||
# Tasks — Add @suaveplan/currency-provider-virtual
|
||||
|
||||
## Spec-completeness preflight (AGENTS.md §12a)
|
||||
|
||||
- [x] Numbered checklist of every `### Requirement` and `#### Scenario`
|
||||
drafted in `specs/currency-provider-virtual/spec.md` (5 requirements,
|
||||
16 scenarios).
|
||||
|
||||
## Package scaffold
|
||||
|
||||
- [x] `packages/finance/currency-provider-virtual/package.json` (0.0.0,
|
||||
universal, `sideEffects: false`)
|
||||
- [x] `tsconfig.json`, `tsconfig.lib.json`
|
||||
- [x] `vite.config.ts` (externals: `@suaveplan/currency`,
|
||||
`@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`,
|
||||
`tslib`)
|
||||
- [x] `vitest.config.ts` (istanbul, 100/100/100/100, polyfills setup)
|
||||
- [x] `tests/setup/polyfills.ts`
|
||||
- [x] `README.md` (≥200 words, Modules section)
|
||||
|
||||
## Implementation
|
||||
|
||||
- [x] `src/error-codes/error-codes.ts` — re-export `UNKNOWN_CURRENCY` code +
|
||||
`CurrencyProviderError` from slim core for ergonomic access
|
||||
- [x] `src/error-codes/error-codes.md`
|
||||
- [x] `src/provider/provider.types.ts` — `VirtualCurrencyProviderConfig`,
|
||||
`VirtualCurrencyProviderOptions`, `VirtualRateEntry` shapes
|
||||
- [x] `src/provider/provider.ts` — `VirtualCurrencyProvider` class
|
||||
- [x] `src/provider/provider.md`
|
||||
- [x] `src/index.ts` — explicit named barrel
|
||||
- [x] `src/provider/index.ts` (feature barrel)
|
||||
|
||||
## Tests
|
||||
|
||||
- [x] `src/provider/provider.test.ts` — covers constructor, `setRate`,
|
||||
`removeRate`, `clear`, `hasRate`, `getRate`, `listCurrencies`,
|
||||
`fetchRates` (default base, custom base, target filter, empty table),
|
||||
`UNKNOWN_CURRENCY` error path, `FakeClock`-driven timestamp,
|
||||
`supportsVirtual` flag
|
||||
- [x] `tests/dep-graph.test.ts` — root barrel has no `export *`; feature
|
||||
barrels have no `export *`; subpath exports match feature modules
|
||||
|
||||
## Documentation
|
||||
|
||||
- [x] Every implementation `.ts` has a sibling `.md` (§13 step 5 grep
|
||||
returns zero)
|
||||
- [x] `README.md` at package root with Install / Quick start / Features /
|
||||
Modules sections
|
||||
|
||||
## Pre-archive gates (AGENTS.md §13)
|
||||
|
||||
- [ ] Step 0 — spec reconciliation: every `### Requirement` and
|
||||
`**Verified by:**` resolves to a real file + verbatim test name
|
||||
- [ ] Step 1 — `bunx turbo lint typecheck test --filter=@suaveplan/currency-provider-virtual`
|
||||
- [ ] Step 2 — `bunx turbo test:coverage --filter=@suaveplan/currency-provider-virtual`
|
||||
reports 100/100/100/100 via istanbul
|
||||
- [ ] Step 3 — `bunx turbo build --filter=@suaveplan/currency-provider-virtual` green
|
||||
- [ ] Step 4 — banned-pattern grep returns zero
|
||||
- [ ] Step 4a — barrel-shape grep returns zero
|
||||
- [ ] Step 4b — test-runner-import grep returns zero
|
||||
- [ ] Step 5 — missing-`.md` grep returns zero
|
||||
- [ ] Step 6 — README present
|
||||
- [ ] Step 7 — bare-version grep returns zero
|
||||
- [ ] Step 8 — subpath-export count matches feature module count
|
||||
- [ ] Step 9 — security-scan grep returns zero
|
||||
|
||||
## Root-doc refresh (AGENTS.md §13b)
|
||||
|
||||
- [ ] Re-run `bun scripts/folder-reorg/generate-manifest.ts` to refresh
|
||||
`MANIFEST.md`
|
||||
- [ ] Re-run `bun scripts/generate-missing.ts` to refresh `MISSING.yml`
|
||||
- [ ] Add `@suaveplan/currency-provider-virtual` row to `STATUS.md`
|
||||
- [ ] Add `@suaveplan/currency-provider-virtual` row to `SUMMARY.md`
|
||||
- [ ] `bunx markdownlint-cli2 --fix` across touched root + change files
|
||||
|
||||
## Archive (AGENTS.md §14)
|
||||
|
||||
- [ ] Fold `specs/currency-provider-virtual/spec.md` into
|
||||
`openspec/specs/currency-provider-virtual/spec.md`
|
||||
- [ ] Move `openspec/changes/add-currency-provider-virtual/` →
|
||||
`openspec/archive/2026-05-17-add-currency-provider-virtual/`
|
||||
Reference in New Issue
Block a user