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,147 @@
# Design: @suaveplan/currency-provider-kraken
> Non-normative. The normative requirements live in
> `./specs/currency-provider-kraken/spec.md`. The cross-cutting
> foundation-with-sibling-plugins architecture lives in
> `openspec/archive/2026-05-17-slim-currency-providers/design.md` — read it
> first; this document covers only the Kraken-specific deltas.
## Shape
```text
@suaveplan/currency-provider-kraken
├── src/symbols/symbols.ts # KRAKEN_SYMBOL_MAP + pair→symbol reverse
├── src/provider/provider.ts # KrakenProvider class + DEFAULT_KRAKEN_BASE_URL
└── src/index.ts # explicit named barrel
```
Two feature modules. Each carries a sibling `.md`, a `.test.ts`, and a
feature-folder `index.ts` (named-exports only — `export *` is banned per
AGENTS.md §6a).
## Wire shape
The Kraken public ticker returns:
```json
{
"error": ["EGeneral:Too many requests"],
"result": {
"XXBTZUSD": { "c": ["67000.00", "0.1"], "..." },
"XETHZUSD": { "c": ["3500.00", "1.0"], "..." }
}
}
```
- `error` is **always** present (empty array on success). A non-empty array
is the canonical upstream-failure signal and maps to
`CurrencyProviderError(UPSTREAM_ERROR)`. The first entry is carried on
`upstreamMessage` so consumers can dispatch on rate-limit / endpoint /
pair-unknown messages.
- `result` is keyed by Kraken's non-standard pair name (`XXBTZUSD`, not
`BTCUSD`). The symbol map normalises both directions; the reverse map
lets us go from the wire pair back to a Suaveplan symbol in O(1).
- `c` is `[price, lotVolume]` — `c[0]` is the last-trade-closed price.
## Symbol mapping
Twelve symbols are mapped, matching the upstream coverage. The list is
intentionally small (rather than dynamic) because:
- Kraken's full pair list rotates frequently (new listings, delistings) and
pulling the live `AssetPairs` list per request would double the round trips.
- The mapped set is the high-volume subset most consumers actually want.
- Callers who need an additional symbol can supply a custom symbol-to-pair
override via the constructor (`config.extraSymbols`) without forking the
package. The override map is merged into `KRAKEN_SYMBOL_MAP` at
construction time.
```ts
const KRAKEN_SYMBOL_MAP: Readonly<Record<string, string>> = Object.freeze({
BTC: "XXBTZUSD",
ETH: "XETHZUSD",
SOL: "SOLUSD",
USDT: "USDTZUSD",
DOT: "DOTUSD",
ADA: "ADAUSD",
LINK: "LINKUSD",
AVAX: "AVAXUSD",
ATOM: "ATOMUSD",
LTC: "XLTCZUSD",
DOGE: "XDGUSD",
XRP: "XXRPZUSD",
});
```
## Rate model
Kraken returns the USD price of one unit of the crypto (e.g. `1 BTC = 67000
USD`). The `CurrencyProvider` contract returns rates as "1 base = N target"
where base defaults to `USD`. The rate for `USD → BTC` is therefore the
reciprocal: `1 / 67000`. This matches the upstream behaviour verbatim and is
what every downstream `CurrencyConverter` assumes.
If `c[0]` does not parse to a positive finite number, the entry is dropped
(skipped) rather than raising — Kraken occasionally returns `"0.00"` for
freshly-listed pairs that haven't yet traded. If the entire mapped set drops,
the provider raises `UNKNOWN_CURRENCY` so the caller sees the failure rather
than a silent empty rate map.
## Targets filtering
When `options.targets` is non-empty, the provider filters to those targets
after building the full rate map. Any target that is not in
`KRAKEN_SYMBOL_MAP` is a no-op (it simply doesn't appear in the result). If
the filtered set is empty AND `options.targets` was non-empty, the provider
raises `UNKNOWN_CURRENCY` — this is the "the caller asked for X / Y / Z and
Kraken can't price any of them" signal, distinct from "Kraken is down"
(`HTTP_ERROR`), "Kraken returned an error array" (`UPSTREAM_ERROR`), and
"the response body is malformed" (`PARSE_ERROR`).
## Error path matrix
| Wire condition | Code | `providerName` | Other discriminants |
|---|---|---|---|
| Non-2xx response | `HTTP_ERROR` | `"kraken"` | `status` |
| `data.error.length > 0` | `UPSTREAM_ERROR` | `"kraken"` | `upstreamMessage = data.error[0]` |
| JSON parse failure | `PARSE_ERROR` | `"kraken"` | (cause: original error) |
| `data.result` not an object | `PARSE_ERROR` | `"kraken"` | (cause: shape-check failure) |
| All requested targets unmapped | `UNKNOWN_CURRENCY` | `"kraken"` | — |
`HTTP_ERROR` is raised by the shared `httpRequest()` helper (we don't
re-implement it); the other three are raised inline after the response body
is read.
## Test strategy
- `symbols.test.ts` — asserts the 12-symbol map is exported, immutable
(`Object.isFrozen`), and that the reverse map is consistent.
- `provider.test.ts` — uses a recording `FetchLike` closure (the same
pattern the slim core's `http.test.ts` established). Covers:
- happy path with a real-shape Kraken response (`error: []`, `result: {…}`)
- `supportsCrypto` / `supportsFiat` metadata
- rates are reciprocals of `c[0]`
- `options.base` default of `"USD"`
- `options.targets` filtering (subset)
- `options.targets` filtering with one unmapped → still returns mapped ones
- `options.targets` all unmapped → `UNKNOWN_CURRENCY`
- `data.error` non-empty → `UPSTREAM_ERROR` with `upstreamMessage`
- non-2xx response → `HTTP_ERROR` with `status` (via the shared helper)
- JSON-parse failure → `PARSE_ERROR`
- shape-check failure (missing `result`) → `PARSE_ERROR`
- `c[0]` non-positive / non-finite → entry skipped, others retained
- custom `baseUrl` is honoured
- custom `fetch` (FetchLike) is used in place of `globalThis.fetch`
- `options.signal` is forwarded into the `httpRequest` call (abort
propagates)
All tests import runner primitives from `@suaveplan/testing/runner`.
No `vi.mock`, no `vi.stubGlobal` — only injection through the constructor.
## Tier purity
Universal-tier. The Kraken endpoint is reachable from any runtime that has
`fetch`: Node ≥ 18, Bun, modern browsers (subject to Kraken's CORS policy
— at time of writing the public ticker endpoint serves
`Access-Control-Allow-Origin: *`), Web Workers. No `node:`/`bun:`/DOM
imports.

View File

@@ -0,0 +1,109 @@
# Add @suaveplan/currency-provider-kraken sibling package
**Package**: `@suaveplan/currency-provider-kraken`
**Status**: PROPOSED
**Type**: new package (genesis-original sibling of `@suaveplan/currency-providers`)
- **Design**: ./design.md
- **Spec delta**: ./specs/currency-provider-kraken/spec.md
## Why
The slim refactor archived at
`openspec/archive/2026-05-17-slim-currency-providers/` moved every concrete
provider out of `@suaveplan/currency-providers` and committed to landing each
one as its own `@suaveplan/currency-provider-<name>` sibling. Kraken is one of
the nine siblings the slim core's `proposal.md ## Affected dependents` block
explicitly enumerates — this change ships it.
Kraken is a free public-ticker provider (no API key) with a regulated-exchange
real-time trading price. Wiring it through the slim core's `httpRequest` +
`defineCurrencyProvider` factory gives consumers a granular install (only pay
for Kraken when they want Kraken) and lets the provider iterate on its own
version cadence — exactly the foundation-with-sibling-plugins shape established
by `@suaveplan/network-plugin-*` and `@suaveplan/drive-plugin-*`.
## What changes
### ADDED
- New package `@suaveplan/currency-provider-kraken` at
`packages/finance/currency-provider-kraken/` (universal-tier, `0.1.0`,
`sideEffects: false`).
- `KrakenProvider` class implementing the `CurrencyProvider` contract from
`@suaveplan/currency`. Constructor accepts an optional `NoKeyConfig` from
`@suaveplan/currency-providers` (`baseUrl` override + injectable `FetchLike`).
- `KRAKEN_SYMBOL_MAP` — the canonical mapping from Suaveplan symbol
(`BTC`, `ETH`, `SOL`, `USDT`, `DOT`, `ADA`, `LINK`, `AVAX`, `ATOM`, `LTC`,
`DOGE`, `XRP`) to Kraken pair (`XXBTZUSD`, `XETHZUSD`, …) — exported so
callers can probe / extend.
- `DEFAULT_KRAKEN_BASE_URL = "https://api.kraken.com/0/public"` exported
constant.
- Uses the shared `httpRequest()` helper from
`@suaveplan/currency-providers/http/http` for the GET — non-2xx responses
raise `CurrencyProviderError(HTTP_ERROR)` with `providerName: "kraken"` and
the HTTP status populated.
- Maps the Kraken-specific `data.error: string[]` array to
`CurrencyProviderError(UPSTREAM_ERROR)` with the first message carried on
`upstreamMessage`.
- Raises `CurrencyProviderError(PARSE_ERROR)` when the response body fails to
JSON-parse, when the `result` map is missing / not an object, or when a
ticker's `c[0]` price cannot be parsed to a positive finite number.
- Raises `CurrencyProviderError(UNKNOWN_CURRENCY)` when every requested
`options.targets` symbol resolves to an unknown / unmapped Kraken pair, so
callers don't silently receive an empty rate set.
## Source mapping
Upstream reference for parity (the legacy 9-provider bundle):
| Capability | Upstream source |
|---|---|
| `KrakenProvider` class | `../suaveplan/packages/finance/currency-providers/src/crypto/kraken.ts` L1-L91 |
| Kraken symbol → pair table | `../suaveplan/packages/finance/currency-providers/src/crypto/kraken.ts` L11-L24 |
| `c[0]` last-trade-closed price + reciprocal-to-USD rate calc | `../suaveplan/packages/finance/currency-providers/src/crypto/kraken.ts` L67-L77 |
| Kraken `error: string[]` upstream-failure detection | `../suaveplan/packages/finance/currency-providers/src/crypto/kraken.ts` L58-L60 |
| HTTP-error path | `../suaveplan/packages/finance/currency-providers/src/crypto/kraken.ts` L46-L51 (now delegated to `httpRequest`) |
Genesis-native deltas vs. upstream:
| Capability | Upstream | Migrated | Notes |
|---|---|---|---|
| Plain `Error` HTTP failure | ✅ | ⬆️ | Now `CurrencyProviderError(HTTP_ERROR)` via shared `httpRequest`. |
| Plain `Error` Kraken-error array | ✅ | ⬆️ | Now `CurrencyProviderError(UPSTREAM_ERROR)` with `upstreamMessage`. |
| Hardcoded `globalThis.fetch` | ✅ | ⬆️ | Injectable `FetchLike` via `NoKeyConfig.fetch`. |
| Silent empty rate set on unknown target | ✅ | ⬆️ | Now `CurrencyProviderError(UNKNOWN_CURRENCY)` when every requested target is unmapped. |
| `BTC`/`ETH`/`SOL`/`USDT`/`DOT`/`ADA`/`LINK`/`AVAX`/`ATOM`/`LTC`/`DOGE`/`XRP` symbol map | ✅ | ✅ | Preserved verbatim. |
| Reciprocal-of-USD-price rate model | ✅ | ✅ | Preserved verbatim. |
No upstream enterprise audit exists for `currency-providers` (the slim core's
design notes were the genesis-native source).
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/currency-provider-kraken` to
enumerate. The package is brand-new so there are no current dependents:
- No current dependents (verified by
`bunx turbo ls --filter=...@suaveplan/currency-provider-kraken`).
This package consumes `@suaveplan/currency-providers` (the slim core) for the
`httpRequest` helper, the `CurrencyProviderError` + `CURRENCY_PROVIDER_CODES`
registry, and the `NoKeyConfig` shape. It consumes `@suaveplan/currency` for
the `CurrencyProvider` / `RateFetchOptions` / `RateFetchResult` contracts and
`@suaveplan/types` for the `FetchLike` shape. None of those packages need
re-testing — they are all unchanged.
## Out of scope
- Authenticated Kraken endpoints (private balance / order endpoints) — this
provider wraps only the public ticker, matching the slim core's "free, no
API key" classification.
- WebSocket / streaming ticker — same deferral as the slim core's
`## Out of scope`.
- Historical-rate / OHLC endpoints — same deferral.
- A `KrakenProvider` built via `defineCurrencyProvider` (object-literal
idiom) — the class form is the canonical surface; the factory remains
available for callers who prefer it.

View File

@@ -0,0 +1,185 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: currency-provider-kraken (add-currency-provider-kraken)
**Package**: `@suaveplan/currency-provider-kraken`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time.
## ADDED Requirements
### Requirement: KrakenProvider implements the CurrencyProvider contract
The package SHALL export a `KrakenProvider` class implementing the
`CurrencyProvider` contract from `@suaveplan/currency`. Instances MUST
expose `name = "kraken"`, `supportsCrypto = true`, `supportsFiat = false`,
and a `fetchRates(options?)` method returning a `Promise<RateFetchResult>`.
#### Scenario: identity metadata is fixed
- **WHEN** a new `KrakenProvider()` is constructed
- **THEN** `provider.name === "kraken"`, `provider.supportsCrypto === true`,
and `provider.supportsFiat === false`
**Verified by:** `src/provider/provider.test.ts::"exposes the canonical kraken identity"`
---
### Requirement: Constructor accepts NoKeyConfig with optional extras
The constructor SHALL accept an optional `KrakenProviderConfig` extending
the slim core's `NoKeyConfig` (optional `baseUrl`, optional injectable
`FetchLike`). It MUST additionally accept an optional `extraSymbols` record
that is merged on top of `KRAKEN_SYMBOL_MAP` so callers can extend coverage
without forking the package. The constructor SHALL NOT require an API key.
#### Scenario: defaults are used when no config is supplied
- **WHEN** `new KrakenProvider()` is constructed and `fetchRates()` is called
- **THEN** the request URL begins with `DEFAULT_KRAKEN_BASE_URL` and the
injected `globalThis.fetch` is used
#### Scenario: custom baseUrl and fetch are honoured
- **WHEN** `new KrakenProvider({ baseUrl: "https://custom.kraken", fetch: recordingFetch })` is constructed and `fetchRates()` is called
- **THEN** `recordingFetch` is invoked exactly once with a URL beginning with `"https://custom.kraken"`
#### Scenario: extraSymbols extends the symbol map
- **WHEN** `new KrakenProvider({ extraSymbols: { MATIC: "MATICUSD" } })` is constructed and the Kraken response carries `MATICUSD`
- **THEN** the returned `rates` map contains a `MATIC` entry
**Verified by:** `src/provider/provider.test.ts::"defaults to globalThis.fetch and DEFAULT_KRAKEN_BASE_URL"` AND `src/provider/provider.test.ts::"honours custom baseUrl and fetch"` AND `src/provider/provider.test.ts::"extends the symbol map via extraSymbols"`
---
### Requirement: Uses the shared httpRequest helper
The provider SHALL issue its request through `httpRequest()` from
`@suaveplan/currency-providers/http/http` so that abort-signal composition,
timeout derivation, and non-2xx → `HTTP_ERROR` mapping are uniform across
every sibling provider. It SHALL forward `options.signal` to the helper.
#### Scenario: non-2xx response raises HTTP_ERROR
- **WHEN** the injected fetch responds with status 503
- **THEN** `fetchRates()` rejects with a `CurrencyProviderError` whose
`code === CURRENCY_PROVIDER_CODES.HTTP_ERROR`, `providerName === "kraken"`,
and `status === 503`
#### Scenario: caller signal aborts the request
- **WHEN** `options.signal` is an already-aborted `AbortSignal`
- **THEN** `fetchRates({ signal })` rejects with an abort error (the
injected fetch sees an aborted signal)
**Verified by:** `src/provider/provider.test.ts::"raises HTTP_ERROR on non-2xx response"` AND `src/provider/provider.test.ts::"forwards options.signal to the underlying fetch"`
---
### Requirement: Maps Kraken error array to UPSTREAM_ERROR
When the Kraken response body's `error` field is a non-empty string array,
the provider SHALL raise `CurrencyProviderError(UPSTREAM_ERROR)` with
`providerName: "kraken"` and `upstreamMessage` set to the first element of
the array. The error message string SHALL include the first upstream
message so existing `Error.message` consumers continue to match it.
#### Scenario: non-empty error array maps to UPSTREAM_ERROR
- **WHEN** the response body is `{ error: ["EGeneral:Too many requests"], result: {} }`
- **THEN** `fetchRates()` rejects with a `CurrencyProviderError` whose
`code === CURRENCY_PROVIDER_CODES.UPSTREAM_ERROR`,
`upstreamMessage === "EGeneral:Too many requests"`,
and `message` contains `"Too many requests"`
**Verified by:** `src/provider/provider.test.ts::"maps a non-empty error array to UPSTREAM_ERROR"`
---
### Requirement: Maps unparsable bodies to PARSE_ERROR
When the response body is not valid JSON, or when the parsed body's
`result` field is missing / not an object, the provider SHALL raise
`CurrencyProviderError(PARSE_ERROR)` with `providerName: "kraken"`.
#### Scenario: malformed JSON raises PARSE_ERROR
- **WHEN** the response body is `"not-json"`
- **THEN** `fetchRates()` rejects with a `CurrencyProviderError` whose
`code === CURRENCY_PROVIDER_CODES.PARSE_ERROR`
#### Scenario: missing result field raises PARSE_ERROR
- **WHEN** the response body is `{ "error": [] }` (no `result`)
- **THEN** `fetchRates()` rejects with a `CurrencyProviderError` whose
`code === CURRENCY_PROVIDER_CODES.PARSE_ERROR`
**Verified by:** `src/provider/provider.test.ts::"raises PARSE_ERROR on malformed JSON"` AND `src/provider/provider.test.ts::"raises PARSE_ERROR when result is missing"`
---
### Requirement: Maps ticker prices to reciprocal-of-USD rates
For every entry in `data.result` whose Kraken pair matches a mapped symbol,
the provider SHALL set `rates[symbol] = 1 / Number.parseFloat(ticker.c[0])`
when that float is positive and finite. Entries whose price does not parse
to a positive finite number SHALL be skipped (not raised) — Kraken
occasionally returns `"0.00"` for freshly-listed pairs.
#### Scenario: positive prices are reciprocated
- **WHEN** the response carries `XXBTZUSD: { c: ["67000.00", "0.1"] }` and `XETHZUSD: { c: ["3500.00", "1.0"] }`
- **THEN** the returned rates satisfy `rates.BTC ≈ 1/67000` and `rates.ETH ≈ 1/3500`
#### Scenario: zero / non-finite prices are skipped, not raised
- **WHEN** the response carries `XXBTZUSD: { c: ["0.00", "0"] }` alongside a valid `XETHZUSD` entry
- **THEN** the returned rates contain `ETH` but not `BTC`, and no error is raised
**Verified by:** `src/provider/provider.test.ts::"maps positive prices to reciprocal-of-USD rates"` AND `src/provider/provider.test.ts::"skips zero or non-finite prices without raising"`
---
### Requirement: Filters by options.targets and raises on all-unknown targets
When `options.targets` is supplied, the provider SHALL return only rates for
the requested target symbols. If after filtering the resulting rate map is
empty AND `options.targets` was non-empty, the provider SHALL raise
`CurrencyProviderError(UNKNOWN_CURRENCY)` with `providerName: "kraken"` so
callers see the failure rather than a silent empty rate set.
#### Scenario: subset filtering returns only requested targets
- **WHEN** the response carries BTC + ETH + SOL and `fetchRates({ targets: ["BTC"] })` is called
- **THEN** the returned `rates` has exactly one key — `"BTC"`
#### Scenario: all-unknown targets raise UNKNOWN_CURRENCY
- **WHEN** `fetchRates({ targets: ["FAKE1", "FAKE2"] })` is called against a healthy ticker response
- **THEN** `fetchRates` rejects with a `CurrencyProviderError` whose
`code === CURRENCY_PROVIDER_CODES.UNKNOWN_CURRENCY` and
`providerName === "kraken"`
**Verified by:** `src/provider/provider.test.ts::"filters by options.targets"` AND `src/provider/provider.test.ts::"raises UNKNOWN_CURRENCY when every requested target is unmapped"`
---
### Requirement: Exposes the symbol-mapping surface
The package SHALL export `KRAKEN_SYMBOL_MAP` (the canonical symbol → Kraken
pair record), `KRAKEN_PAIR_TO_SYMBOL` (the reverse map, pair → symbol), and
`DEFAULT_KRAKEN_BASE_URL`. The forward map SHALL be `Object.freeze`-d.
#### Scenario: symbol map is exported, frozen, and complete
- **WHEN** the package is imported
- **THEN** `KRAKEN_SYMBOL_MAP` is exported, `Object.isFrozen(KRAKEN_SYMBOL_MAP)` is `true`, and it contains entries for `BTC`, `ETH`, `SOL`, `USDT`, `DOT`, `ADA`, `LINK`, `AVAX`, `ATOM`, `LTC`, `DOGE`, `XRP`
#### Scenario: reverse map is consistent with the forward map
- **WHEN** any entry `KRAKEN_SYMBOL_MAP[symbol] === pair` is examined
- **THEN** `KRAKEN_PAIR_TO_SYMBOL[pair] === symbol`
**Verified by:** `src/symbols/symbols.test.ts::"exports a frozen 12-entry symbol map"` AND `src/symbols/symbols.test.ts::"reverse map is consistent with the forward map"`

View File

@@ -0,0 +1,79 @@
# Tasks — Add @suaveplan/currency-provider-kraken
## Spec-completeness preflight (AGENTS.md §12a)
- [x] Numbered checklist of every `### Requirement` and `#### Scenario`
drafted in `specs/currency-provider-kraken/spec.md` (8 requirements;
see the spec delta).
## Scaffold
- [x] Create `packages/finance/currency-provider-kraken/` with
`package.json` (`0.1.0`, `sideEffects: false`, `type: module`)
- [x] Add `tsconfig.json` + `tsconfig.lib.json` mirroring the slim core
- [x] Add `vite.config.ts` with `preserveModules: true` and the four
external packages
- [x] Add `vitest.config.ts` (forks pool, istanbul provider, 100/100/100/100
thresholds, includes `tests/**/*.test.ts`)
- [x] Add `tests/setup/polyfills.ts` (empty shim)
- [x] Add `README.md` (per CLAUDE.md §5 — H1, paragraph, install, quick
start, modules)
## Implement
- [x] `src/symbols/symbols.ts` — `KRAKEN_SYMBOL_MAP` (12 frozen entries) +
`KRAKEN_PAIR_TO_SYMBOL` reverse map + `mergeSymbolMap` helper
- [x] `src/symbols/symbols.md` — sibling doc (≥200 words; Purpose / Features
/ Basic / Advanced / API Reference / Implementation Notes)
- [x] `src/symbols/symbols.test.ts` — frozen, complete, reverse consistent,
merge override behaviour
- [x] `src/symbols/index.ts` — explicit named barrel
- [x] `src/provider/provider.ts` — `KrakenProvider` class +
`KrakenProviderConfig` extending `NoKeyConfig` with optional
`extraSymbols` map; `DEFAULT_KRAKEN_BASE_URL` constant
- [x] `src/provider/provider.md` — sibling doc
- [x] `src/provider/provider.test.ts` — full scenario coverage matching
the `## Test strategy` in `design.md`
- [x] `src/provider/index.ts` — explicit named barrel
- [x] `src/index.ts` — explicit named barrel re-exporting both feature
modules
## Dep-graph + invariants test
- [x] `tests/dep-graph.test.ts` — no `export *` in `src/index.ts` or any
feature barrel; subpath-export count matches feature-module count
## Pre-archive gates (AGENTS.md §13)
- [x] Step 0 — spec reconciliation (every `### Requirement` +
`**Verified by:**` resolves)
- [x] Step 1 — `bunx turbo lint typecheck test --filter=@suaveplan/currency-provider-kraken`
+ `bun run check` both green
- [x] Step 2 — `bunx turbo test:coverage --filter=@suaveplan/currency-provider-kraken`
reports 100/100/100/100
- [x] Step 3 — `bunx turbo build --filter=@suaveplan/currency-provider-kraken`
green
- [x] Step 4 — banned-pattern grep returns zero
- [x] Step 4a — barrel-shape grep returns zero
- [x] Step 4b — test-runner-import grep returns zero
- [x] Step 5 — missing-`.md` grep returns zero
- [x] Step 6 — README present
- [x] Step 7 — bare-version grep returns zero
- [x] Step 8 — subpath-export count = 2 (matches feature module count)
- [x] Step 9 — security-scan grep returns zero
- [x] Step 10 — dependent sweep (no current dependents — fast-path)
## Root-doc refresh (AGENTS.md §13b)
- [x] `bun scripts/folder-reorg/generate-manifest.ts`
- [x] `bun scripts/generate-missing.ts`
- [x] Update `STATUS.md` row for `@suaveplan/currency-provider-kraken`
- [x] Update `SUMMARY.md` row for `@suaveplan/currency-provider-kraken`
- [x] `bunx markdownlint-cli2 --fix`
## Archive (AGENTS.md §14)
- [x] Fold `specs/currency-provider-kraken/spec.md` into
`openspec/specs/currency-provider-kraken/spec.md`
- [x] Move `openspec/changes/add-currency-provider-kraken/` →
`openspec/archive/2026-05-17-add-currency-provider-kraken/`