first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
{
"name": "library-navigator",
"version": "1.0.0",
"description": "Natural-language library discovery across the 500+ @suaveplan/* packages"
}

View File

@@ -0,0 +1,52 @@
---
name: sp-ab-testing
description: Enterprise A/B testing with deterministic FNV-1a bucketing, targeting rules, variant assignment, conversion tracking, and OTel instrumentation.
---
# sp-ab-testing
| Symbol | Kind | Responsibility |
|---|---|---|
| `createExperimentManager` | Factory | Creates an `ExperimentManager` with `defineExperiment`, `assign`, `track`, and `getResults` |
| `bucketUser` | Function | FNV-1a 32-bit hash maps `(userId, experimentId)` to a stable variant |
| `evaluateTargeting` | Function | Evaluates targeting rules (`eq`, `neq`, `in`, `notIn`, `gt`, `lt`, `gte`, `lte`) with AND logic |
| `computeResults` | Function | Aggregates conversion events per variant and computes conversion rates |
| `ForceVariantProvider` | Class | Provider that forces a specific variant assignment for testing |
| `AB_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/testkit/testing/ab-testing/src/{bucketing,core,targeting,results,providers,types}/`
**Sibling packages:**
- `@suaveplan/feature-flags` — gating for feature rollout
- `@suaveplan/testing` — test assertions and helpers
**Patterns:**
*Quick Start:*
```ts
import { createExperimentManager } from "@suaveplan/ab-testing";
const manager = createExperimentManager();
manager.defineExperiment({ id: "cta", status: "running", variants: [{ id: "ctrl", weight: 1, isControl: true }, { id: "treat", weight: 1 }], primaryMetric: "click" });
const a = manager.assign("cta", "user-123");
```
*Targeting + Sample Rate:*
```ts
manager.defineExperiment({
id: "checkout", status: "running", variants: [...],
targeting: [{ attribute: "country", operator: "in", value: ["US", "CA"] }],
sampleRate: 0.5,
});
```
**Constraints:**
- All experiment definitions validated by Zod schemas (`ExperimentDefinitionSchema`)
- Assignment result is cached per user; repeat calls are stable
- `track()` requires the experiment to exist; throws `AB_CODES.EXPERIMENT_NOT_FOUND` otherwise
- OTel spans only emitted when `@opentelemetry/api` is present
**Checklist:**
- [ ] `manager.assign()` returns the same variant for the same user ID across restarts
- [ ] Targeting rules with multiple conditions use AND logic
- [ ] `getResults()` reflects all tracked conversions per variant
- [ ] Subpath exports (`/bucketing`, `/targeting`, `/results`, `/providers`, `/errors`) resolve

View File

@@ -0,0 +1,96 @@
---
name: sp-analytics-browser
description: Browser-tier client-side product analytics — AnalyticsTracker, BatchTransport, BeaconTransport, SessionManager with consent gate, PII scrubber, and sampling.
---
# Architecture
| Component | Responsibility |
|---|---|
| `AnalyticsTracker` | Central orchestrator; accepts events, coordinates consent → sampling → scrubbing → queue → flush |
| `BatchTransport` | HTTP transport for steady-state flushes with `AbortController` + `timeoutMs` |
| `BeaconTransport` | Page-unload-safe transport via `navigator.sendBeacon` with `fetch` fallback |
| `SessionManager` | Session lifecycle tracking with duration, page count, and referrer capture |
| `AllowAllPolicy` / `DoNotTrackPolicy` | Built-in consent policy implementations |
| `HeadSampler` | Head-based sampling with per-event-type overrides and injectable `random` |
| `scrubPII` | Field-level PII redaction/hash/drop with dotted-path traversal |
| `MemoryStorageBackend` / `WebStorageBackend` | Durable event queue backends (memory + Web Storage) |
| `AnalyticsEventSchema` / `validateAnalyticsEvent` | Zod schema and parse helper for event validation |
| `ANALYTICS_BROWSER_CODES` + `AnalyticsBrowserError` | Structured error registry for transport and validation failures |
| `EventBus<AnalyticsEventMap>` | Lifecycle observability (`track`, `flush`, `drop`, `error`, `shutdown`) via `@suaveplan/events` |
## File Structure
```
packages/observability/monitor/analytics-browser/src/
tracker/ AnalyticsTracker orchestrator
transport/ BatchTransport, BeaconTransport
session/ SessionManager
consent/ AllowAllPolicy, DoNotTrackPolicy, ConsentPolicy SPI
pii/ scrubPII with redact/hash/drop strategies
sampling/ HeadSampler
storage/ MemoryStorageBackend, WebStorageBackend
schema/ AnalyticsEventSchema, validateAnalyticsEvent
error-codes/ ANALYTICS_BROWSER_CODES, AnalyticsBrowserError
telemetry/ OTel counters and histograms
types/ TypeScript interfaces and event map
internal/id/ Cryptographically-random ID generator
```
## Sibling packages
- `@suaveplan/events` — typed EventBus used for lifecycle observability
- `@suaveplan/error` — structured error registry via `defineCodes`
- `@suaveplan/types` — shared TypeScript primitives
- `@suaveplan/testing` — unit test infrastructure
## Patterns
### Quick Start
```ts
import { AnalyticsTracker, BatchTransport } from "@suaveplan/analytics-browser";
const tracker = new AnalyticsTracker(
new BatchTransport({ endpoint: "/api/analytics" }),
);
tracker.identify("user-123");
tracker.track("clicked", { surface: "header" });
await tracker.flush();
```
### Consent + PII + sampling
```ts
import { AnalyticsTracker, DoNotTrackPolicy, HeadSampler } from "@suaveplan/analytics-browser";
const tracker = new AnalyticsTracker(transport, {
consentPolicy: new DoNotTrackPolicy(),
scrubRules: [{ field: "email", strategy: "redact" }],
sampler: new HeadSampler(0.1, { error_reported: 1 }),
});
```
### Lifecycle event bus
```ts
tracker.events.on("track", ({ event }) => {});
tracker.events.on("flush", ({ eventCount, success, latencyMs }) => {});
tracker.events.on("drop", ({ reason, event }) => {});
```
## Constraints
- Browser-only (`navigator.sendBeacon`, `localStorage`, `AbortController`)
- `BeaconTransport` throws `AnalyticsBrowserError(BEACON_REJECTED)` when host rejects beacon and fallback disabled
- Storage backend contract (`enqueue` / `dequeueAll` / `clear` / `getDepth`) is stable SPI
- OTel instruments are no-ops when no `MeterProvider` registered
- All event payloads are single objects (not tuples) on the EventBus
## Checklist
- [ ] Verify `AnalyticsEventSchema` validates all required fields
- [ ] Confirm `ANALYTICS_BROWSER_CODES` registered via `defineCodes`
- [ ] Check subpath exports in `package.json` align with `src/` modules
- [ ] Run `bunx turbo lint typecheck test build --filter=@suaveplan/analytics-browser`
- [ ] Confirm tree-shaking test proves subpath imports are independently loadable

View File

@@ -0,0 +1,53 @@
---
name: sp-audience-rules
description: Universal audience segmentation with 14 comparison operators, AND/OR chaining, dot-notation field resolution, and pluggable SegmentStore.
---
| Class / Function | Responsibility |
| --- | --- |
| `SegmentEvaluator` | Pure-function rule engine evaluating 14 operators with dot-notation field resolution and AND/OR chaining |
| `SegmentRefresher` | Orchestrates segment re-evaluation and persists `contactIds` back to `SegmentStore`; emits `segment:refreshed` on typed `EventBus` |
| `InMemorySegmentStore` | Reference `SegmentStore` with `Clock` + `IdGenerator` injection |
| `hasTag`, `statusIs`, `channelIs`, `lastActivityAfter`, `lastActivityBefore`, `metadataEquals`, `metadataContains` | Seven `SegmentRule` predicate factory helpers |
| `AUDIENCE_RULES_CODES` / `AudienceRulesError` | Stable `AUDIENCE_RULES.*` code registry with typed error hierarchy |
**File Structure:** `packages/commerce/contacts/audience-rules/src/{error-codes,predicates,schemas,segment-evaluator,segment-refresher,store,types}/`
**Sibling packages:**
- `@suaveplan/contacts` — contact and group management; provides `ContactData` consumed by audience-rules
- `@suaveplan/events` — typed `EventBus<TMap>` substrate for `SegmentRefresher` emissions
- `@suaveplan/error` — `defineCodes` + `SuaveplanError` base
- `@suaveplan/types` — `Clock` + `IdGenerator` injectables
**Patterns**
Quick Start:
```ts
import { InMemorySegmentStore, SegmentEvaluator, SegmentRefresher, hasTag, statusIs } from "@suaveplan/audience-rules";
const provider = async () => [{ id: "c1", tags: ["vip"], status: "active" }, { id: "c2", tags: ["basic"], status: "active" }];
const store = new InMemorySegmentStore();
const evaluator = new SegmentEvaluator(provider);
const refresher = new SegmentRefresher(store, evaluator);
const segment = await store.create({ tenantId: "tenant-1", name: "Active VIPs", rules: [hasTag("vip"), statusIs("active")], refreshMode: "manual" });
refresher.events.on("segment:refreshed", ({ segmentId, result }) => console.log(`${segmentId}: ${result.total} matches`));
await refresher.refresh(segment.id);
```
- **14 operators:** `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains`, `not_contains`, `in`, `not_in`, `exists`, `not_exists`, `starts_with`, `ends_with`.
- **Dot-notation paths:** Field access supports nested objects and numeric array indices (e.g. `channels.0.type`).
- **Refresh modes:** `realtime`, `scheduled`, and `manual` for cache policy control.
- **Custom storage:** Implement the `SegmentStore` interface (`create`, `get`, `list`, `update`, `delete`, `updateMembers`) backed by any persistence layer.
- **Subpath imports:** Deep imports available via `package.json` exports map (`./segment-evaluator/segment-evaluator`, `./predicates/predicates`, etc.).
**Constraints**
- Multi-tenant isolation via `tenantId` on `Segment` and `list`.
- All inputs validated with Zod at every public boundary.
- `Clock` and `IdGenerator` are injectable into `InMemorySegmentStore`; no `Date.now()` in production code.
- Stable error codes: `INVALID_INPUT`, `SEGMENT_NOT_FOUND`, `UNKNOWN_OPERATOR`, `PROVIDER_FAILED`.
**Checklist**
- [ ] Read `src/segment-evaluator/segment-evaluator.md` for operator behavior and test matrix
- [ ] Read `src/segment-refresher/segment-refresher.md` for event map and refresh lifecycle
- [ ] Read `src/store/store.md` for `SegmentStore` interface contract
- [ ] Read `src/predicates/predicates.md` for factory function signatures
- [ ] Verify `package.json` exports for subpath deep imports

View File

@@ -0,0 +1,46 @@
---
name: sp-audio-analysis-beat
description: Beat, tempo and onset detection — energy ODF, spectral flux, comb-filter tempo, beat-grid phase alignment. AnalysisOp wrapper with batch/streaming/per-block entry points.
---
# sp-audio-analysis-beat
| Symbol | Kind | Responsibility |
|---|---|---|
| `BeatDetector` | Class | Main entry point — selectable algorithm, batch/streaming/per-block modes |
| `energyOdf` / `energyOdfFromSamples` | Function | Energy-based onset detection function |
| `spectralFlux` / `spectralFluxOdf` | Function | Spectral-flux onset detection function |
| `combTempo` / `combScore` | Function | Comb-filter autocorrelation tempo estimation |
| `buildBeatGrid` | Function | Phase-aligning beat-grid construction from detected onsets |
| `fftRadix2` / `magnitudeSpectrum` | Function | Hand-rolled radix-2 FFT and magnitude spectrum helpers |
| `AUDIO_ANALYSIS_BEAT_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio/audio-analysis-beat/src/{detector,algorithms,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-core` — audio pipeline core
- `@suaveplan/audio-analysis-loudness` — loudness analysis
- `@suaveplan/audio-analysis-pitch` — pitch analysis
- `@suaveplan/audio-analysis-spectrum` — spectrum analysis
**Patterns:**
*Quick Start:*
```ts
import { BeatDetector } from "@suaveplan/audio-analysis-beat";
const detector = new BeatDetector({ algorithm: "spectralFlux" });
const result = detector.detect(audioDocument);
// result.bpm, result.beats, result.onsets
```
**Constraints:**
- Universal — runs in Bun, Node, Deno, and browser with zero platform-only APIs
- Hand-rolled radix-2 FFT over typed arrays
- Subpath exports: `/detector/beat-detector`, `/algorithms/energy-odf`, `/algorithms/spectral-flux`, `/algorithms/comb-tempo`, `/algorithms/beat-grid`, `/error-codes`
- `BeatAlgorithmSchema` validates `"energy"`, `"spectralFlux"`, `"combFilter"`
**Checklist:**
- [ ] `BeatDetector.detect()` returns BPM, beat locations, and onset times
- [ ] `spectralFlux` algorithm uses hand-rolled FFT
- [ ] `buildBeatGrid` aligns phase to detected onsets
- [ ] All three algorithms selectable via `BeatAlgorithmSchema`

View File

@@ -0,0 +1,45 @@
---
name: sp-audio-analysis-loudness
description: ITU-R BS.1770-4 loudness analyser — K-weighting, gated integrated LUFS, momentary/short-term loudness, EBU R128 loudness range, 4x oversampled true-peak, RMS, sample peak, DC offset.
---
# sp-audio-analysis-loudness
| Symbol | Kind | Responsibility |
|---|---|---|
| `LoudnessMeter` | Class | Main entry point — streaming and block-level metering for broadcast compliance |
| `IntegratedLoudness` | Class | Gated integrated LUFS measurement per BS.1770-4 |
| `momentaryLufs` / `shortTermLufs` | Function | Momentary (400ms) and short-term (3s) loudness calculations |
| `loudnessRange` | Function | EBU R128 loudness range (LRA) computation |
| `truePeakDbtp` | Function | 4x oversampled true-peak detection in dBTP |
| `buildKWeighting` / `createKWeighting` | Function | K-weighting filter construction (pre-filter + RLB) |
| `AUDIO_ANALYSIS_LOUDNESS_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio/audio-analysis-loudness/src/{meter,algorithms,filters,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-analysis-beat` — beat/tempo detection
- `@suaveplan/audio-analysis-pitch` — pitch/key/chord detection
- `@suaveplan/audio-analysis-spectrum` — spectral analysis
- `@suaveplan/audio-core` — audio pipeline core
**Patterns:**
*Quick Start:*
```ts
import { LoudnessMeter } from "@suaveplan/audio-analysis-loudness";
const meter = new LoudnessMeter({ sampleRate: 48000 });
const result = meter.measure(audioBlock);
// result.integratedLufs, result.momentaryLufs, result.truePeakDbtp
```
**Constraints:**
- ITU-R BS.1770-4 compliant with absolute (-70 LUFS) and relative (-10 LU) gating
- Subpath exports: `/meter/loudness-meter`, `/algorithms/lufs`, `/algorithms/true-peak`, `/algorithms/loudness-range`, `/filters/k-weighting`, `/error-codes`
- OTel instrumented for block-level metering operations
**Checklist:**
- [ ] `LoudnessMeter.measure()` returns integrated, momentary, short-term, and true-peak values
- [ ] K-weighting filter matches ITU-R BS.1770-4 specification
- [ ] True-peak detection uses 4x oversampling
- [ ] Loudness range follows EBU R128 algorithm

View File

@@ -0,0 +1,47 @@
---
name: sp-audio-analysis-pitch
description: Pitch, key, and chord detection — YIN/McLeod/cepstrum f0, Krumhansl-Schmuckler/Temperley key finding, chromagram template-matched chord progressions with temporal smoothing.
---
# sp-audio-analysis-pitch
| Symbol | Kind | Responsibility |
|---|---|---|
| `PitchDetector` | Class | F0 estimation with selectable algorithm (YIN, McLeod, cepstrum) |
| `KeyDetector` | Class | Key finding via Krumhansl-Schmuckler or Temperley profiles |
| `ChordDetector` | Class | Chromagram template-matched chord progression detection |
| `yinPitch` / `mcleodPitch` / `cepstrumPitch` | Function | Individual f0 estimation algorithms |
| `detectKey` | Function | Key estimation with configurable profile set |
| `matchChord` / `smoothLabels` / `mergeRuns` | Function | Chord matching, temporal smoothing, and run merging |
| `chromagramFromFrame` / `accumulateChroma` / `normaliseChroma` | Function | Chromagram construction and normalisation |
| `AUDIO_ANALYSIS_PITCH_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio/audio-analysis-pitch/src/{detector,algorithms,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-analysis-beat` — beat/tempo detection
- `@suaveplan/audio-analysis-loudness` — loudness analysis
- `@suaveplan/audio-analysis-spectrum` — spectral analysis
- `@suaveplan/audio-core` — audio pipeline core
**Patterns:**
*Quick Start:*
```ts
import { PitchDetector, KeyDetector, ChordDetector } from "@suaveplan/audio-analysis-pitch";
const pitch = new PitchDetector({ algorithm: "yin" }).detect(audio);
const key = new KeyDetector({ profileSet: "krumhansl" }).detect(audio);
const chords = new ChordDetector().detect(audio);
```
**Constraints:**
- Universal — zero browser or Node-only APIs
- Subpath exports: `/detector/pitch-detector`, `/detector/key-detector`, `/detector/chord-detector`, `/algorithms/yin`, `/algorithms/chromagram`, `/algorithms/key-finding`, `/algorithms/chord-matching`, `/error-codes`
- `PitchAlgorithm` schema validates `"yin"`, `"mcleod"`, `"cepstrum"`
- `KeyProfileSet` includes `"krumhansl"` and `"temperley"`
**Checklist:**
- [ ] `PitchDetector` returns accurate f0 for YIN, McLeod, and cepstrum algorithms
- [ ] `KeyDetector` returns correct key estimates with both profile sets
- [ ] `ChordDetector` applies temporal smoothing and run merging
- [ ] Chromagram construction uses standard 12-bin pitch-class mapping

View File

@@ -0,0 +1,48 @@
---
name: sp-audio-analysis-spectrum
description: Spectral analysis — real FFT, Hann/Hamming/Blackman windows, mel filterbank, DCT-II, SpectrumAnalyzer/Spectrogram/MFCC ops with OTel. Zero platform dependencies.
---
# sp-audio-analysis-spectrum
| Symbol | Kind | Responsibility |
|---|---|---|
| `SpectrumAnalyzer` | Class | Real FFT spectrum analysis with configurable window and overlap |
| `Spectrogram` | Class | Time-frequency spectrogram generation |
| `MFCC` | Class | Mel-frequency cepstral coefficient extraction |
| `transform` | Function | Real FFT with caching and validation |
| `applyWindow` / `getWindow` | Function | Window function application (Hann, Hamming, Blackman) |
| `buildMelFilterbank` / `applyMelFilterbank` | Function | Mel-scale filterbank construction and application |
| `dctII` | Function | DCT-II transform for MFCC computation |
| `hzToMel` / `melToHz` | Function | Mel-scale frequency conversions |
| `AUDIO_ANALYSIS_SPECTRUM_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio/audio-analysis-spectrum/src/{analyzer,algorithms,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-analysis-beat` — beat/tempo detection
- `@suaveplan/audio-analysis-loudness` — loudness analysis
- `@suaveplan/audio-analysis-pitch` — pitch/key/chord detection
- `@suaveplan/audio-core` — audio pipeline core
**Patterns:**
*Quick Start:*
```ts
import { SpectrumAnalyzer, Spectrogram, MFCC } from "@suaveplan/audio-analysis-spectrum";
const spectrum = new SpectrumAnalyzer({ frameSize: 2048, window: "hann" }).analyze(audio);
const spec = new Spectrogram({ frameSize: 2048, hopSize: 512 }).run(audio);
const mfcc = new MFCC({ numCoefficients: 13 }).run(audio);
```
**Constraints:**
- Universal — zero browser or Node-only APIs
- Subpath exports: `/analyzer/spectrum-analyzer`, `/analyzer/spectrogram`, `/analyzer/mfcc`, `/algorithms/fft`, `/algorithms/window`, `/algorithms/mel-scale`, `/algorithms/dct`, `/error-codes`
- FFT cache, window cache, mel filterbank cache, and DCT cache are all clearable via `clear*Cache()` functions
- Frame size validated against `FFT_FRAME_MIN` and `FFT_FRAME_MAX`
**Checklist:**
- [ ] `SpectrumAnalyzer.analyze()` returns magnitude spectrum with correct windowing
- [ ] `Spectrogram.run()` returns a 2D time-frequency matrix
- [ ] `MFCC.run()` returns the specified number of cepstral coefficients
- [ ] `transform()` validates frame size against min/max bounds

View File

@@ -0,0 +1,78 @@
---
name: sp-audio-cli
description: Server-tier audio CLI tool — chainable pipeline grammar, batch processing, codec conversion, analysis, info, play, record. Thin shell over audio-core + codecs + engine-server.
---
# sp-audio-cli
| Symbol | Kind | Responsibility |
|---|---|---|
| `AudioCLI` | Class | Main CLI orchestrator with pipeline parsing and command dispatch |
| `createDefaultCodecRegistry` | Function | Builds a codec registry with all built-in codecs |
| `detectCodecForOutput` | Function | Detects appropriate codec from output file extension |
| `parsePipeline` | Function | Parses chainable pipeline grammar string into `ParsedPipeline` |
| `validatePipeline` | Function | Validates pipeline steps against available codecs and commands |
| `runProcess` | Function | Executes a process command (decode → effect → encode chain) |
| `runBatch` | Function | Executes batch processing over file glob patterns |
| `runAnalyze` | Function | Runs audio analysis and returns JSON report |
| `runConvert` | Function | Converts audio between formats |
| `runInfo` | Function | Returns metadata info report for audio file |
| `runPlay` | Function | Plays audio through default output device |
| `runRecord` | Function | Records audio from default input device |
| `createSilentMicrophone` | Function | Creates a dummy microphone adapter for testing |
| `noopSpeaker` | Function | No-op speaker adapter for testing |
| `AUDIO_CLI_CODES` | Constant | Error codes via `defineCodes` |
| `AudioCliError` | Class | Typed error with audio-cli codes |
| `EXIT_CODES` | Constant | Shell exit code mapping |
**File Structure:** `packages/media/audio/audio-cli/src/{cli,commands,pipeline,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-core` — audio engine primitives
- `@suaveplan/audio-codecs` — codec registry and plugins
- `@suaveplan/audio-engine-server` — server-side audio engine
**Patterns:**
*Quick Start:*
```ts
import { AudioCLI } from "@suaveplan/audio-cli";
const cli = new AudioCLI();
await cli.run(["convert", "input.wav", "output.mp3"]);
```
*Pipeline:*
```ts
import { parsePipeline } from "@suaveplan/audio-cli/pipeline/parser";
const pipeline = parsePipeline("decode:mp3 → normalize → encode:flac");
```
*Batch:*
```ts
import { runBatch } from "@suaveplan/audio-cli";
await runBatch({ pattern: "*.wav", command: "convert", outputExt: "mp3" });
```
*Analysis:*
```ts
import { runAnalyze } from "@suaveplan/audio-cli";
const report = await runAnalyze({ input: "track.wav" });
```
**Constraints:**
- Node.js ≥22 required (uses `node:fs`, `node:child_process`)
- Binary: `suaveplan-audio` registered in `package.json` `bin`
- Subpath exports: `cli`, `commands/process`, `commands/batch`, `commands/analyze`, `commands/convert`, `commands/info`, `commands/play`, `commands/record`, `pipeline/parser`, `error-codes`
- Pipeline grammar uses `→` to chain steps
- Batch processing supports glob patterns and parallel execution
- Analysis produces structured JSON reports
- All commands support `--help` and structured option schemas
**Checklist:**
- [ ] `AudioCLI` parses and dispatches all commands
- [ ] Pipeline grammar parses chainable step sequences
- [ ] Batch command processes glob patterns in parallel
- [ ] Analysis command returns structured JSON report
- [ ] Convert command handles all registered codecs
- [ ] Play and record commands interface with OS audio I/O
- [ ] Error codes use `defineCodes` from `@suaveplan/error`

View File

@@ -0,0 +1,75 @@
---
name: sp-audio-codec-ffmpeg-native
description: Server-tier ffmpeg-native AudioCodec plugin. Spawns system ffmpeg binary via fluent-ffmpeg for chunked decode/encode of wav/mp3/ogg/opus/flac/aac/webm. AbortSignal cleanup with SIGTERM→SIGKILL escalation.
---
# sp-audio-codec-ffmpeg-native
| Symbol | Kind | Responsibility |
|---|---|---|
| `FfmpegNativeCodec` | Class | `AudioCodec` implementation using system ffmpeg binary |
| `resolveBinary` | Function | Resolves ffmpeg binary path with optional overrides |
| `runDecode` | Function | Spawns ffmpeg for decode: input → PCM F32 interleaved |
| `runEncode` | Function | Spawns ffmpeg for encode: PCM F32 → target format |
| `createStreamingDecoder` | Function | Returns `FfmpegStreamingDecoder` for chunked decode |
| `createStreamingEncoder` | Function | Returns `FfmpegStreamingEncoder` for chunked encode |
| `sniff` | Function | Detects audio format from file header |
| `validatePcm` | Function | Validates PCM buffer parameters |
| `interleaveF32` / `deinterleaveF32` | Function | Channel interleave/deinterleave helpers |
| `concat` | Function | Concatenates PCM buffers |
| `register` | Function | One-line helper to register codec with registry |
| `SUPPORTED_ENCODE_FORMATS` | Constant | Supported output formats array |
| `ENCODE_MIME_TYPES` | Constant | MIME type mapping per format |
| `AUDIO_CODEC_FFMPEG_NATIVE_CODES` | Constant | Error codes via `defineCodes` |
| `AudioCodecFfmpegNativeError` | Class | Typed error with ffmpeg-native codes |
**File Structure:** `packages/media/audio/audio-codec-ffmpeg-native/src/{codec,binary,runner,stream,register,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-codecs` — codec registry interface
- `@suaveplan/audio-core` — audio primitives
- `@suaveplan/audio-codec-ffmpeg-wasm` — browser-tier ffmpeg.wasm equivalent
**Patterns:**
*Quick Start:*
```ts
import { FfmpegNativeCodec, register } from "@suaveplan/audio-codec-ffmpeg-native";
register(registry);
```
*Decode:*
```ts
import { runDecode } from "@suaveplan/audio-codec-ffmpeg-native";
const { pcm, sampleRate, channels } = await runDecode({ input: "input.mp3" });
```
*Encode:*
```ts
import { runEncode } from "@suaveplan/audio-codec-ffmpeg-native";
await runEncode({ input: pcm, sampleRate, channels, output: "out.flac", format: "flac" });
```
*Streaming:*
```ts
import { createStreamingDecoder } from "@suaveplan/audio-codec-ffmpeg-native";
const decoder = createStreamingDecoder({ format: "mp3", sampleRate: 44100, channels: 2 });
```
**Constraints:**
- Node.js ≥22 required; spawns `ffmpeg` system binary
- Uses `fluent-ffmpeg` for command building; `@ffmpeg-installer/ffmpeg` optional
- Supports decode/encode: WAV, MP3, OGG, Opus, FLAC, AAC, WebM
- Streaming decoder/encoder for chunked processing
- AbortSignal triggers SIGTERM → SIGKILL escalation for cleanup
- Bounded concurrency to prevent process exhaustion
- Subpath exports: `binary/binary`, `codec/codec`, `runner/runner`, `stream/stream-decoder`, `stream/stream-encoder`, `register/register`, `error-codes`
**Checklist:**
- [ ] `FfmpegNativeCodec` implements the `AudioCodec` interface from `@suaveplan/audio-codecs`
- [ ] `resolveBinary` finds ffmpeg via PATH or optional installer
- [ ] Decode produces interleaved F32 PCM with correct sample rate and channel count
- [ ] Encode accepts interleaved F32 PCM and writes valid output file
- [ ] Streaming decoder processes chunks without loading entire file
- [ ] AbortSignal terminates ffmpeg process with SIGTERM then SIGKILL
- [ ] All error codes use `defineCodes` from `@suaveplan/error`

View File

@@ -0,0 +1,72 @@
---
name: sp-audio-codec-ffmpeg-wasm
description: Browser-tier ffmpeg.wasm AudioCodec plugin. Lazy-loaded @ffmpeg/core WASM in Web Worker. Decode/encode WAV/MP3/OGG/Opus/FLAC/AAC/WebM. AbortSignal, timeout watchdog, COI-gated multi-thread.
---
# sp-audio-codec-ffmpeg-wasm
| Symbol | Kind | Responsibility |
|---|---|---|
| `FfmpegWasmCodec` | Class | `AudioCodec` implementation using ffmpeg.wasm in a Web Worker |
| `FfmpegWasmCodecOptions` | Type | Core URL, worker URL, timeouts, multi-thread options |
| `FfmpegWasmCodecOptionsSchema` | Schema | Zod schema for options validation |
| `loadCore` | Function | Lazy-loads `@ffmpeg/core` WASM module |
| `runDecode` | Function | Decode via ffmpeg.wasm: input → PCM F32 |
| `runEncode` | Function | Encode via ffmpeg.wasm: PCM F32 → target format |
| `sniffFormat` | Function | Detects audio format from buffer header |
| `buildCommand` | Function | Builds ffmpeg CLI command string for encode |
| `FfmpegWasmStreamingDecoder` | Class | Chunked streaming decoder |
| `FfmpegWasmStreamingEncoder` | Class | Chunked streaming encoder |
| `WorkerHost` | Class | Manages Web Worker lifecycle and message passing |
| `AUDIO_CODEC_FFMPEG_WASM_CODES` | Constant | Error codes via `defineCodes` |
| `AudioCodecFfmpegWasmError` | Class | Typed error with ffmpeg-wasm codes |
| `register` | Function | One-line helper to register codec with registry |
| `SUPPORTED_FORMATS` | Constant | Supported format strings |
| `FORMAT_MIME` | Constant | MIME type mapping per format |
**File Structure:** `packages/media/audio/audio-codec-ffmpeg-wasm/src/{codec,core-loader,decode,encode,streaming,worker,register,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-codecs` — codec registry interface
- `@suaveplan/audio-core` — audio primitives
- `@suaveplan/audio-codec-ffmpeg-native` — server-tier ffmpeg-native equivalent
**Patterns:**
*Quick Start:*
```ts
import { FfmpegWasmCodec, register } from "@suaveplan/audio-codec-ffmpeg-wasm";
register(registry);
```
*Decode:*
```ts
import { runDecode, loadCore } from "@suaveplan/audio-codec-ffmpeg-wasm";
await loadCore({ coreUrl: "/ffmpeg/core.js" });
const { pcm, sampleRate, channels } = await runDecode({ input: fileBuffer, format: "mp3" });
```
*Streaming:*
```ts
import { FfmpegWasmStreamingDecoder } from "@suaveplan/audio-codec-ffmpeg-wasm";
const decoder = new FfmpegWasmStreamingDecoder({ format: "mp3", sampleRate: 44100, channels: 2 });
```
**Constraints:**
- Browser-only; lazy-loads `@ffmpeg/core` WASM inside a Web Worker
- Optional dependencies: `@ffmpeg/ffmpeg`, `@ffmpeg/core`, `@ffmpeg/util`
- Supports decode/encode: WAV, MP3, OGG, Opus, FLAC, AAC, WebM
- Multi-threading requires COI (Cross-Origin Isolation) headers
- Timeout watchdog kills stuck ffmpeg operations
- AbortSignal cancels in-flight operations
- Subpath exports: `codec/ffmpeg-wasm-codec`, `core-loader/core-loader`, `decode/decode-pipeline`, `encode/encode-pipeline`, `streaming/streaming-decoder`, `streaming/streaming-encoder`, `worker/worker-host`, `register/register`, `error-codes`
**Checklist:**
- [ ] `FfmpegWasmCodec` implements the `AudioCodec` interface from `@suaveplan/audio-codecs`
- [ ] `loadCore` lazily loads WASM only when first needed
- [ ] WorkerHost manages Web Worker lifecycle and message RPC
- [ ] Decode produces F32 PCM from source format
- [ ] Encode writes valid output in target format
- [ ] Streaming decoder processes chunks without full materialisation
- [ ] AbortSignal cancels ffmpeg operation; timeout watchdog kills stuck runs
- [ ] Multi-thread only activates when COI headers are present

View File

@@ -0,0 +1,80 @@
---
name: sp-audio-codecs
description: Universal audio codec registry — decode/encode plugin loader with format detection, progressive streaming, native WAV/AIFF/CAF codecs, lazy-loaded WASM codecs for MP3/FLAC/OGG/Opus/AAC/WebM/QOA.
---
# sp-audio-codecs
| Symbol | Kind | Responsibility |
|---|---|---|
| `CodecRegistry` | Class | Registry for audio codec plugins with format auto-detection |
| `AudioCodec` | Interface | Contract for all codec implementations (decode/encode/streaming) |
| `decodeSource` | Function | Decode audio from any source (URL, Blob, Buffer) via registry |
| `progressiveDecode` | Function | Progressive streaming decode with yield per chunk |
| `encodeTo` | Function | Encode audio to target format via registry |
| `detectSource` | Function | Detect audio format from source header |
| `createDecodeTransform` | Function | Returns a `TransformStream` for streaming decode |
| `createEncodeTransform` | Function | Returns a `TransformStream` for streaming encode |
| `createWasmCodec` | Function | Factory for lazy-loaded WASM codec plugins |
| `WavCodec` / `AiffCodec` / `CafCodec` | Class | Native codec implementations (no WASM) |
| `Mp3Codec` / `FlacCodec` / `OggVorbisCodec` / `OpusCodec` / `AacCodec` / `WebMCodec` / `QoaCodec` | Class | WASM-backed codec implementations |
| `AUDIO_CODECS_CODES` | Constant | Error codes via `defineCodes` |
| `AudioCodecsError` | Class | Typed error with audio-codecs codes |
| `AudioCodecSchema` | Schema | Zod schema for codec plugin validation |
| `CodecRegistryOptionsSchema` | Schema | Zod schema for registry options |
**File Structure:** `packages/media/audio/audio-codecs/src/{registry,codecs,decode,encode,detect,stream,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-core` — audio primitives (AudioBuffer, PCM types)
- `@suaveplan/audio-codec-ffmpeg-native` — server-tier ffmpeg plugin
- `@suaveplan/audio-codec-ffmpeg-wasm` — browser-tier ffmpeg.wasm plugin
- `@suaveplan/streams` — streaming utilities
**Patterns:**
*Quick Start:*
```ts
import { CodecRegistry, decodeSource, encodeTo } from "@suaveplan/audio-codecs";
const registry = new CodecRegistry();
const { pcm, sampleRate, channels } = await decodeSource({ source: "input.mp3", registry });
await encodeTo({ source: pcm, format: "flac", registry, output: "out.flac" });
```
*Format detection:*
```ts
import { detectSource } from "@suaveplan/audio-codecs";
const { format, mimeType } = await detectSource(buffer);
```
*Streaming:*
```ts
import { createDecodeTransform } from "@suaveplan/audio-codecs";
const transform = createDecodeTransform({ format: "mp3", registry });
readable.pipeThrough(transform).pipeTo(writable);
```
*WASM codec:*
```ts
import { createWasmCodec } from "@suaveplan/audio-codecs";
const mp3Codec = createWasmCodec({ format: "mp3", loader: () => import("mp3-wasm-module") });
```
**Constraints:**
- Universal runtime: works in browser and Node.js with `node:fs` gated at runtime
- Native codecs (WAV, AIFF, CAF) require no WASM
- WASM codecs (MP3, FLAC, OGG, Opus, AAC, WebM, QOA) are lazy-loaded
- Progressive decode yields chunks for large files without full materialisation
- Format detection uses header sniffing (first N bytes)
- Registry pattern: codecs register themselves; consumers query by format
- Subpath exports per codec: `./codecs/wav`, `./codecs/mp3`, `./codecs/flac`, etc.
- Subpath exports: `registry/registry`, `decode/decode-source`, `encode/encode-to`, `detect/detect-format`, `stream/stream-decoder`, `stream/stream-encoder`, `error-codes`
**Checklist:**
- [ ] `CodecRegistry` accepts and queries registered codec plugins
- [ ] `decodeSource` auto-detects format and dispatches to correct codec
- [ ] `encodeTo` writes valid output for all registered formats
- [ ] `detectSource` correctly identifies format from file header
- [ ] Progressive decode yields chunks without loading entire file
- [ ] Native codecs work without WASM; WASM codecs lazy-load on first use
- [ ] All error codes use `defineCodes` from `@suaveplan/error`

View File

@@ -0,0 +1,63 @@
---
name: sp-audio-controllers
description: MIDI hardware controller plugins — Kontrol S88, Maschine, Traktor S4. MidiControllerPlugin base, hot-plug FSM, sequencer, arpeggiator, CC-mapping DSL, LED feedback, bindParameter integration.
---
# sp-audio-controllers
| Symbol | Kind | Responsibility |
|---|---|---|
| `MidiControllerPlugin` | Class | Base class for MIDI hardware controller plugins with hot-plug FSM |
| `createConnectionMachine` | Function | FSM factory for MIDI connection lifecycle (disconnected → connecting → connected → error) |
| `KontrolS88Plugin` | Class | Native Instruments Kontrol S88 integration with LED color and scale modes |
| `MaschinePlugin` | Class | Native Instruments Maschine integration with sample loading |
| `TraktorS4Plugin` | Class | Native Instruments Traktor S4 DJ controller integration with deck tracking |
| `Sequencer` | Class | Transport-synced step sequencer with `StepCallback` |
| `Arpeggiator` | Class | Transport-synced arpeggiator with multiple arp modes |
| `captureNextLearn` | Function | Learn mode — captures the next incoming CC message and returns a `LearnResult` |
| `bindParameter` / `bindPluginPreset` | Function | Binds a MIDI source to an audio-fx parameter or preset |
| `AUDIO_CONTROLLERS_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio/audio-controllers/src/{base,plugins,sequencer,arpeggiator,learn,binding,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin base (bindParameter target)
- `@suaveplan/audio-transport` — transport sync for sequencer/arpeggiator
- `@suaveplan/fsm` — hot-plug connection state machine
- `@suaveplan/midi` — MIDI message parsing and generation
**Patterns:**
*Quick Start:*
```ts
import { KontrolS88Plugin } from "@suaveplan/audio-controllers/plugins/kontrol-s88";
const controller = new KontrolS88Plugin({ adapter: webMidiAdapter });
await controller.connect();
controller.setLED(0, { r: 255, g: 0, b: 0 });
```
*Learn mode:*
```ts
import { captureNextLearn } from "@suaveplan/audio-controllers/learn";
const result = await captureNextLearn(controller); // { cc, channel, value }
```
*Bind to FX parameter:*
```ts
import { bindParameter } from "@suaveplan/audio-controllers/binding";
bindParameter({ cc: 1, channel: 0 }, filterPlugin.cutoff);
```
**Constraints:**
- `MidiAccessAdapter` is injectable for testability (Web MIDI API wrapper)
- Hot-plug FSM via `@suaveplan/fsm` handles connect/disconnect/reconnect
- Subpath exports: `/base/midi-controller-plugin`, `/plugins/kontrol-s88`, `/plugins/maschine`, `/plugins/traktor-s4`, `/sequencer/sequencer`, `/arpeggiator/arpeggiator`, `/learn/learn-mode`, `/binding`, `/error-codes`
- Sequencer and arpeggiator are transport-synced via `@suaveplan/audio-transport`
- LED feedback is bidirectional — controller state reflected on hardware LEDs
**Checklist:**
- [ ] `MidiControllerPlugin` base handles connection lifecycle via FSM
- [ ] `KontrolS88Plugin` sets LED colors and scale modes correctly
- [ ] `Sequencer` fires step callbacks in sync with transport
- [ ] `captureNextLearn` returns the next incoming CC message
- [ ] `bindParameter` maps MIDI CC to FX parameter with smooth interpolation

View File

@@ -0,0 +1,67 @@
---
name: sp-audio-core
description: Universal audio data model and processing primitives — paged AudioDocument, non-destructive PlanEngine, block-level ProcessPipeline, typed OpRegistry. Zero platform deps.
---
# sp-audio-core
| Symbol | Kind | Responsibility |
|---|---|---|
| `AudioDocument` | Class | Paged, immutable audio document — append, splice, trim, insert, and stream operations |
| `PlanEngine` | Class | Non-destructive plan compiler — builds a `CompiledPlan` of `Segment` transforms |
| `ProcessPipeline` | Class | Block-level audio processing pipeline — applies `ProcessFn` over typed-array blocks |
| `OpRegistry` | Class | Typed registry for audio operations (`AudioOpDescriptor`) |
| `allocatePage` / `readPages` / `walkPages` / `releasePage` | Function | Paged memory management for audio buffers |
| `BLOCK_SIZE` / `PAGE_SIZE` | Constant | Default block and page sizes for processing |
| `createAnalysisOp` | Function | Factory for `AnalysisOp` wrappers that run analysis over audio documents |
| `renderBlock` | Function | Renders a processed block to an output buffer |
| `computeStats` | Function | Computes `ComputedStats` (peak, RMS, DC) over audio data |
| `createPlanTransformStream` | Function | Converts a `CompiledPlan` into a readable byte stream |
| `AUDIO_CORE_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio/audio-core/src/{document,page,plan,process,registry,render,stats,stream,analysis,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-analysis-beat` — beat detection (AnalysisOp consumer)
- `@suaveplan/audio-analysis-loudness` — loudness analysis (AnalysisOp consumer)
- `@suaveplan/audio-analysis-pitch` — pitch detection (AnalysisOp consumer)
- `@suaveplan/audio-analysis-spectrum` — spectral analysis (AnalysisOp consumer)
- `@suaveplan/audio-decode-wav` — WAV decoder (produces AudioDocuments)
**Patterns:**
*Quick Start:*
```ts
import { AudioDocument } from "@suaveplan/audio-core";
const doc = AudioDocument.fromDecodedAudio(decodedWav);
const edited = doc.trim({ start: 0, end: 30 }).normalize();
```
*PlanEngine (non-destructive):*
```ts
import { PlanEngine } from "@suaveplan/audio-core";
const plan = new PlanEngine();
plan.trim(0, 30).normalize().fadeIn(0.5);
const compiled = plan.compile(doc);
```
*ProcessPipeline:*
```ts
import { ProcessPipeline } from "@suaveplan/audio-core";
const pipeline = new ProcessPipeline([gainStage, filterStage]);
const output = pipeline.processBlock(inputBlock);
```
**Constraints:**
- Universal — zero browser or Node-only APIs
- Subpath exports: `/document/document`, `/page/page`, `/plan/plan-engine`, `/plan/segment`, `/process/process-pipeline`, `/process/block-processor`, `/registry/op-registry`, `/stream/stream`, `/render/render`, `/stats/stats`, `/error-codes`, `/analysis`, `/analysis/analysis-op`
- `AudioDocument` uses paged memory (`PAGE_SIZE` default) for efficient large-file handling
- `PlanEngine` is non-destructive — edits compile into a transform plan, not in-place mutation
- `AnalysisOp` is the standard interface all analysis plugins implement
**Checklist:**
- [ ] `AudioDocument` supports append, splice, trim, insert without in-place mutation
- [ ] `PlanEngine.compile()` returns a transform plan that can be rendered lazily
- [ ] `ProcessPipeline` processes audio in `BLOCK_SIZE` chunks
- [ ] `OpRegistry` registers and resolves audio operations by name
- [ ] `createAnalysisOp()` produces an analysis wrapper compatible with all analysis plugins

View File

@@ -0,0 +1,52 @@
---
name: sp-audio-decode-wav
description: Universal WAV decoder wrapper over @audio/decode-wav — decodeWav, stream decoder, frozen DecodedAudio, AbortSignal, OTel, AUDIO_DECODE.WAV.* error codes.
---
# sp-audio-decode-wav
| Symbol | Kind | Responsibility |
|---|---|---|
| `decodeWav` | Function | Decodes a `Uint8Array` of WAV bytes into a frozen `DecodedAudio` object |
| `createWavStreamDecoder` | Function | Returns a `WavStreamDecoder` for incremental byte streams |
| `DecodedAudio` / `DecodedAudioSchema` | Type | Frozen, validated decoded-audio shape (sampleRate, channels, duration, data) |
| `inspectWavHeader` | Function | Validates WAV header and returns `InspectResult` (ok / corrupt / truncated / unsupported) |
| `AUDIO_DECODE_WAV_CODES` | Constant | Error-code registry (`AUDIO_DECODE.WAV.*`) via `@suaveplan/error` |
| `getInstruments` / `recordSuccess` / `recordError` | Function | OTel tracer, counters, and histograms for decode operations |
**File Structure:** `packages/media/audio/audio-decode-wav/src/{decode-wav,stream,otel,error-codes,types,utils}/`
**Sibling packages:**
- `@suaveplan/audio-core` — audio pipeline core
- `@suaveplan/audio-codecs` — codec registry
**Patterns:**
*Quick Start:*
```ts
import { decodeWav } from "@suaveplan/audio-decode-wav";
const audio = await decodeWav(wavBytes); // frozen DecodedAudio
```
*Stream decoding:*
```ts
import { createWavStreamDecoder } from "@suaveplan/audio-decode-wav/stream/stream";
const decoder = createWavStreamDecoder();
for await (const chunk of readableStream) {
decoder.push(chunk);
}
const audio = decoder.finalize();
```
**Constraints:**
- Wraps upstream `@audio/decode-wav` v1.2.0
- `DecodedAudio` is deeply frozen after validation against `DecodedAudioSchema`
- Honors `AbortSignal` for cancellation
- OTel instruments gated by provider availability
- Subpath exports: `/decode-wav/decode-wav`, `/stream/stream`, `/otel/otel`, `/error-codes/error-codes`, `/types/decoded-audio`, `/types/options`
**Checklist:**
- [ ] `decodeWav` returns a frozen `DecodedAudio` with validated schema
- [ ] `createWavStreamDecoder` handles incremental byte chunks
- [ ] `inspectWavHeader` detects corrupt, truncated, and unsupported files
- [ ] OTel counters increment on success/error

View File

@@ -0,0 +1,62 @@
---
name: sp-audio-engine-browser
description: Browser-tier TimelineAdapter — Web Audio rendering, AudioWorklet hosting, FX plugin createAudioNode integration, HTML5 fallback, offline render. For TimelineEngine.
---
# sp-audio-engine-browser
| Symbol | Kind | Responsibility |
|---|---|---|
| `BrowserTimelineAdapter` | Class | Browser-tier `TimelineAdapter` — connects `TimelineEngine` to Web Audio API |
| `WebAudioGraph` | Class | Manages `AudioNode` connections, snapshots, and node lifecycle |
| `AudioWorkletManager` | Class | Loads and manages `AudioWorklet` modules with schema-validated options |
| `BusOrchestrator` | Class | Orchestrates audio bus routing between tracks and FX |
| `createFxNode` | Function | Creates a Web Audio `AudioNode` from an `AudioFxPlugin` descriptor |
| `Html5AudioFallback` | Class | Fallback audio playback using HTML5 `<audio>` when Web Audio is unavailable |
| `renderOffline` | Function | Offline renders a timeline to an `AudioBuffer` without a real audio context |
| `decode` | Function | Decodes audio data in the browser (wraps `AudioContext.decodeAudioData`) |
| `AUDIO_ENGINE_BROWSER_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio/audio-engine-browser/src/{adapter,graph,worklet,fallback,offline,decode,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-engine-server` — server-tier adapter (file I/O)
- `@suaveplan/audio-timeline` — timeline engine
- `@suaveplan/audio-fx-plugin-base` — FX plugin base
- `@suaveplan/audio-transport` — transport sync
**Patterns:**
*Quick Start:*
```ts
import { BrowserTimelineAdapter } from "@suaveplan/audio-engine-browser";
const adapter = new BrowserTimelineAdapter(audioContext);
adapter.connect(timelineEngine);
```
*AudioWorklet:*
```ts
import { AudioWorkletManager } from "@suaveplan/audio-engine-browser/worklet/worklet-manager";
const manager = new AudioWorkletManager(audioContext);
await manager.loadModule("/worklets/my-processor.js");
```
*Offline render:*
```ts
import { renderOffline } from "@suaveplan/audio-engine-browser/offline/offline-render";
const buffer = await renderOffline({ duration: 30, sampleRate: 48000 }, timeline);
```
**Constraints:**
- Browser-only — depends on Web Audio API and `standardized-audio-context`
- Subpath exports: `/adapter/browser-adapter`, `/graph/web-audio-graph`, `/worklet/worklet-manager`, `/fallback/html5-fallback`, `/offline/offline-render`, `/decode`, `/error-codes`
- `AudioWorkletManager` validates options via `WorkletManagerOptionsSchema`
- `Html5AudioFallback` used when Web Audio is unsupported or blocked
- OTel instrumented for graph operations and worklet loads
**Checklist:**
- [ ] `BrowserTimelineAdapter` connects timeline to Web Audio output
- [ ] `WebAudioGraph` correctly wires nodes and produces serializable snapshots
- [ ] `AudioWorkletManager` loads and unloads worklet modules
- [ ] `renderOffline` produces an `AudioBuffer` without real audio hardware
- [ ] `Html5AudioFallback` plays audio when Web Audio is unavailable

View File

@@ -0,0 +1,61 @@
---
name: sp-audio-engine-server
description: Server-tier audio I/O — FileSourceAdapter, FileSinkAdapter, ServerTimelineAdapter for offline rendering. File I/O + plugin chain. Node >=22.
---
# sp-audio-engine-server
| Symbol | Kind | Responsibility |
|---|---|---|
| `ServerTimelineAdapter` | Class | Server-tier `TimelineAdapter` — connects `TimelineEngine` to file output |
| `FileSourceAdapter` | Class | Reads audio files from disk into `AudioDocument` streams |
| `FileSinkAdapter` | Class | Writes processed audio to disk in various formats |
| `PluginChain` | Class | Server-side FX plugin chain with `BusRegistry` and sidechain support |
| `DefaultBusRegistry` | Class | Default implementation of `BusRegistry` for plugin routing |
| `BusOrchestrator` | Class | Orchestrates audio bus routing in server context |
| `AUDIO_ENGINE_SERVER_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio/audio-engine-server/src/{adapter,source,sink,fx,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-engine-browser` — browser-tier adapter (Web Audio)
- `@suaveplan/audio-timeline` — timeline engine
- `@suaveplan/audio-codecs` — codec support for file I/O
- `@suaveplan/audio-transport` — transport sync
**Patterns:**
*Quick Start:*
```ts
import { ServerTimelineAdapter } from "@suaveplan/audio-engine-server";
const adapter = new ServerTimelineAdapter({ sampleRate: 48000 });
adapter.connect(timelineEngine);
await adapter.renderToFile("./output.wav");
```
*File source:*
```ts
import { FileSourceAdapter } from "@suaveplan/audio-engine-server/source/file-source";
const source = new FileSourceAdapter();
const doc = await source.load("./input.wav");
```
*Plugin chain:*
```ts
import { PluginChain } from "@suaveplan/audio-engine-server/fx/plugin-chain";
const chain = new PluginChain([eqPlugin, compressorPlugin]);
```
**Constraints:**
- Node.js >=22 only — uses filesystem APIs
- Subpath exports: `/source/file-source`, `/sink/file-sink`, `/adapter/server-adapter`, `/fx/plugin-chain`, `/error-codes`
- `FileSourceAdapter` and `FileSinkAdapter` use `@suaveplan/audio-codecs` for format support
- `PluginChain` supports sidechain buses via `BusRegistry`
- OTel instrumented for file I/O and rendering operations
**Checklist:**
- [ ] `ServerTimelineAdapter` renders timeline to a file on disk
- [ ] `FileSourceAdapter` loads audio files into `AudioDocument` streams
- [ ] `FileSinkAdapter` writes audio in multiple formats
- [ ] `PluginChain` processes audio through FX with bus routing
- [ ] Sidechain buses route audio between plugins correctly

View File

@@ -0,0 +1,45 @@
---
name: sp-audio-fingerprint
description: Python-tier audio fingerprinting service — librosa chromagram + scipy peak detection + constellation hashing + Jaccard matching. FastAPI backend + typed TypeScript HTTP client.
---
# sp-audio-fingerprint
| Symbol | Kind | Responsibility |
|---|---|---|
| `PythonAudioService` | Class | Typed HTTP client for the FastAPI fingerprinting service (browser/Node) |
| `AudioFingerprinter` | Class | Python core implementing chromagram-based Shazam-style fingerprinting |
| `FingerprintRequest` / `FingerprintResponse` | Type | Request/response shapes for fingerprint generation |
| `MatchRequest` / `MatchResponse` | Type | Request/response shapes for Jaccard similarity matching |
**File Structure:** `packages/media/audio/audio-fingerprint/src/{api,client,types}/` (+ Python backend)
**Sibling packages:**
- `@suaveplan/react-audio-fingerprint` — React UI surface (browser tier)
**Patterns:**
*Quick Start (TypeScript client):*
```ts
import { PythonAudioService } from "@suaveplan/audio-fingerprint/client";
const service = new PythonAudioService({ baseUrl: "http://localhost:8000" });
const fp = await service.fingerprint(audioBuffer);
const match = await service.match(fp);
```
*Quick Start (Python backend):*
```bash
micromamba run -n suaveplan-audio-fingerprint uvicorn src.api.app:app --host 0.0.0.0 --port 8000
```
**Constraints:**
- Dual-runtime: Python FastAPI backend + TypeScript HTTP client
- Python env managed via micromamba (`environment.yml` + `pyproject.toml`)
- Subpath exports: `/client`, `/client/python-audio-service`, `/types`
- High-accuracy backend behind `@suaveplan/react-audio-fingerprint`
**Checklist:**
- [ ] `PythonAudioService.fingerprint()` sends audio and returns a fingerprint
- [ ] `PythonAudioService.match()` returns Jaccard similarity scores
- [ ] FastAPI health endpoint responds at `/health`
- [ ] Python tests pass via `pytest`

View File

@@ -0,0 +1,43 @@
---
name: sp-audio-fx-plugin-audio-analyzer
description: Real-time audio analysis FX plugin — beat, tempo, key, pitch, spectral features. Emits analysis updates via callback. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-audio-analyzer
| Symbol | Kind | Responsibility |
|---|---|---|
| `AudioAnalyzerPlugin` | Class | Real-time analysis plugin extending `AbstractAudioFxPlugin` |
| `AudioAnalyzerPluginOptions` | Type | Configurable analysis targets (beat, tempo, key, pitch, spectral) |
| `AnalysisUpdate` | Type | Emitted analysis result with beat position, energy, tempo, key, pitch |
| `AudioAnalyzerPreset` / `AudioAnalyzerPresetData` | Type | Serializable preset for analysis configuration |
| `AudioAnalyzerPluginOptionsSchema` | Schema | Zod schema for plugin options validation |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-audio-analyzer/src/{audio-analyzer,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-analysis-beat` — beat detection algorithm
- `@suaveplan/audio-analysis-pitch` — pitch detection algorithm
- `@suaveplan/audio-analysis-spectrum` — spectral analysis algorithm
**Patterns:**
*Quick Start:*
```ts
import { AudioAnalyzerPlugin } from "@suaveplan/audio-fx-plugin-audio-analyzer";
const analyzer = new AudioAnalyzerPlugin({ onAnalysis: (update) => console.log(update.bpm) });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Analysis runs per-block during `process()` callback
- `AnalysisCallback` receives `AnalysisUpdate` with all requested features
- Preset schema validates analysis target selection
- Zero audio output — analysis plugin is a pass-through or sink
**Checklist:**
- [ ] `AudioAnalyzerPlugin` extends `AbstractAudioFxPlugin`
- [ ] `process()` emits `AnalysisUpdate` with beat, tempo, key, pitch, and spectral features
- [ ] Plugin options schema validates analysis target selection
- [ ] Preset round-trips through JSON without data loss

View File

@@ -0,0 +1,44 @@
---
name: sp-audio-fx-plugin-audio-player
description: Audio player FX plugin — loads and plays audio documents within a plugin chain. Transport-synced playback with seek and loop. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-audio-player
| Symbol | Kind | Responsibility |
|---|---|---|
| `AudioPlayerPlugin` | Class | Player plugin extending `AbstractAudioFxPlugin` — loads and streams audio |
| `AudioPlayerLoadOptions` | Type | Load configuration (document, start offset, loop) |
| `AudioPlayerState` | Type | Runtime state: idle, loading, playing, paused, error |
| `AudioPlayerPresetData` | Type | Serializable preset with load options and default gain |
| `AudioPlayerPluginOptionsSchema` | Schema | Zod schema for plugin options |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-audio-player/src/{audio-player,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-core` — audio document model
- `@suaveplan/audio-transport` — transport sync
**Patterns:**
*Quick Start:*
```ts
import { AudioPlayerPlugin } from "@suaveplan/audio-fx-plugin-audio-player";
const player = new AudioPlayerPlugin();
await player.load({ document: drumLoop, startOffset: 0, loop: true });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Playback is transport-synced via `ChainContext` transport position
- Supports seek, loop, and gain control
- `AudioPlayerState` reflects current playback status
- Preset schema validates load options and gain
**Checklist:**
- [ ] `AudioPlayerPlugin` extends `AbstractAudioFxPlugin`
- [ ] `load()` accepts an `AudioDocument` and playback options
- [ ] Playback syncs to transport position via `ChainContext`
- [ ] Loop and seek operations work within the plugin chain
- [ ] Preset round-trips through JSON without data loss

View File

@@ -0,0 +1,42 @@
---
name: sp-audio-fx-plugin-auto-filter
description: Auto-filter FX plugin — LFO-modulated filter sweep (lowpass/bandpass/highpass) with rate, depth, and resonance. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-auto-filter
| Symbol | Kind | Responsibility |
|---|---|---|
| `AutoFilterPlugin` | Class | Auto-filter FX plugin extending `AbstractAudioFxPlugin` |
| `AutoFilterPluginOptions` | Type | Filter type, LFO rate, depth, resonance, mix |
| `AutoFilterPresetDataSchema` | Schema | Zod schema for serialisable preset state |
| `AutoFilterPluginOptionsSchema` | Schema | Zod schema for plugin options validation |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-auto-filter/src/{auto-filter,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-filter` — static filter plugin
**Patterns:**
*Quick Start:*
```ts
import { AutoFilterPlugin } from "@suaveplan/audio-fx-plugin-auto-filter";
const filter = new AutoFilterPlugin({ type: "lowpass", rate: 0.5, depth: 0.8, resonance: 2 });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- LFO modulates filter cutoff frequency per-block
- Supports lowpass, bandpass, and highpass filter types
- Rate syncs to transport BPM when `sync: true`
- Mix control for dry/wet blend
- Preset schema validates all parameters
**Checklist:**
- [ ] `AutoFilterPlugin` extends `AbstractAudioFxPlugin`
- [ ] LFO modulates cutoff at the configured rate
- [ ] Filter type selectable: lowpass, bandpass, highpass
- [ ] Rate syncs to transport BPM when enabled
- [ ] Mix control blends dry and wet signals

View File

@@ -0,0 +1,69 @@
---
name: sp-audio-fx-plugin-base
description: Foundation for the audio-fx-plugin-* family — AudioFxPlugin contract, PluginChain, ChainContext, BusRegistry, ParameterSchedule, PresetSnapshot, biquad utils. Browser/server/worklet universal.
---
# sp-audio-fx-plugin-base
| Symbol | Kind | Responsibility |
|---|---|---|
| `AbstractAudioFxPlugin` | Class | Base class for all audio FX plugins — handles lifecycle, parameters, and presets |
| `AudioFxPlugin` | Interface | Core contract: `process(input, output, context)`, parameters, metadata, capabilities |
| `PluginChain` | Class | Chains multiple FX plugins with IO validation and event bus |
| `ChainContextFactory` | Function | Creates a `ChainContext` with ambient state, transport, and bus registry |
| `BusRegistry` | Interface | Audio bus routing for sidechain and multi-channel plugins |
| `ParameterSchedule` | Class | Schedules parameter changes with curves (linear, exponential, step) |
| `PresetSnapshot` / `PresetSnapshotSchema` | Type | Zod schema for serialisable plugin preset state |
| `computeBiquad` / `identityCoeffs` | Function | Biquad filter coefficient computation |
| `validateChainIo` | Function | Validates that a plugin chain has compatible input/output formats |
| `isSourcePlugin` | Function | Type guard for source/generator plugins |
| `AUDIO_FX_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-base/src/{base,chain,registry,biquad,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-reverb` — reverb plugin
- `@suaveplan/audio-fx-plugin-delay` — delay plugin
- `@suaveplan/audio-fx-plugin-compressor` — compressor plugin
- `@suaveplan/audio-fx-plugin-eq` — EQ plugin
- `@suaveplan/audio-core` — audio document model
- `@suaveplan/audio-transport` — transport sync
**Patterns:**
*Quick Start (plugin implementation):*
```ts
import { AbstractAudioFxPlugin } from "@suaveplan/audio-fx-plugin-base";
class MyPlugin extends AbstractAudioFxPlugin {
process(input, output, context) { /* ... */ }
}
```
*Plugin chain:*
```ts
import { PluginChain } from "@suaveplan/audio-fx-plugin-base/chain";
const chain = new PluginChain([eq, compressor, reverb]);
chain.processBlock(input, output, context);
```
*Preset:*
```ts
import { PresetSnapshotSchema } from "@suaveplan/audio-fx-plugin-base";
const preset = PresetSnapshotSchema.parse(json);
plugin.loadPreset(preset);
```
**Constraints:**
- Universal — runs in browser, server, offline, and AudioWorklet contexts
- Subpath exports: `/base`, `/chain`, `/registry`, `/biquad`, `/error-codes`
- `PluginCapabilities` describes supported channel layouts, sample rates, and IO formats
- `ChainContext` carries ambient state (transport position, BPM, track state)
- All presets are versioned and validated against `PresetSnapshotSchema`
- Biquad helpers support lowpass, highpass, bandpass, peaking, lowshelf, highshelf, notch, allpass
**Checklist:**
- [ ] `AbstractAudioFxPlugin` base handles parameter changes and preset loading
- [ ] `PluginChain` validates IO compatibility between linked plugins
- [ ] `ParameterSchedule` interpolates parameter values along curves
- [ ] `PresetSnapshot` round-trips through JSON without data loss
- [ ] `computeBiquad` produces stable coefficients for all filter types

View File

@@ -0,0 +1,41 @@
---
name: sp-audio-fx-plugin-chorus
description: Chorus FX plugin — modulated short delay with multiple voices for thickening and shimmer. Rate, depth, voices, mix. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-chorus
| Symbol | Kind | Responsibility |
|---|---|---|
| `ChorusPlugin` | Class | Chorus FX plugin extending `AbstractAudioFxPlugin` |
| `ChorusPluginOptions` | Type | Rate, depth, number of voices, delay time, mix, feedback |
| `ChorusPluginOptionsSchema` | Schema | Zod schema for options validation |
| `ChorusPresetDataSchema` | Schema | Zod schema for serialisable preset |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-chorus/src/{chorus,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-flanger` — flanger plugin (similar modulated delay)
**Patterns:**
*Quick Start:*
```ts
import { ChorusPlugin } from "@suaveplan/audio-fx-plugin-chorus";
const chorus = new ChorusPlugin({ rate: 0.5, depth: 0.6, voices: 3, mix: 0.5 });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- LFO modulates delay time per voice with phase offset
- Multiple voices (typically 2–4) create thickening effect
- Mix control for dry/wet blend
- Preset schema validates all parameters
**Checklist:**
- [ ] `ChorusPlugin` extends `AbstractAudioFxPlugin`
- [ ] LFO modulates delay time per voice
- [ ] Multiple voices with phase offsets create chorus effect
- [ ] Mix control blends dry and wet signals
- [ ] Preset round-trips through JSON without data loss

View File

@@ -0,0 +1,48 @@
---
name: sp-audio-fx-plugin-compressor
description: Compressor FX plugin — threshold, ratio, attack, release, knee, makeup gain. Sidechain input support. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-compressor
| Symbol | Kind | Responsibility |
|---|---|---|
| `CompressorPlugin` | Class | Compressor FX plugin extending `AbstractAudioFxPlugin` |
| `CompressorPluginOptions` | Type | Threshold, ratio, attack, release, knee, makeup gain, sidechain |
| `CompressorPluginOptionsSchema` | Schema | Zod schema for options validation |
| `CompressorPresetDataSchema` | Schema | Zod schema for serialisable preset |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-compressor/src/{compressor,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-limiter` — limiter plugin
- `@suaveplan/audio-fx-plugin-gate` — noise gate plugin
**Patterns:**
*Quick Start:*
```ts
import { CompressorPlugin } from "@suaveplan/audio-fx-plugin-compressor";
const comp = new CompressorPlugin({ threshold: -18, ratio: 4, attack: 5, release: 50 });
```
*Sidechain:*
```ts
const comp = new CompressorPlugin({ threshold: -20, ratio: 10, sidechainBus: "kick" });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Envelope follower with configurable attack and release times (ms)
- Soft/hard knee options
- Sidechain input via `BusRegistry` for ducking effects
- Makeup gain compensates for level reduction
- Preset schema validates all parameters
**Checklist:**
- [ ] `CompressorPlugin` extends `AbstractAudioFxPlugin`
- [ ] Gain reduction follows threshold and ratio settings
- [ ] Attack and release envelope times are accurate
- [ ] Sidechain input triggers compression from external bus
- [ ] Makeup gain restores perceived loudness

View File

@@ -0,0 +1,47 @@
---
name: sp-audio-fx-plugin-delay
description: Delay FX plugin — tempo-synced or free-time delay with feedback, filter, ping-pong stereo, and mix. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-delay
| Symbol | Kind | Responsibility |
|---|---|---|
| `DelayPlugin` | Class | Delay FX plugin extending `AbstractAudioFxPlugin` |
| `DelayPluginOptions` | Type | Delay time, feedback, filter type/cutoff, ping-pong, mix, sync |
| `DelayPluginOptionsSchema` | Schema | Zod schema for options validation |
| `DelayPresetDataSchema` | Schema | Zod schema for serialisable preset |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-delay/src/{delay,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-reverb` — reverb plugin
**Patterns:**
*Quick Start:*
```ts
import { DelayPlugin } from "@suaveplan/audio-fx-plugin-delay";
const delay = new DelayPlugin({ time: 250, feedback: 0.4, mix: 0.3 });
```
*Tempo-synced:*
```ts
const delay = new DelayPlugin({ sync: true, noteValue: "1/8", feedback: 0.5 });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Delay time in ms or tempo-synced note values
- Feedback with optional lowpass/highpass filter in feedback path
- Ping-pong mode alternates between left and right channels
- Mix control for dry/wet blend
- Preset schema validates all parameters
**Checklist:**
- [ ] `DelayPlugin` extends `AbstractAudioFxPlugin`
- [ ] Delay time accurate in both free and synced modes
- [ ] Feedback loop includes optional filter
- [ ] Ping-pong mode creates stereo alternation
- [ ] Preset round-trips through JSON without data loss

View File

@@ -0,0 +1,44 @@
---
name: sp-audio-fx-plugin-distortion
description: Distortion FX plugin — soft clip, hard clip, tube, fuzz modes with drive, tone, and mix. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-distortion
| Symbol | Kind | Responsibility |
|---|---|---|
| `DistortionPlugin` | Class | Distortion FX plugin extending `AbstractAudioFxPlugin` |
| `DistortionPluginOptions` | Type | Mode, drive, tone, mix, output gain |
| `DistortionMode` | Type | `"softClip"`, `"hardClip"`, `"tube"`, `"fuzz"` |
| `DistortionModeSchema` | Schema | Zod schema for mode validation |
| `DistortionPluginOptionsSchema` | Schema | Zod schema for options validation |
| `DistortionPresetDataSchema` | Schema | Zod schema for serialisable preset |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-distortion/src/{distortion,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-eq` — tone-shaping EQ
**Patterns:**
*Quick Start:*
```ts
import { DistortionPlugin } from "@suaveplan/audio-fx-plugin-distortion";
const dist = new DistortionPlugin({ mode: "tube", drive: 0.7, tone: 0.5, mix: 0.3 });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Four distortion modes: soft clip, hard clip, tube emulation, fuzz
- Drive controls input gain before distortion stage
- Tone is a post-distortion tilt EQ (bright/dark)
- Mix control for dry/wet blend
- Preset schema validates mode and all parameters
**Checklist:**
- [ ] `DistortionPlugin` extends `AbstractAudioFxPlugin`
- [ ] All four modes produce distinct distortion characters
- [ ] Drive increases harmonic content before clipping
- [ ] Tone control shapes post-distortion brightness
- [ ] Preset round-trips through JSON without data loss

View File

@@ -0,0 +1,52 @@
---
name: sp-audio-fx-plugin-eq
description: Parametric/shelving/high-pass/low-pass EQ plugin — multi-band biquad, sample-accurate parameter automation, preset round-tripping. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-eq
| Symbol | Kind | Responsibility |
|---|---|---|
| `EQPlugin` | Class | Parametric EQ plugin extending `AbstractAudioFxPlugin` |
| `EQBand` | Type | Single band: type, frequency, gain, Q |
| `EQBandType` | Type | `"peaking"`, `"lowShelf"`, `"highShelf"`, `"lowPass"`, `"highPass"`, `"notch"`, `"bandPass"` |
| `EQ_PRESETS` | Constant | Built-in presets (flat, vocal boost, bass cut, etc.) |
| `EQPluginOptionsSchema` | Schema | Zod schema for options validation |
| `EQPresetDataSchema` | Schema | Zod schema for serialisable preset |
| `computeBiquad` | Function | Biquad coefficient computation (re-exported from base) |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-eq/src/{eq,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-filter` — single-filter plugin
**Patterns:**
*Quick Start:*
```ts
import { EQPlugin, EQ_PRESETS } from "@suaveplan/audio-fx-plugin-eq";
const eq = new EQPlugin(EQ_PRESETS.vocalBoost);
```
*Custom bands:*
```ts
const eq = new EQPlugin({ bands: [
{ type: "highPass", freq: 80, gain: 0, q: 0.7 },
{ type: "peaking", freq: 3000, gain: 3, q: 1.2 },
] });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Multi-band biquad implementation with per-band gain, frequency, Q
- Sample-accurate parameter automation via `ParameterSchedule`
- `EQ_BAND_COUNT` defines maximum bands
- Preset round-trips through JSON without data loss
**Checklist:**
- [ ] `EQPlugin` extends `AbstractAudioFxPlugin`
- [ ] All 7 band types produce correct biquad coefficients
- [ ] Multi-band processing sums individual band outputs
- [ ] Sample-accurate parameter automation works per-band
- [ ] Built-in presets load and sound correct

View File

@@ -0,0 +1,44 @@
---
name: sp-audio-fx-plugin-filter
description: Static filter FX plugin — lowpass, highpass, bandpass, notch, allpass with cutoff, resonance, and slope. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-filter
| Symbol | Kind | Responsibility |
|---|---|---|
| `FilterPlugin` | Class | Static filter FX plugin extending `AbstractAudioFxPlugin` |
| `FilterMode` | Type | `"lowpass"`, `"highpass"`, `"bandpass"`, `"notch"`, `"allpass"` |
| `FilterPluginOptions` | Type | Mode, cutoff, resonance, slope (dB/octave) |
| `FilterModeSchema` | Schema | Zod schema for mode validation |
| `FilterPluginOptionsSchema` | Schema | Zod schema for options validation |
| `FilterPresetDataSchema` | Schema | Zod schema for serialisable preset |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-filter/src/{filter,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-eq` — parametric EQ plugin
- `@suaveplan/audio-fx-plugin-auto-filter` — LFO-modulated filter
**Patterns:**
*Quick Start:*
```ts
import { FilterPlugin } from "@suaveplan/audio-fx-plugin-filter";
const filter = new FilterPlugin({ mode: "lowpass", cutoff: 2000, resonance: 1 });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Static cutoff (no modulation) — use `AutoFilterPlugin` for LFO sweeps
- Resonance boosts frequencies near cutoff
- Slope options: 12dB/octave or 24dB/octave
- Preset schema validates mode and all parameters
**Checklist:**
- [ ] `FilterPlugin` extends `AbstractAudioFxPlugin`
- [ ] All five filter modes produce correct frequency responses
- [ ] Resonance peaks near cutoff frequency
- [ ] Slope controls filter steepness (12dB vs 24dB)
- [ ] Preset round-trips through JSON without data loss

View File

@@ -0,0 +1,42 @@
---
name: sp-audio-fx-plugin-flanger
description: Flanger FX plugin — short modulated delay with feedback for jet-plane sweep effect. Rate, depth, feedback, mix. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-flanger
| Symbol | Kind | Responsibility |
|---|---|---|
| `FlangerPlugin` | Class | Flanger FX plugin extending `AbstractAudioFxPlugin` |
| `FlangerPluginOptions` | Type | Rate, depth, feedback, delay range, mix |
| `FlangerPluginOptionsSchema` | Schema | Zod schema for options validation |
| `FlangerPresetDataSchema` | Schema | Zod schema for serialisable preset |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-flanger/src/{flanger,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-chorus` — chorus plugin (longer delay, no feedback)
**Patterns:**
*Quick Start:*
```ts
import { FlangerPlugin } from "@suaveplan/audio-fx-plugin-flanger";
const flanger = new FlangerPlugin({ rate: 0.3, depth: 0.7, feedback: 0.6, mix: 0.5 });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Very short delay (0.5–15ms) modulated by LFO
- Feedback creates resonant comb-filter peaks
- Rate syncs to transport BPM when `sync: true`
- Mix control for dry/wet blend
- Preset schema validates all parameters
**Checklist:**
- [ ] `FlangerPlugin` extends `AbstractAudioFxPlugin`
- [ ] LFO modulates delay in the 0.5–15ms range
- [ ] Feedback creates characteristic comb-filter resonances
- [ ] Rate syncs to transport BPM when enabled
- [ ] Preset round-trips through JSON without data loss

View File

@@ -0,0 +1,56 @@
---
name: sp-audio-fx-plugin-game-audio
description: Game audio FX plugin — spatial audio sources, listener orientation, proximity voice chat, music layer mixing, game state-driven parameters. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-game-audio
| Symbol | Kind | Responsibility |
|---|---|---|
| `GameAudioPlugin` | Class | Game audio plugin extending `AbstractAudioFxPlugin` |
| `GameAudioPluginOptions` | Type | Spatial audio config, voice chat, music layers, game state bindings |
| `SpatialAudioSource` | Type | 3D positioned audio source with attenuation and occlusion |
| `ListenerOrientation` | Type | Listener position and rotation in 3D space |
| `MusicLayer` | Type | Stems that crossfade based on game intensity |
| `ProximityVoiceChatConfig` | Type | Voice chat with distance-based attenuation |
| `GameState` | Type | Game state values that drive audio parameters |
| `GameAudioPluginOptionsSchema` | Schema | Zod schema for options validation |
| `GameAudioPresetDataSchema` | Schema | Zod schema for serialisable preset |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-game-audio/src/{game-audio,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-spatial` — spatial audio plugin
**Patterns:**
*Quick Start:*
```ts
import { GameAudioPlugin } from "@suaveplan/audio-fx-plugin-game-audio";
const gameAudio = new GameAudioPlugin({
spatialSources: [{ id: "engine", position: { x: 0, y: 0, z: 5 } }],
listener: { position: { x: 0, y: 0, z: 0 }, forward: { x: 0, y: 0, z: 1 } },
});
```
*Game state-driven:*
```ts
gameAudio.setGameState({ intensity: 0.8, health: 0.3 });
// music layers crossfade, spatial sources update
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Spatial audio uses distance attenuation and occlusion models
- Music layers crossfade based on game intensity value
- Proximity voice chat attenuates by distance between participants
- Game state updates drive parameter automation
- Preset schema validates 3D vectors and game state bindings
**Checklist:**
- [ ] `GameAudioPlugin` extends `AbstractAudioFxPlugin`
- [ ] Spatial sources attenuate by distance and respect occlusion
- [ ] Listener orientation affects 3D audio positioning
- [ ] Music layers crossfade smoothly with game intensity
- [ ] Proximity voice chat attenuates by participant distance

View File

@@ -0,0 +1,44 @@
---
name: sp-audio-fx-plugin-gate
description: Noise gate FX plugin — threshold, attack, hold, release, hysteresis, range. Silences audio below threshold. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-gate
| Symbol | Kind | Responsibility |
|---|---|---|
| `GatePlugin` | Class | Noise gate FX plugin extending `AbstractAudioFxPlugin` |
| `GatePluginOptions` | Type | Threshold, attack, hold, release, hysteresis, range |
| `GatePluginOptionsSchema` | Schema | Zod schema for options validation |
| `GatePresetDataSchema` | Schema | Zod schema for serialisable preset |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-gate/src/{gate,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-compressor` — compressor plugin
- `@suaveplan/audio-fx-plugin-expander` — expander plugin
**Patterns:**
*Quick Start:*
```ts
import { GatePlugin } from "@suaveplan/audio-fx-plugin-gate";
const gate = new GatePlugin({ threshold: -40, attack: 1, hold: 50, release: 100 });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Closes (mutes) when signal drops below threshold; opens when above
- Attack time for opening, release time for closing
- Hold time prevents rapid flutter near threshold
- Hysteresis prevents chatter (different open/close thresholds)
- Range controls how much attenuation is applied when closed
- Preset schema validates all parameters
**Checklist:**
- [ ] `GatePlugin` extends `AbstractAudioFxPlugin`
- [ ] Signal below threshold is attenuated by range amount
- [ ] Attack and release envelope times are accurate
- [ ] Hold time prevents rapid open/close flutter
- [ ] Hysteresis prevents chatter at threshold boundary

View File

@@ -0,0 +1,46 @@
---
name: sp-audio-fx-plugin-glados
description: GLaDOS-style voice effect plugin — ring modulation, pitch shifting, formant filtering, chorus, delay for robotic/AI voice character. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-glados
| Symbol | Kind | Responsibility |
|---|---|---|
| `GLaDOSPlugin` | Class | GLaDOS-style voice effect plugin extending `AbstractAudioFxPlugin` |
| `GLaDOSConfig` | Type | Ring mod frequency, pitch shift, formant shift, chorus rate, delay time |
| `GLaDOSPreset` | Type | Named preset configuration |
| `getGLaDOSPresets` | Function | Returns built-in presets (still-alive, turret, core) |
| `GLaDOSPluginOptionsSchema` | Schema | Zod schema for options validation |
| `GLaDOSPresetSchema` | Schema | Zod schema for preset validation |
| `GLaDOSPresetDataSchema` | Schema | Zod schema for serialisable preset |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-glados/src/{glados,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-pitch-shifter` — pitch shift component
**Patterns:**
*Quick Start:*
```ts
import { GLaDOSPlugin, getGLaDOSPresets } from "@suaveplan/audio-fx-plugin-glados";
const glados = new GLaDOSPlugin(getGLaDOSPresets().stillAlive);
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Ring modulation creates metallic harmonic content
- Pitch shifting detunes the voice up or down
- Formant filtering shifts vocal character without changing pitch
- Chorus and delay add spatial depth
- Built-in presets: "stillAlive", "turret", "core"
- Preset schema validates all effect parameters
**Checklist:**
- [ ] `GLaDOSPlugin` extends `AbstractAudioFxPlugin`
- [ ] Ring modulation produces metallic harmonics
- [ ] Pitch shift changes pitch without altering speed
- [ ] Formant shift changes vocal character independently of pitch
- [ ] Built-in presets load correctly and sound distinct

View File

@@ -0,0 +1,43 @@
---
name: sp-audio-fx-plugin-limiter
description: Brickwall limiter FX plugin — threshold, release, lookahead, true-peak detection. Prevents clipping at output stage. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-limiter
| Symbol | Kind | Responsibility |
|---|---|---|
| `LimiterPlugin` | Class | Brickwall limiter FX plugin extending `AbstractAudioFxPlugin` |
| `LimiterPluginOptions` | Type | Threshold, release, lookahead, true-peak, ceiling |
| `LimiterPluginOptionsSchema` | Schema | Zod schema for options validation |
| `LimiterPresetDataSchema` | Schema | Zod schema for serialisable preset |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-limiter/src/{limiter,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-compressor` — compressor plugin
**Patterns:**
*Quick Start:*
```ts
import { LimiterPlugin } from "@suaveplan/audio-fx-plugin-limiter";
const limiter = new LimiterPlugin({ threshold: -1, release: 50, ceiling: -0.1 });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Infinite ratio — signal never exceeds threshold + ceiling
- Lookahead enables predictive gain reduction (smoother limiting)
- True-peak detection prevents inter-sample peaks
- Ceiling sets absolute maximum output level
- Typically placed at end of master bus chain
- Preset schema validates all parameters
**Checklist:**
- [ ] `LimiterPlugin` extends `AbstractAudioFxPlugin`
- [ ] Output never exceeds ceiling level
- [ ] Lookahead produces smoother gain reduction than zero-lookahead
- [ ] True-peak detection catches inter-sample overs
- [ ] Release time controls gain recovery speed

View File

@@ -0,0 +1,59 @@
---
name: sp-audio-fx-plugin-microphone
description: Microphone simulation FX plugin — models frequency response, distortion, dynamics, noise, resonance, wow/flutter of vintage mics. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-microphone
| Symbol | Kind | Responsibility |
|---|---|---|
| `MicrophoneSimulationPlugin` | Class | Mic simulation plugin extending `AbstractAudioFxPlugin` |
| `createMicrophonePlugin` | Function | Factory for `MicrophoneSimulationPlugin` |
| `MicrophoneConfig` | Type | Complete mic model configuration |
| `FrequencyControls` | Type | Frequency response curve (EQ profile) |
| `DistortionControls` | Type | Harmonic distortion amount and character |
| `DynamicsControls` | Type | Compression/saturation behavior |
| `NoiseControls` | Type | Self-noise and hum modeling |
| `ResonanceControls` | Type | Body resonance and proximity effect |
| `WowAndFlutterControls` | Type | Tape-style pitch instability |
| `MicrophoneSimulationPluginOptionsSchema` | Schema | Zod schema for options validation |
| `MicrophonePresetDataSchema` | Schema | Zod schema for serialisable preset |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-microphone/src/{microphone,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-eq` — EQ component
- `@suaveplan/audio-fx-plugin-compressor` — dynamics component
**Patterns:**
*Quick Start:*
```ts
import { MicrophoneSimulationPlugin } from "@suaveplan/audio-fx-plugin-microphone";
const mic = new MicrophoneSimulationPlugin({ model: "sm58" });
```
*Custom model:*
```ts
const mic = createMicrophonePlugin({
frequency: { lowCut: 80, presenceBoost: 4000 },
distortion: { amount: 0.1, character: "warm" },
noise: { selfNoise: -70, hum: 0 },
});
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Models vintage microphone characteristics: frequency response, distortion, noise
- Proximity effect boosts bass when source is close
- Wow and flutter simulate analog tape pitch instability
- Multiple UI control sections for parameter grouping
- Preset schema validates all mic model parameters
**Checklist:**
- [ ] `MicrophoneSimulationPlugin` extends `AbstractAudioFxPlugin`
- [ ] Frequency response matches modeled microphone profile
- [ ] Distortion increases harmonically with input level
- [ ] Proximity effect boosts low frequencies for close sources
- [ ] Noise floor and hum are configurable per model

View File

@@ -0,0 +1,55 @@
---
name: sp-audio-fx-plugin-mixer
description: Mixer FX plugin — multi-channel strip with gain, pan, mute, solo, sends, and VU metering. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-mixer
| Symbol | Kind | Responsibility |
|---|---|---|
| `MixerPlugin` | Class | Multi-channel mixer plugin extending `AbstractAudioFxPlugin` |
| `MixerChannel` | Type | Single channel strip: gain, pan, mute, solo, sends |
| `MixerChannelOptions` | Type | Per-channel configuration |
| `MixerVuSnapshot` | Type | VU meter levels per channel |
| `MixerPluginOptionsSchema` | Schema | Zod schema for options validation |
| `MixerPresetDataSchema` | Schema | Zod schema for serialisable preset |
| `MixerSendsSchema` | Schema | Zod schema for send bus configuration |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-mixer/src/{mixer,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-timeline` — timeline mixer integration
**Patterns:**
*Quick Start:*
```ts
import { MixerPlugin } from "@suaveplan/audio-fx-plugin-mixer";
const mixer = new MixerPlugin({ channels: [
{ name: "Drums", gain: 0.8, pan: 0 },
{ name: "Bass", gain: 0.9, pan: 0 },
] });
```
*Sends:*
```ts
const mixer = new MixerPlugin({ channels: [
{ name: "Vocals", gain: 1.0, sends: [{ bus: "reverb", amount: 0.3 }] },
] });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Per-channel gain, pan (linear or constant-power), mute, solo
- Sends route audio to effect buses with configurable amount
- VU metering returns level snapshots per channel
- Solo isolates selected channels (mutes all others)
- Preset schema validates channel configuration
**Checklist:**
- [ ] `MixerPlugin` extends `AbstractAudioFxPlugin`
- [ ] Per-channel gain and pan work independently
- [ ] Mute silences a channel; solo isolates it
- [ ] Sends route audio to external buses with level control
- [ ] VU snapshot returns accurate peak/RMS levels

View File

@@ -0,0 +1,44 @@
---
name: sp-audio-fx-plugin-phaser
description: Phaser FX plugin — allpass filter cascade with LFO modulation for sweeping notches. Rate, depth, stages, feedback, mix. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-phaser
| Symbol | Kind | Responsibility |
|---|---|---|
| `PhaserPlugin` | Class | Phaser FX plugin extending `AbstractAudioFxPlugin` |
| `PhaserPluginOptions` | Type | Rate, depth, stages, feedback, center frequency, mix |
| `PhaserPluginOptionsSchema` | Schema | Zod schema for options validation |
| `PhaserPresetDataSchema` | Schema | Zod schema for serialisable preset |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-phaser/src/{phaser,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-flanger` — flanger plugin (similar modulated delay)
**Patterns:**
*Quick Start:*
```ts
import { PhaserPlugin } from "@suaveplan/audio-fx-plugin-phaser";
const phaser = new PhaserPlugin({ rate: 0.4, depth: 0.6, stages: 6, feedback: 0.5, mix: 0.4 });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Cascade of allpass filters creates frequency notches
- LFO modulates center frequency of allpass chain
- More stages = more notches = richer effect
- Feedback routes output back to input for resonance
- Rate syncs to transport BPM when `sync: true`
- Mix control for dry/wet blend
- Preset schema validates all parameters
**Checklist:**
- [ ] `PhaserPlugin` extends `AbstractAudioFxPlugin`
- [ ] Allpass cascade creates frequency notches
- [ ] LFO sweeps notches through the spectrum
- [ ] Feedback increases resonance at notch frequencies
- [ ] More stages produce more notches

View File

@@ -0,0 +1,43 @@
---
name: sp-audio-fx-plugin-pitch-shifter
description: Pitch shifter FX plugin — time-stretching pitch change without speed alteration. Grain size, overlap, formant preservation. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-pitch-shifter
| Symbol | Kind | Responsibility |
|---|---|---|
| `PitchShifterPlugin` | Class | Pitch shifter FX plugin extending `AbstractAudioFxPlugin` |
| `PitchShifterPluginOptions` | Type | Semitones, grain size, overlap, formant preserve, mix |
| `PitchShifterPluginOptionsSchema` | Schema | Zod schema for options validation |
| `PitchShifterPresetDataSchema` | Schema | Zod schema for serialisable preset |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-pitch-shifter/src/{pitch-shifter,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-glados` — GLaDOS effect (uses pitch shift)
**Patterns:**
*Quick Start:*
```ts
import { PitchShifterPlugin } from "@suaveplan/audio-fx-plugin-pitch-shifter";
const shifter = new PitchShifterPlugin({ semitones: -7, grainSize: 80, overlap: 0.2 });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Grain-based time-stretching preserves playback speed
- Formant preservation maintains vocal character during shifts
- Larger grains = smoother sound, more latency
- Overlap controls crossfade between grains
- Mix control for dry/wet blend
- Preset schema validates all parameters
**Checklist:**
- [ ] `PitchShifterPlugin` extends `AbstractAudioFxPlugin`
- [ ] Pitch changes by specified semitones without speed change
- [ ] Formant preservation maintains vocal character
- [ ] Grain size tradeoff between quality and latency
- [ ] Overlap crossfade reduces grain boundary artifacts

View File

@@ -0,0 +1,66 @@
---
name: sp-audio-fx-plugin-reverb
description: Reverb FX plugin — algorithmic (Schroeder/Moorer) and convolution engines. Room size, decay, pre-delay, damping, diffusion. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-reverb
| Symbol | Kind | Responsibility |
|---|---|---|
| `ReverbPlugin` | Class | Reverb FX plugin extending `AbstractAudioFxPlugin` |
| `algorithmicStep` | Function | Algorithmic reverb processing step (Schroeder/Moorer) |
| `convolveStep` | Function | Convolution reverb processing step |
| `createChannelState` / `resetChannelState` | Function | Channel state management for algorithmic engine |
| `updateCombFeedbacks` | Function | Updates comb filter feedbacks based on decay time |
| `REVERB_PRESETS` | Constant | Built-in room presets (hall, room, plate, cathedral) |
| `REVERB_PRESET_NAMES` | Constant | Array of available preset names |
| `ReverbPluginOptionsSchema` | Schema | Zod schema for options validation |
| `ReverbPresetDataSchema` | Schema | Zod schema for serialisable preset |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-reverb/src/{reverb,engines,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-ir-core` — impulse response registry (convolution)
- `@suaveplan/audio-ir-loader` — IR loader (convolution)
**Patterns:**
*Quick Start:*
```ts
import { ReverbPlugin, REVERB_PRESETS } from "@suaveplan/audio-fx-plugin-reverb";
const reverb = new ReverbPlugin(REVERB_PRESETS.hall);
```
*Algorithmic:*
```ts
const reverb = new ReverbPlugin({
algorithm: "algorithmic",
roomSize: 0.8, decay: 2.5, preDelay: 30, damping: 0.5,
});
```
*Convolution:*
```ts
const reverb = new ReverbPlugin({
algorithm: "convolution",
impulseResponse: "ir://room/cathedral",
});
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Algorithmic engine: Schroeder/Moorer with comb + allpass filters
- Convolution engine: FFT-based IR convolution
- `ALLPASS_DELAYS_MS` and `COMB_DELAYS_MS` constants for algorithmic engine
- Pre-delay separates direct sound from reverb tail
- Damping simulates high-frequency air absorption
- Preset schema validates algorithm and all parameters
**Checklist:**
- [ ] `ReverbPlugin` extends `AbstractAudioFxPlugin`
- [ ] Algorithmic engine produces natural-sounding reverb tails
- [ ] Convolution engine loads and applies impulse responses
- [ ] Decay time controls reverb tail length
- [ ] Pre-delay separates direct sound from reflections
- [ ] Built-in presets cover common room types

View File

@@ -0,0 +1,60 @@
---
name: sp-audio-fx-plugin-sampler
description: Sampler FX plugin — trigger and playback audio samples with pitch, velocity, looping, and envelope control. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-sampler
| Symbol | Kind | Responsibility |
|---|---|---|
| `SamplerPlugin` | Class | Sampler FX plugin extending `AbstractAudioFxPlugin` |
| `SamplerLoadedSample` | Type | Loaded sample with buffer, loop points, root note |
| `SamplerLoadOptions` | Type | Sample loading configuration (url, loop, rootKey) |
| `SamplerPluginOptions` | Type | Global sampler settings (voices, polyphony, default envelope) |
| `SamplerPresetData` | Type | Serialisable preset with sample map and per-zone settings |
| `SamplerLoadOptionsSchema` | Schema | Zod schema for load options validation |
| `SamplerPluginOptionsSchema` | Schema | Zod schema for plugin options validation |
| `SamplerPresetDataSchema` | Schema | Zod schema for preset validation |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-sampler/src/{sampler,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-synthesizer` — synthesizer plugin (similar voice architecture)
**Patterns:**
*Quick Start:*
```ts
import { SamplerPlugin } from "@suaveplan/audio-fx-plugin-sampler";
const sampler = new SamplerPlugin();
await sampler.load("kick", { url: "/samples/kick.wav", rootKey: 36 });
sampler.trigger("kick", { note: 36, velocity: 100 });
```
*Multi-zone mapping:*
```ts
await sampler.load("piano", {
zones: [
{ range: [36, 48], url: "/samples/piano-low.wav", rootKey: 42 },
{ range: [49, 72], url: "/samples/piano-high.wav", rootKey: 60 },
],
});
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Per-sample pitch transposition based on root note vs played note
- Velocity-sensitive playback with configurable curves
- Loop points for sustained samples
- Envelope (ADSR) per voice
- Polyphonic voice stealing when max voices exceeded
- Preset schema validates sample maps and zone configurations
**Checklist:**
- [ ] `SamplerPlugin` extends `AbstractAudioFxPlugin`
- [ ] Samples load asynchronously and are addressable by name
- [ ] Pitch transposition follows root note mapping
- [ ] Velocity scales amplitude and optional filter cutoff
- [ ] Loop points sustain during note hold
- [ ] Voice stealing maintains oldest-voice-first or quietest-first policy

View File

@@ -0,0 +1,54 @@
---
name: sp-audio-fx-plugin-spatial
description: Spatial audio FX plugin — 3D positional audio with HRTF, distance attenuation, doppler, and orientation. Vec3 positions. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-spatial
| Symbol | Kind | Responsibility |
|---|---|---|
| `SpatialAudioPlugin` | Class | Spatial audio FX plugin extending `AbstractAudioFxPlugin` |
| `Vec3` | Type | 3D vector: x, y, z coordinates |
| `SpatialAudioPluginOptions` | Type | Listener position, source position, HRTF index, max distance, rolloff |
| `SpatialPresetData` | Type | Serialisable spatial configuration |
| `SpatialAudioPluginOptionsSchema` | Schema | Zod schema for options validation |
| `SpatialPresetDataSchema` | Schema | Zod schema for preset validation |
| `Vec3Schema` | Schema | Zod schema for 3D vector validation |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-spatial/src/{spatial,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-ir-core` — impulse response registry (HRTF data)
**Patterns:**
*Quick Start:*
```ts
import { SpatialAudioPlugin } from "@suaveplan/audio-fx-plugin-spatial";
const spatial = new SpatialAudioPlugin({
listenerPosition: { x: 0, y: 0, z: 0 },
sourcePosition: { x: 5, y: 0, z: -2 },
});
```
*Moving source:*
```ts
spatial.setSourcePosition({ x: 10, y: 2, z: 0 });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- HRTF-based binaural rendering for headphone spatialisation
- Distance attenuation with configurable rolloff model (linear, inverse, exponential)
- Doppler shift when source moves relative to listener
- Listener orientation (yaw, pitch, roll) affects perceived direction
- Max distance clamps attenuation
- Preset schema validates positions and spatial parameters
**Checklist:**
- [ ] `SpatialAudioPlugin` extends `AbstractAudioFxPlugin`
- [ ] HRTF convolution produces directional audio
- [ ] Distance attenuation follows chosen rolloff model
- [ ] Doppler shift updates with source velocity
- [ ] Listener orientation affects azimuth and elevation perception

View File

@@ -0,0 +1,75 @@
---
name: sp-audio-fx-plugin-spessasynth
description: SF2/SF3 SoundFont synthesizer plugin wrapping SpessaSynth. Multi-timbral, MIDI router, voice manager, parameter system, telemetry. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-spessasynth
| Symbol | Kind | Responsibility |
|---|---|---|
| `SpessaSynthPlugin` | Class | Main SoundFont synthesizer plugin extending `AbstractAudioFxPlugin` |
| `SpessaSynthInitOptions` | Type | Initialisation: SoundFont, sample rate, polyphony limit |
| `SpessaSynthPluginOptions` | Type | Plugin-level options for SpessaSynth engine |
| `SpessaSynthPresetData` | Type | Serialisable preset with bank/program assignments |
| `SpessaSynthPresetDataSchema` | Schema | Zod schema for preset validation |
| `loadSoundFont` | Function | Loads SF2/SF3 file into `LoadedSoundFont` |
| `MidiRouter` | Class | Routes MIDI messages to channels with program/bank changes |
| `VoiceManager` | Class | Polyphonic voice allocation, stealing, and lifecycle |
| `buildParameterDeclarations` | Function | Builds parameter ID map for channel controls |
| `channelParameterId` / `parseChannelParameterId` | Function | Encode/decode channel parameter IDs |
| `registerSpessaSynthPlugin` | Function | Registers plugin type with the FX system |
| `SPESSASYNTH_CODES` | Constant | Error codes via `defineCodes` |
| `SpessaSynthError` | Class | Typed error with SpessaSynth codes |
| `getActiveVoicesValue` / `recordProcessDuration` | Function | OpenTelemetry voice and timing metrics |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-spessasynth/src/{plugin,loader,midi-router,parameters,voice-manager,telemetry,error-codes,register,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-core` — audio engine primitives
- `@suaveplan/midi` — MIDI message types
**Patterns:**
*Quick Start:*
```ts
import { SpessaSynthPlugin, loadSoundFont } from "@suaveplan/audio-fx-plugin-spessasynth";
const sf = await loadSoundFont("/sounds/GeneralUser.sf2");
const synth = new SpessaSynthPlugin({ soundFont: sf, maxPolyphony: 128 });
```
*MIDI routing:*
```ts
import { MidiRouter } from "@suaveplan/audio-fx-plugin-spessasynth/midi-router";
const router = new MidiRouter(synth);
router.send({ type: "noteOn", channel: 0, note: 60, velocity: 100 });
```
*Voice management:*
```ts
import { VoiceManager, DEFAULT_MAX_POLYPHONY } from "@suaveplan/audio-fx-plugin-spessasynth/voice-manager";
```
*Registration:*
```ts
import { registerSpessaSynthPlugin } from "@suaveplan/audio-fx-plugin-spessasynth/register";
registerSpessaSynthPlugin(fxRegistry);
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Wraps `spessasynth_lib` and `spessasynth_core` (optional dependencies)
- Multi-timbral: 16 MIDI channels with independent bank/program
- Channel parameters encoded as IDs via `channelParameterId`
- Polyphony managed by `VoiceManager` with configurable max
- Telemetry records active voices and process duration via OpenTelemetry
- Subpath exports: `error-codes`, `loader`, `midi-router`, `parameters`, `plugin`, `register`, `telemetry`, `voice-manager`
**Checklist:**
- [ ] `SpessaSynthPlugin` extends `AbstractAudioFxPlugin`
- [ ] SF2/SF3 SoundFonts load and parse correctly
- [ ] MIDI router dispatches note on/off, CC, program change, pitch bend
- [ ] Voice manager respects `maxPolyphony` and steals voices gracefully
- [ ] Channel parameter IDs encode channel index + parameter kind
- [ ] Telemetry exports voice count and process duration metrics
- [ ] All subpath exports are declared in `package.json`

View File

@@ -0,0 +1,62 @@
---
name: sp-audio-fx-plugin-synthesizer
description: Synthesizer FX plugin — subtractive synthesis with oscillators, filters, envelopes, LFOs, and multiple waveforms. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-synthesizer
| Symbol | Kind | Responsibility |
|---|---|---|
| `SynthesizerPlugin` | Class | Subtractive synthesizer plugin extending `AbstractAudioFxPlugin` |
| `SynthesizerPluginOptions` | Type | Oscillators, filter, envelope, LFO, waveform selection |
| `SynthesizerPresetData` | Type | Serialisable synth preset with full voice configuration |
| `SynthesizerVoiceSnapshot` | Type | Runtime voice state for inspection |
| `SynthWaveform` | Type | Waveform type: sine, square, sawtooth, triangle, noise |
| `SynthesizerPluginOptionsSchema` | Schema | Zod schema for options validation |
| `SynthesizerPresetDataSchema` | Schema | Zod schema for preset validation |
| `SynthWaveformSchema` | Schema | Zod schema for waveform enum validation |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-synthesizer/src/{synthesizer,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-sampler` — sampler plugin (complementary)
**Patterns:**
*Quick Start:*
```ts
import { SynthesizerPlugin } from "@suaveplan/audio-fx-plugin-synthesizer";
const synth = new SynthesizerPlugin({
oscillators: [{ waveform: "sawtooth", detune: 5 }],
filter: { type: "lowpass", frequency: 800, resonance: 2 },
envelope: { attack: 0.01, decay: 0.3, sustain: 0.5, release: 0.5 },
});
synth.trigger({ note: 60, velocity: 100 });
```
*LFO modulation:*
```ts
const synth = new SynthesizerPlugin({
lfo: { waveform: "sine", rate: 5, depth: 200, target: "filter.frequency" },
});
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Subtractive synthesis: oscillators → filter → amplifier
- Multiple oscillators with detune and mixing
- Filter types: lowpass, highpass, bandpass, notch
- ADSR envelope on filter and amplifier
- LFO targets: pitch, filter frequency, amplitude
- Waveforms: sine, square, sawtooth, triangle, noise
- Polyphonic voice allocation with configurable max voices
- Preset schema validates complete voice architecture
**Checklist:**
- [ ] `SynthesizerPlugin` extends `AbstractAudioFxPlugin`
- [ ] Oscillators generate correct waveforms
- [ ] Filter responds to cutoff, resonance, and envelope modulation
- [ ] ADSR envelope shapes amplitude and filter over time
- [ ] LFO modulates target parameter at specified rate and depth
- [ ] Polyphony limit is respected with voice stealing

View File

@@ -0,0 +1,52 @@
---
name: sp-audio-fx-plugin-tremolo
description: Tremolo FX plugin — amplitude modulation with LFO. Rate, depth, waveform (sine/square/saw/triangle), phase, mix. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-tremolo
| Symbol | Kind | Responsibility |
|---|---|---|
| `TremoloPlugin` | Class | Tremolo FX plugin extending `AbstractAudioFxPlugin` |
| `TremoloPluginOptions` | Type | Rate, depth, waveform, phase, mix |
| `TremoloPresetData` | Type | Serialisable tremolo preset |
| `TremoloWaveform` | Type | LFO waveform: sine, square, sawtooth, triangle |
| `TremoloPluginOptionsSchema` | Schema | Zod schema for options validation |
| `TremoloPresetDataSchema` | Schema | Zod schema for preset validation |
| `TremoloWaveformSchema` | Schema | Zod schema for waveform enum validation |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-tremolo/src/{tremolo,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-phaser` — phaser plugin (similar LFO modulation)
**Patterns:**
*Quick Start:*
```ts
import { TremoloPlugin } from "@suaveplan/audio-fx-plugin-tremolo";
const tremolo = new TremoloPlugin({ rate: 5, depth: 0.7, waveform: "sine" });
```
*Square wave chop:*
```ts
const tremolo = new TremoloPlugin({ rate: 8, depth: 1.0, waveform: "square" });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Amplitude modulation by LFO at specified rate
- Depth controls modulation amount (0 = none, 1 = full silence)
- Waveform shapes the amplitude envelope: sine, square, sawtooth, triangle
- Phase offsets the LFO start point
- Rate syncs to transport BPM when `sync: true`
- Mix control for dry/wet blend
- Preset schema validates all parameters
**Checklist:**
- [ ] `TremoloPlugin` extends `AbstractAudioFxPlugin`
- [ ] LFO modulates amplitude at specified rate
- [ ] Depth scales from subtle to full chop
- [ ] Waveform shapes the modulation contour
- [ ] Phase offset shifts the LFO cycle start

View File

@@ -0,0 +1,48 @@
---
name: sp-audio-fx-plugin-widener
description: Stereo widener FX plugin — mid/side processing to expand or narrow stereo image. Width, delay, crossfeed. Part of audio-fx-plugin-* family.
---
# sp-audio-fx-plugin-widener
| Symbol | Kind | Responsibility |
|---|---|---|
| `StereoWidenerPlugin` | Class | Stereo widener FX plugin extending `AbstractAudioFxPlugin` |
| `StereoWidenerPluginOptions` | Type | Width factor, delay, crossfeed amount |
| `StereoWidenerPresetData` | Type | Serialisable widener preset |
| `StereoWidenerPluginOptionsSchema` | Schema | Zod schema for options validation |
| `StereoWidenerPresetDataSchema` | Schema | Zod schema for preset validation |
**File Structure:** `packages/media/audio-fx/audio-fx-plugin-widener/src/{widener,schemas}/`
**Sibling packages:**
- `@suaveplan/audio-fx-plugin-base` — FX plugin foundation
- `@suaveplan/audio-fx-plugin-spatial` — spatial audio (3D positioning)
**Patterns:**
*Quick Start:*
```ts
import { StereoWidenerPlugin } from "@suaveplan/audio-fx-plugin-widener";
const widener = new StereoWidenerPlugin({ width: 1.5 });
```
*Narrow to mono:*
```ts
const widener = new StereoWidenerPlugin({ width: 0 });
```
**Constraints:**
- Extends `AbstractAudioFxPlugin` from `@suaveplan/audio-fx-plugin-base`
- Mid/side processing: width scales the side channel
- Width > 1 expands stereo image; width < 1 narrows it; width = 0 = mono
- Optional delay between channels for Haas effect widening
- Crossfeed blends channels to prevent phase issues
- Preset schema validates width, delay, and crossfeed parameters
**Checklist:**
- [ ] `StereoWidenerPlugin` extends `AbstractAudioFxPlugin`
- [ ] Width scales the side channel in mid/side processing
- [ ] Width = 0 produces true mono output
- [ ] Delay option creates Haas-effect widening
- [ ] Crossfeed prevents excessive phase cancellation

View File

@@ -0,0 +1,54 @@
---
name: sp-audio-ir-core
description: Foundation contract for the audio-ir family — ImpulseResponse/IrManifest Zod schemas, in-memory IrRegistry, AUDIO_IR.* error codes, OTel surface. Zero audio runtime deps.
---
# sp-audio-ir-core
| Symbol | Kind | Responsibility |
|---|---|---|
| `createIrRegistry` | Function | Creates an in-memory `IrRegistry` for impulse-response lookup and registration |
| `IrRegistry` | Interface | Lookup and registration surface for impulse responses by family/ID |
| `registerManifest` | Function | Registers an `IrManifest` into the global registry |
| `ImpulseResponse` / `ImpulseResponseSchema` | Type | Zod schema for an IR entry (name, uri, duration, sampleRate) |
| `IrManifest` / `IrManifestSchema` | Type | Zod schema for a manifest (slug, entries) |
| `IrManifestEntry` / `IrManifestEntrySchema` | Type | Zod schema for individual manifest entries |
| `createUriPluginCollector` | Function | Creates a `UriPluginCollector` for bundle-registration integration |
| `AUDIO_IR_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio-ir/audio-ir-core/src/{types,registry,registrar,error-codes,otel}/`
**Sibling packages:**
- `@suaveplan/audio-ir-loader` — IR loader (resolves URIs to audio buffers)
- `@suaveplan/audio-ir-room-plugin-club` — content package (club IRs)
- `@suaveplan/audio-ir-room-plugin-pool` — content package (pool IRs)
- `@suaveplan/storage-uri` — URI resolution
- `@suaveplan/storage-uri-plugin-bundle` — bundle registration plugin
**Patterns:**
*Quick Start:*
```ts
import { createIrRegistry, registerManifest } from "@suaveplan/audio-ir-core";
const registry = createIrRegistry();
registerManifest({ slug: "my-irs", entries: [{ id: "hall", name: "Concert Hall", uri: "bundle://my-irs/hall.wav" }] });
const ir = registry.lookup("room", "hall");
```
*URI plugin collector:*
```ts
import { createUriPluginCollector } from "@suaveplan/audio-ir-core/registrar";
const collector = createUriPluginCollector();
```
**Constraints:**
- Zero `@suaveplan/audio-*` runtime dependencies — pure contract/schema package
- Subpath exports: `/types`, `/registry`, `/registrar`, `/error-codes`, `/otel`
- IRs are referenced by URI and resolved through `@suaveplan/storage-uri`
- OTel instruments for lookup and registration operations
**Checklist:**
- [ ] `createIrRegistry()` returns a registry supporting lookup by family + ID
- [ ] `registerManifest()` adds manifest entries to the global registry
- [ ] `ImpulseResponseSchema` validates all required IR fields
- [ ] `createUriPluginCollector()` integrates with bundle registration

View File

@@ -0,0 +1,52 @@
---
name: sp-audio-ir-loader
description: Pipeline package for the audio-ir family — composes IrRegistry, StorageUriResolver, AudioDecoder, and manifest validation into a single createIrLoader(). Universal.
---
# sp-audio-ir-loader
| Symbol | Kind | Responsibility |
|---|---|---|
| `createIrLoader` | Function | Composes registry, URI resolver, decoder, and manifest validation into a unified loader |
| `IrLoader` | Interface | Load surface: `load(family, id)`, `preload(options)`, `cacheStats()` |
| `ImpulseResponseCache` | Class | In-memory LRU cache for decoded impulse responses |
| `impulseResponseBytes` | Function | Loads raw IR bytes from a resolved URI |
| `validateAndFinalize` | Function | Validates decoded audio against manifest expectations |
| `runPreload` | Function | Preloads a set of IRs into the cache |
| `AUDIO_IR_LOADER_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio-ir/audio-ir-loader/src/{loader,cache,otel,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-ir-core` — IR schemas and registry
- `@suaveplan/audio-ir-room-plugin-club` — content package (club IRs)
- `@suaveplan/audio-ir-room-plugin-pool` — content package (pool IRs)
- `@suaveplan/storage-uri` — URI resolution
- `@suaveplan/audio-decode-wav` — WAV decoder
**Patterns:**
*Quick Start:*
```ts
import { createIrLoader } from "@suaveplan/audio-ir-loader";
const loader = createIrLoader({ registry, resolver, decoder });
const ir = await loader.load("room", "club1"); // decoded audio buffer
```
*Preload:*
```ts
await loader.preload({ families: ["room"], ids: ["club1", "club2"] });
```
**Constraints:**
- Universal — zero platform-only APIs
- Subpath exports: `/error-codes`
- Caches decoded audio buffers to avoid repeated decode
- OTel instruments for cache hits, misses, load success, and errors
- Manifest validation ensures decoded audio matches expected format
**Checklist:**
- [ ] `createIrLoader()` returns a loader that resolves URIs to decoded audio
- [ ] `preload()` warms the cache for specified IRs
- [ ] `ImpulseResponseCache` evicts least-recently-used entries
- [ ] Manifest validation rejects mismatched sample rates or channel counts

View File

@@ -0,0 +1,49 @@
---
name: sp-audio-ir-room-plugin-club
description: Bundle-only audio-ir content package — ships club1.wav and club2.wav room impulse responses, registered via storage-uri-plugin-bundle and audio-ir-core.
---
# sp-audio-ir-room-plugin-club
| Symbol | Kind | Responsibility |
|---|---|---|
| `manifest` | Constant | Single source of truth for which impulse responses this package ships |
| `BUNDLE_PACKAGE_SLUG` | Constant | Unique slug for bundle registration (`"audio-ir-room-plugin-club"`) |
| `MANIFEST_SLUG` | Constant | Manifest identifier |
| `register` | Function | Side-effecting entrypoint that registers the bundled IRs with `@suaveplan/audio-ir-core` via `@suaveplan/storage-uri-plugin-bundle` |
**File Structure:** `packages/media/audio-ir/audio-ir-room-plugin-club/src/{manifest,register}/`
**Sibling packages:**
- `@suaveplan/audio-ir-core` — IR engine core
- `@suaveplan/audio-ir-loader` — IR loader
- `@suaveplan/audio-ir-room-plugin-pool` — sibling content package (pool IRs)
- `@suaveplan/storage-uri` — URI resolution
- `@suaveplan/storage-uri-plugin-bundle` — bundle registration plugin
**Patterns:**
*Quick Start:*
```ts
import { register } from "@suaveplan/audio-ir-room-plugin-club/register";
register(); // registers club1.wav and club2.wav with the IR engine
```
*Inspect manifest:*
```ts
import { manifest } from "@suaveplan/audio-ir-room-plugin-club/manifest";
console.log(manifest.entries); // [{ name: "club1" }, { name: "club2" }]
```
**Constraints:**
- Bundle-only content package — no runtime audio processing code
- Ships `.wav` files in the `files/` directory
- Registers through `@suaveplan/storage-uri-plugin-bundle` + `@suaveplan/audio-ir-core`
- Canonical example for all future `audio-ir-*-plugin-<name>` content packages
- Subpath exports: `/manifest`, `/register`
**Checklist:**
- [ ] `register()` makes `club1` and `club2` IRs resolvable via the URI system
- [ ] `manifest` lists all shipped impulse responses accurately
- [ ] Package ships `.wav` assets in the `files/` directory
- [ ] Follows the bundle-only content-package pattern

View File

@@ -0,0 +1,43 @@
---
name: sp-audio-ir-room-plugin-pool
description: Audio-ir content package — ships outdoor-swimming-pool IR with https CDN fallback. Bundle + fallback pattern via storage-uri-plugin-bundle and storage-uri-plugin-fallback.
---
# sp-audio-ir-room-plugin-pool
| Symbol | Kind | Responsibility |
|---|---|---|
| `manifest` | Constant | Single source of truth for shipped impulse responses and fallback URIs |
| `register` | Function | Side-effecting entrypoint that registers bundled IRs and CDN fallback with the URI system |
| `BUNDLE_PACKAGE_SLUG` | Constant | Unique slug for bundle registration |
| `BUNDLE_URI` | Constant | Bundle URI prefix |
| `CDN_URI` | Constant | HTTPS CDN fallback URI |
**File Structure:** `packages/media/audio-ir/audio-ir-room-plugin-pool/src/{manifest,register}/`
**Sibling packages:**
- `@suaveplan/audio-ir-core` — IR schemas and registry
- `@suaveplan/audio-ir-loader` — IR loader pipeline
- `@suaveplan/audio-ir-room-plugin-club` — sibling content package (club IRs)
- `@suaveplan/storage-uri-plugin-bundle` — bundle registration plugin
- `@suaveplan/storage-uri-plugin-fallback` — fallback URI plugin
**Patterns:**
*Quick Start:*
```ts
import { register } from "@suaveplan/audio-ir-room-plugin-pool/register";
register(); // registers pool IR with bundle + CDN fallback
```
**Constraints:**
- Bundle + fallback pattern — ships local `.wav` file plus HTTPS CDN fallback
- Subpath exports: `/manifest`, `/register`
- Uses both `@suaveplan/storage-uri-plugin-bundle` and `@suaveplan/storage-uri-plugin-fallback`
- Canonical example of the bundle+fallback content-package pattern
**Checklist:**
- [ ] `register()` makes the pool IR resolvable via bundle URI
- [ ] Fallback CDN URI is registered for offline-bundle fallback
- [ ] `manifest` lists the shipped impulse response and its metadata
- [ ] Follows the bundle+fallback content-package pattern

View File

@@ -0,0 +1,54 @@
---
name: sp-audio-metadata
description: Universal audio metadata extractor — ID3v1/v2, MP4/M4A atoms, FLAC Vorbis comments, Ogg, WAV, AIFF. Returns normalised Track record.
---
# sp-audio-metadata
| Symbol | Kind | Responsibility |
|---|---|---|
| `extractMetadata` | Function | Auto-detects format and extracts normalised `AudioMetadata` / `Track` |
| `MetadataExtractor` | Class | Pluggable extractor with format detection and parser registry |
| `parseId3v1` / `parseId3v2` | Function | ID3 tag parsers (v1.0/1.1 and v2.3/v2.4) |
| `parseMp4` | Function | MP4/M4A atom parser (moov, meta, ilst) |
| `parseFlac` / `parseVorbisCommentBlock` | Function | FLAC Vorbis comment and metadata block parser |
| `parseOgg` | Function | Ogg Vorbis comment parser |
| `parseWav` | Function | WAV RIFF chunk parser (LIST-INFO, ID3) |
| `parseAiff` | Function | AIFF ID3 chunk parser |
| `detectAudioFormat` | Function | Format detection from file header magic bytes |
| `METADATA_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio/audio-metadata/src/{core,parsers,errors,types,utils}/`
**Sibling packages:**
- `@suaveplan/audio-decode-wav` — WAV decoder
- `@suaveplan/audio-core` — audio document model
**Patterns:**
*Quick Start:*
```ts
import { extractMetadata } from "@suaveplan/audio-metadata";
const meta = await extractMetadata(fileBuffer);
// meta.title, meta.artist, meta.album, meta.duration, meta.coverArt
```
*Format-specific parser:*
```ts
import { parseId3v2 } from "@suaveplan/audio-metadata/parsers/id3v2";
const tags = parseId3v2(buffer);
```
**Constraints:**
- Universal — zero browser or Node-only APIs; pure buffer parsing
- Subpath exports: `/parsers/id3v2`, `/parsers/id3v1`, `/parsers/mp4-atom`, `/parsers/flac-vorbis`, `/parsers/ogg-vorbis`, `/parsers/wav-riff`, `/parsers/aiff-id3`, `/errors`
- `detectAudioFormat` identifies format from magic bytes before parsing
- `Track` schema normalises across all formats (title, artist, album, genre, year, trackNumber, duration, coverArt)
- OTel instrumented for extraction operations
**Checklist:**
- [ ] `extractMetadata` auto-detects format and returns normalised metadata
- [ ] `parseId3v2` handles both UTF-16 and UTF-8 text frames
- [ ] `parseMp4` extracts iTunes-style metadata atoms
- [ ] `detectAudioFormat` correctly identifies all supported formats from headers
- [ ] Normalised `Track` includes title, artist, album, duration, and cover art

View File

@@ -0,0 +1,54 @@
---
name: sp-audio-sprite-packer
description: Binary audio sprite map packer — 80%+ compression vs individual files. Concatenates clips into a single binary with a compact JSON manifest.
---
# sp-audio-sprite-packer
| Symbol | Kind | Responsibility |
|---|---|---|
| `AudioSpritePacker` | Class | Main packer — concatenates audio clips, emits binary + manifest |
| `createAudioSpritePacker` | Function | Factory for `AudioSpritePacker` with schema-validated options |
| `buildManifest` | Function | Builds a compact `AudioSpriteManifest` from packed clips |
| `validateManifest` | Function | Validates a manifest against `AudioSpriteManifestSchema` |
| `NullAudioAdapter` | Class | No-op audio adapter for testing / non-browser environments |
| `createNullAudioAdapter` | Function | Factory for the null adapter |
| `AUDIO_SPRITE_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio/audio-sprite-packer/src/{core,adapters,utils,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-sprite-player` — sprite player (unpacks and plays sprites)
- `@suaveplan/audio-core` — audio document model
**Patterns:**
*Quick Start:*
```ts
import { createAudioSpritePacker } from "@suaveplan/audio-sprite-packer";
const packer = createAudioSpritePacker({ format: "wav" });
const { binary, manifest } = await packer.pack(clips);
// binary: concatenated audio bytes
// manifest: { entries: [{ name, offset, duration }] }
```
*Validate manifest:*
```ts
import { validateManifest } from "@suaveplan/audio-sprite-packer/utils/manifest/manifest";
validateManifest(manifest); // throws on schema violation
```
**Constraints:**
- Universal — zero platform-only APIs
- Subpath exports: `/core/audio-sprite-packer`, `/adapters/null-adapter`, `/utils/manifest`, `/error-codes`
- `AudioSpritePackerOptionsSchema` validates format, sampleRate, and quality
- `AudioClipInputSchema` validates each clip (name, buffer, trim options)
- Manifest includes SHA-256 hashes for integrity verification
- Null adapter provided for server-side packing without audio hardware
**Checklist:**
- [ ] `pack()` produces a single binary and a JSON manifest
- [ ] Manifest entries include correct byte offsets and durations
- [ ] `validateManifest` rejects malformed manifests
- [ ] Null adapter works in non-browser environments
- [ ] SHA-256 hashes included for integrity

View File

@@ -0,0 +1,51 @@
---
name: sp-audio-sprite-player
description: Universal audio sprite playback engine — validates manifests, schedules voices, voice-stealing, injectable SpritePlaybackAdapter. Plays packed audio sprites.
---
# sp-audio-sprite-player
| Symbol | Kind | Responsibility |
|---|---|---|
| `SpritePlayer` | Class | Main playback engine — schedules sprite voices, handles polyphony and voice-stealing |
| `createSpritePlayer` | Function | Factory for `SpritePlayer` with schema-validated options |
| `VoiceManager` | Class | Polyphonic voice allocator with stealing strategy |
| `SpritePlaybackAdapter` | Interface | Injectable output adapter (Web Audio, HTML5, or custom) |
| `NullSpritePlaybackAdapter` | Class | No-op adapter for testing |
| `SpritePlayerEvents` | Class | Event emitter for play, stop, complete, error, and voice-stolen events |
| `SpritePlayerTelemetry` | Class | OTel telemetry for play counts, voice usage, and latency |
| `AUDIO_SPRITE_PLAYER_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio/audio-sprite-player/src/{sprite-player,voice-manager,adapter,events,schemas,telemetry,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-sprite-packer` — sprite packer (produces binaries + manifests)
- `@suaveplan/audio-core` — audio document model
**Patterns:**
*Quick Start:*
```ts
import { createSpritePlayer } from "@suaveplan/audio-sprite-player";
const player = createSpritePlayer({ adapter: webAudioAdapter, maxVoices: 32 });
player.play("explosion"); // plays the sprite named "explosion"
```
*Voice stealing:*
```ts
player.play("explosion", { priority: 10 }); // higher priority = harder to steal
```
**Constraints:**
- Universal — zero platform-only APIs; adapter pattern delegates output
- Subpath exports: `/sprite-player/sprite-player`, `/voice-manager/voice-manager`, `/adapter`, `/adapter/null`, `/events/events`, `/telemetry/telemetry`, `/schemas`, `/error-codes`
- `SpritePlayerOptionsSchema` validates maxVoices, defaultGain, and adapter
- Voice stealing uses LRU + priority strategy
- OTel telemetry for voice allocation, play counts, and errors
**Checklist:**
- [ ] `play()` schedules a sprite voice and returns a `SpritePlayHandle`
- [ ] Voice stealing evicts the lowest-priority / oldest voice when max exceeded
- [ ] `NullSpritePlaybackAdapter` records all calls for test assertions
- [ ] Events fire for play, stop, complete, error, and voice-stolen
- [ ] Manifest validation rejects invalid sprite entries before playback

View File

@@ -0,0 +1,60 @@
---
name: sp-audio-timeline
description: Headless DAW timeline engine — multi-track arrangements with clips, playback, loop regions, selection, zoom, per-track mixer. Delegates rendering to pluggable TimelineAdapter.
---
# sp-audio-timeline
| Symbol | Kind | Responsibility |
|---|---|---|
| `TimelineEngine` | Class | Headless DAW timeline — multi-track playback, loop regions, selection, zoom |
| `TimelineAdapter` | Interface | Pluggable rendering adapter (browser/server) |
| `NullTimelineAdapter` | Class | No-op adapter for testing |
| `createTrack` / `updateTrack` / `removeTrack` | Function | Track CRUD operations |
| `createClip` / `moveClip` / `splitClip` / `trimClip` | Function | Clip operations with boundary validation |
| `setLoopRegion` / `setSelection` / `setZoomLevel` | Function | Timeline state mutations |
| `setMasterVolume` / `setTrackVolume` / `setTrackPan` / `setTrackMute` / `setTrackSolo` | Function | Mixer state mutations |
| `ClipSchema` / `TrackSchema` / `TimelineStateSchema` | Type | Zod schemas for timeline entities |
| `AUDIO_TIMELINE_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio/audio-timeline/src/{engine,track,clip,operations,mixer,zoom,adapter,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-engine-browser` — browser `TimelineAdapter` (Web Audio)
- `@suaveplan/audio-engine-server` — server `TimelineAdapter` (file output)
- `@suaveplan/audio-core` — audio document model
- `@suaveplan/audio-transport` — transport sync
- `@suaveplan/fsm` — playback state machine
- `@suaveplan/undo` — undo/redo support
**Patterns:**
*Quick Start:*
```ts
import { TimelineEngine } from "@suaveplan/audio-timeline";
const timeline = new TimelineEngine({ bpm: 120, timeSignature: [4, 4] });
timeline.addTrack({ name: "Drums" });
timeline.addClip({ trackId: "drums", start: 0, duration: 4, document: drumLoop });
```
*Clip operations:*
```ts
import { splitClip, moveClip, trimClip } from "@suaveplan/audio-timeline/operations/clip-operations";
splitClip(timeline, clipId, 2); // split at 2 beats
moveClip(timeline, clipId, 4); // move to beat 4
trimClip(timeline, clipId, { start: 0.5, end: 3.5 });
```
**Constraints:**
- Headless — no rendering logic; delegates to `TimelineAdapter`
- Subpath exports: `/engine`, `/track`, `/clip`, `/operations/clip-operations`, `/operations/timeline-operations`, `/adapter`, `/mixer`, `/zoom`, `/error-codes`
- All mutations are undoable via `@suaveplan/undo`
- `TimelineStateSchema` validates the full engine state
- OTel instrumented for playback and editing operations
**Checklist:**
- [ ] `TimelineEngine` supports multi-track playback with loop regions
- [ ] `splitClip` / `moveClip` / `trimClip` validate boundaries and reject invalid ops
- [ ] Mixer supports per-track volume, pan, mute, and solo
- [ ] Zoom operations use `DEFAULT_ZOOM_TABLE` for discrete levels
- [ ] All clip operations are undoable

View File

@@ -0,0 +1,66 @@
---
name: sp-audio-transport
description: Universal musical clock and transport — sample-accurate scheduling with PPQ, BPM automation, swing, time signatures, loop regions. Zero platform deps.
---
# sp-audio-transport
| Symbol | Kind | Responsibility |
|---|---|---|
| `Transport` | Class | Musical clock — play, pause, stop, seek, BPM, time signature, loop |
| `TransportScheduler` | Class | Sample-accurate event scheduler with lookahead |
| `TickSource` | Class | Generates tick events at transport rate (PPQ-based) |
| `Signal` | Class | Automation signal — linear/exponential/step curves between points |
| `fromBarsBeatsSixteenths` / `toBarsBeatsSixteenths` | Function | Human-readable time ↔ tick conversions |
| `ticksToSeconds` / `secondsToTicks` | Function | Tick ↔ seconds conversions at current BPM |
| `parseTimeToken` | Function | Parses time strings like `"1:2:0"` or `"4m"` or `"2.5s"` |
| `AUDIO_TRANSPORT_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio/audio-transport/src/{transport,scheduler,tick-source,signal,time,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-timeline` — timeline engine (transport consumer)
- `@suaveplan/audio-engine-browser` — Web Audio adapter
- `@suaveplan/audio-engine-server` — file-render adapter
- `@suaveplan/audio-controllers` — MIDI controller sync
- `@suaveplan/fsm` — transport state machine
- `@suaveplan/timing` — timing primitives
**Patterns:**
*Quick Start:*
```ts
import { Transport } from "@suaveplan/audio-transport";
const transport = new Transport({ bpm: 128, ppq: 960 });
transport.schedule((time) => console.log("tick", time), "0:0:0");
transport.start();
```
*BPM automation:*
```ts
import { Signal } from "@suaveplan/audio-transport/signal/signal";
const bpmSignal = new Signal([[0, 120], [4, 140]], "linear");
transport.bpm = bpmSignal;
```
*Time conversion:*
```ts
import { fromBarsBeatsSixteenths, ticksToSeconds } from "@suaveplan/audio-transport/time/time";
const ticks = fromBarsBeatsSixteenths("2:1:0"); // bar 2, beat 1, sixteenth 0
const seconds = ticksToSeconds(ticks, 120); // at 120 BPM
```
**Constraints:**
- Universal — zero browser or Node-only APIs
- Subpath exports: `/transport/transport`, `/scheduler/scheduler`, `/tick-source/tick-source`, `/signal/signal`, `/time/time`, `/error-codes`
- PPQ (pulses per quarter) default is 960 for high-resolution scheduling
- Swing applies to eighth-note subdivisions
- Loop regions are inclusive-start, exclusive-end
- OTel instrumented for schedule and transport state changes
**Checklist:**
- [ ] `Transport.start()` / `pause()` / `stop()` / `seek()` work sample-accurately
- [ ] `TransportScheduler` fires callbacks within the lookahead window
- [ ] `Signal` interpolates linear, exponential, and step curves
- [ ] `parseTimeToken` handles bars:beats:sixteenths, measures, and seconds
- [ ] BPM automation changes timing dynamically

View File

@@ -0,0 +1,92 @@
---
name: sp-audit-logging
description: Tamper-proof GDPR-compliant audit logging with cryptographic chain-integrity, pluggable storage, retention policies, and a rich event taxonomy.
---
# Architecture
| Component | Responsibility |
|---|---|
| `AuditLogger` | Orchestrates log, query, verify, export, and retention with injectable storage adapter |
| `MemoryAuditAdapter` | Full in-memory storage with rich query filtering for tests and development |
| `BaseAuditStorageAdapter` | Abstract base class for custom storage backends |
| `computeEventHash` / `verifyChain` | HMAC-SHA256 chain signing and tamper-evident verification via `previousHash` |
| `exportGDPRData` | Article 15/20 compliant data export with event summary statistics |
| `AUDIT_CODES` + `AuditError` | Structured errors with stable `AUDIT.*` codes via `@suaveplan/error` |
| `AuditEventType` / `AuditSeverity` | Rich taxonomy: auth, authorization, data access, system, GDPR events |
## File Structure
```
packages/observability/monitor/audit-logging/src/
adapters/memory/ MemoryAuditAdapter (in-memory storage + query)
adapters/storage/ BaseAuditStorageAdapter (abstract base)
core/audit-logger/ AuditLogger orchestrator
core/chain-integrity/ HMAC signing, hash computation, chain verification
errors/ AuditError and AUDIT_CODES
schemas/ Zod schemas for all public types
types/ TypeScript interfaces (AuditEvent, AuditActor, AuditTarget, etc.)
constants/ Event type constants, retention defaults, OTEL metric names
```
## Sibling packages
- `@suaveplan/crypto` / `@suaveplan/crypto-hash` — HMAC-SHA256 chain integrity
- `@suaveplan/events` — EventBus integration (`audit:event:logged`)
- `@suaveplan/logger` — structured logging integration
- `@suaveplan/serialize` — serialization for chain hashing
- `@suaveplan/error` — structured error registry via `defineCodes`
- `@suaveplan/testing` — unit test infrastructure
## Patterns
### Quick Start
```ts
import { AuditLogger, MemoryAuditAdapter, AuditEventType, AuditSeverity } from "@suaveplan/audit-logging";
const logger = new AuditLogger({
storage: new MemoryAuditAdapter(),
secretKey: process.env.AUDIT_SECRET_KEY!,
enableChainIntegrity: true,
});
const event = await logger.log({
type: AuditEventType.AUTH_LOGIN,
severity: AuditSeverity.INFO,
actor: { userId: "user-123", ipAddress: "203.0.113.1" },
target: { type: "session", id: "sess-456" },
});
console.log(event.hash); // hex HMAC-SHA256
```
### Verify chain integrity
```ts
const events = await logger.query({ order: "asc" });
const result = await logger.verifyChainIntegrity(events);
console.log(result.valid); // true
```
### GDPR export
```ts
const gdprExport = await logger.exportGDPRData("user-123");
console.log(gdprExport.summary.totalEvents);
```
## Constraints
- `secretKey` required when `enableChainIntegrity: true`
- Chain integrity links events via `previousHash`; tampering breaks the chain
- Retention policy supports archive-before-delete with configurable TTL
- OTel counter `audit.events_logged_total` tagged by `event_type` and `severity`
## Checklist
- [ ] Verify `AuditLogger` supports all event taxonomy categories
- [ ] Confirm `AUDIT_CODES` registered via `defineCodes("AUDIT", [...])`
- [ ] Check subpath exports in `package.json` align with `src/` modules
- [ ] Run `bunx turbo lint typecheck test build --filter=@suaveplan/audit-logging`
- [ ] Confirm chain integrity test proves tamper detection works

View File

@@ -0,0 +1,103 @@
---
name: sp-auth
description: Provider-agnostic authentication with JWT lifecycle, API key management, session management, and pluggable storage adapters
---
# sp-auth
## Architecture
| Class / Function | Responsibility |
|---|---|
| `AuthAdapter` | Interface contract for pluggable auth storage (sessions, blacklist, API keys). |
| `MemoryAuthAdapter` | In-memory reference implementation of `AuthAdapter`. |
| `issueToken` | Creates HS256-signed access + refresh token pair and persists the session. |
| `validateSessionToken` | Verifies signature, checks expiry and blacklist; returns discriminated `TokenValidationResult`. |
| `refreshToken` | Validates refresh token, blacklists old JTI, issues new pair. |
| `createApiKey` / `validateApiKey` / `revokeApiKey` | Full API key lifecycle with permissions. |
| `requirePermission` / `requireAllPermissions` / `requireAnyPermission` | Framework-agnostic middleware factories for permission gating. |
| `AUTH_CODES` + `AuthError` | Stable error registry with subclasses (`TokenExpiredError`, `PermissionDeniedError`, `ApiKeyRevokedError`, etc.). |
## File Structure
```
identity/auth/auth/src/
adapters/
auth/ — AuthAdapter interface
memory/ — MemoryAuthAdapter implementation
api-keys/ — createApiKey, validateApiKey, revokeApiKey, rotateApiKey, etc.
errors/ — AuthError hierarchy + AUTH_CODES
jwt/ — issueToken, validateSessionToken, refreshToken, revokeToken
middleware/ — requirePermission, checkPermission, withPermissions, etc.
schemas/ — Zod schemas for JWTOptions, ApiKeyOptions, MiddlewareOptions
types/ — Public type aliases (JWTPayload, SessionInfo, UserContext, etc.)
```
## Sibling packages
- `sp-better-auth-surrealdb` — SurrealDB adapter for Better Auth
- `sp-jwt-tenant` — tenant-scoped JWT utilities
- `sp-macos-keychain` — macOS keychain credential storage
- `sp-oauth-client` — OAuth client implementation
- `sp-permissions` — permission model and evaluation
- `sp-webauthn` — WebAuthn credential management
## Patterns
### Quick Start — JWT issue and validate
```ts
import { issueToken, validateSessionToken, MemoryAuthAdapter } from "@suaveplan/auth";
const adapter = new MemoryAuthAdapter();
const opts = { secret: process.env["JWT_SECRET"] ?? "" };
const { accessToken } = await issueToken(adapter, "user-1", opts);
const result = await validateSessionToken(adapter, accessToken, opts);
if (result.valid) console.log("User:", result.payload?.sub);
```
### API key lifecycle
```ts
import { createApiKey, validateApiKey, revokeApiKey } from "@suaveplan/auth";
const { rawKey } = await createApiKey(adapter, { name: "CI Key", userId: "u1" });
const v = await validateApiKey(adapter, rawKey);
await revokeApiKey(adapter, v.apiKey!.id);
```
### Permission middleware
```ts
import { requirePermission, requireAllPermissions } from "@suaveplan/auth";
const adminOnly = requirePermission("admin:write", adapter, jwtOpts);
const superAdmin = requireAllPermissions(["admin:write", "admin:delete"], adapter, jwtOpts);
```
### Refresh token rotation
```ts
import { refreshToken, revokeAllUserTokens } from "@suaveplan/auth";
const tokens = await refreshToken(adapter, refreshTokenString, opts);
await revokeAllUserTokens(adapter, "user-1"); // logout everywhere
```
## Constraints
- Pure HS256 (HMAC-SHA256) signing via Web Crypto API — no external JWT library dependency.
- Secrets shorter than 32 characters are accepted at the implementation level but the Zod schema enforces a 32-character minimum.
- Refresh token rotation blacklists the old JTI immediately on use to limit replay windows.
- Base64url encoding is implemented without `Buffer` for browser compatibility.
- `verifyTokenMiddleware` reads `Authorization: Bearer <token>`; `optionalTokenMiddleware` allows anonymous access.
- All crypto operations use `crypto.subtle` so the module runs in both Node.js and browsers.
- 100% statement + branch + function + line coverage required.
## Checklist
- [ ] `bunx turbo lint typecheck test --filter=@suaveplan/auth` passes
- [ ] `bunx turbo test:coverage --filter=@suaveplan/auth` reports 100%
- [ ] `bun run docs:check --filter=@suaveplan/auth` passes
- [ ] Every `src/**/*.md` has a sibling `.ts` module with matching exports

View File

@@ -0,0 +1,144 @@
---
id: sp-barcode
name: Barcode
library: "@suaveplan/barcode"
description: >
1D barcode generation and parsing for seven symbologies: Code 128, Code 39,
EAN-13, EAN-8, UPC-A, UPC-E, and ITF. Outputs SVG strings and binary
patterns with no DOM or canvas dependency. Includes validation, check-digit
calculation, and per-symbology low-level encoders.
---
# `@suaveplan/barcode`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **Encoder** | Central dispatch + SVG rendering | `encode(data, symbology, options?)`, `renderSvg` |
| **Validator** | Cross-symbology validation, parsing, check digits | `validateBarcode`, `parse`, `calculateCheckDigit` |
| **Symbologies** | Per-symbology encode/validate pairs | `encodeCode128`, `validateCode128`, `encodeEan13`, `validateEan13`, … |
| **Errors** | Typed errors with stable codes | `BarcodeError`, `BARCODE_CODES` |
## File Structure
```
src/
├── index.ts # Public barrel
├── core/
│ ├── encoder/encoder.ts # encode(), renderSvg()
│ └── validator/validator.ts # validateBarcode(), parse(), calculateCheckDigit()
├── symbologies/
│ ├── code128.ts # Code 128 Subset B (ASCII 32–127), mod-103 check
│ ├── code39.ts # Code 39 (alphanumeric + 7 specials)
│ ├── ean13.ts # EAN-13 (L/G/R codes, Luhn check)
│ ├── ean8.ts # EAN-8
│ ├── upca.ts # UPC-A
│ ├── upce.ts # UPC-E
│ └── itf.ts # Interleaved 2 of 5
├── types/barcode.types.ts # BarcodeSymbology, BarcodeOptions, BarcodeResult
└── errors/barcode-error.ts # BarcodeError, BARCODE_CODES
```
## Patterns
### Basic — Encode a barcode to SVG
```ts
import { encode } from "@suaveplan/barcode";
const result = encode("123456789012", "ean13", {
width: 2,
height: 100,
margin: 10,
includeText: true,
color: "#000000",
background: "#ffffff",
});
// result.encoded → binary string ('1'=bar, '0'=space)
// result.svg → full SVG markup
// result.width → total SVG width in px
// result.height → total SVG height in px
```
### Basic — Validate data before encoding
```ts
import { validateBarcode } from "@suaveplan/barcode";
if (validateBarcode("1234567890128", "ean13")) {
const result = encode("1234567890128", "ean13");
}
```
### Basic — Parse a barcode string
```ts
import { parse } from "@suaveplan/barcode";
const parsed = parse("1234567890128", "ean13");
// parsed → { symbology: "ean13", data: "1234567890128", valid: true, checkDigit: "8" }
```
### Basic — Calculate a check digit
```ts
import { calculateCheckDigit } from "@suaveplan/barcode";
const check = calculateCheckDigit("123456789012", "ean13"); // "8"
const check8 = calculateCheckDigit("1234567", "ean8"); // computes EAN-8 check
```
### Advanced — Use a symbology encoder directly
```ts
import { encodeCode128, validateCode128, checkValueCode128 } from "@suaveplan/barcode";
if (validateCode128("Hello World")) {
const pattern = encodeCode128("Hello World");
const check = checkValueCode128("Hello World"); // 0–102
}
```
### Advanced — Render a custom binary pattern to SVG
```ts
import { renderSvg } from "@suaveplan/barcode";
const svg = renderSvg(binaryPattern, humanReadableText, {
width: 2, height: 100, margin: 10, includeText: true, color: "#000", background: "#fff"
});
```
## Constraints
- **Runtime-agnostic**: outputs SVG strings and binary patterns; no DOM or canvas dependency.
- **Code 128**: Subset B only (ASCII 32–127). Throws `BARCODE_CODES.INVALID_DATA` for characters outside this range.
- **EAN-13**: accepts 12 digits (auto-computes check digit) or 13 digits (validates existing check digit).
- **EAN-8**: accepts 7 digits (auto-computes) or 8 digits (validates).
- **UPC-A**: accepts 11 digits (auto-computes) or 12 digits (validates).
- **UPC-E**: accepts 8 digits with check digit validation.
- **ITF**: accepts an even number of digits.
- **SVG text escaping**: `renderSvg` escapes XML special characters (`& < > " '`) in the human-readable text.
- **Error codes**: `BARCODE_CODES.INVALID_DATA`, `BARCODE_CODES.UNSUPPORTED_SYMBOLOGY`, `BARCODE_CODES.ENCODE_FAILED`.
- **Dependencies**: `@opentelemetry/api`, `zod`, `tslib`.
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/barcode` |
| Symbologies | `@suaveplan/barcode/symbologies` |
| Encoder | `@suaveplan/barcode/encoder` |
| Validator | `@suaveplan/barcode/validator` |
| Errors | `@suaveplan/barcode/errors` |
## Checklist
- [ ] Need to generate a barcode SVG → `encode(data, symbology, options?)`
- [ ] Need to validate data for a symbology → `validateBarcode(data, symbology)`
- [ ] Need to extract check digit from EAN/UPC → `parse(data, symbology)`
- [ ] Need to compute a check digit → `calculateCheckDigit(data, "ean13" \| "ean8" \| "upca")`
- [ ] Need low-level binary pattern → per-symbology `encodeXxx(data)`
- [ ] Need to render a custom pattern to SVG → `renderSvg(pattern, text, options)`

View File

@@ -0,0 +1,100 @@
---
name: sp-better-auth-surrealdb
description: Better-Auth Adapter for SurrealDB v3 — 17-table schema, Where[]→SurrealQL compilation, record-link prefixing, id strategies, conformance harness.
---
# Architecture
| Component | Responsibility |
|---|---|
| `createBetterAuthSurrealAdapter` | Returns 10-method Better-Auth `Adapter` (create/find/update/delete/count) |
| `betterAuthSurrealAdapter` | v1.6 integration surface wrapping `createAdapterFactory` |
| `compileWhere` | Translates Better-Auth `Where[]` filters to parameterised SurrealQL `WHERE` |
| `compileSelect` | Compiles `select?: string[]` projection into SurrealQL column list |
| `compileSortBy` | Compiles `sortBy` into SurrealQL `ORDER BY` fragment |
| `applyLinkPrefix` / `stripLinkPrefix` | Auto-prefixes record-link fields on write, strips on read |
| `prepareCreate` / `normaliseReturnedId` | Id strategy handlers: `record-id` (default), `plain`, `ulid` |
| `applyBetterAuthSchema` / `BETTER_AUTH_SURREAL_SCHEMA` | Bundled canonical 17-table SurrealQL schema |
| `createInMemoryKeyStore` | In-memory JWKS storage with rotation and expiration |
| `BETTER_AUTH_SURREALDB_CODES` + `BetterAuthSurrealError` | Structured error registry |
| `instrument` | OTel instrumentation wrapper for adapter operations |
| `runAdapterConformance` | Reusable conformance test harness |
## File Structure
```
packages/identity/auth/better-auth-surrealdb/src/
adapter/ createBetterAuthSurrealAdapter, betterAuthSurrealAdapter (v1.6), internals, operations
compile/ compileWhere, compileSelect, compileSortBy
id-strategy/ prepareCreate, normaliseReturnedId, generateUlid
link-fields/ applyLinkPrefix, stripLinkPrefix, DEFAULT_BETTER_AUTH_LINK_FIELDS
schema/ BETTER_AUTH_SURREAL_SCHEMA, applyBetterAuthSchema
schemas/ Zod schemas for options and link field maps
error-codes/ BETTER_AUTH_SURREALDB_CODES and BetterAuthSurrealError
otel/ instrument, AdapterOutcome, resetInstrumentsForTesting
types/ TypeScript interfaces (BetterAuthSurrealAdapterOptions, Row, etc.)
testing/ runAdapterConformance harness
```
## Sibling packages
- `@suaveplan/surrealdb` — SurrealAdapter for database operations
- `@suaveplan/testing-integration-surrealdb` — SurrealDB container fixture for conformance tests
- `@suaveplan/error` — structured error registry via `defineCodes`
- `@suaveplan/types` — shared TypeScript primitives
## Patterns
### Quick Start (v1.6)
```ts
import { SurrealAdapter } from "@suaveplan/surrealdb";
import { applyBetterAuthSchema } from "@suaveplan/better-auth-surrealdb/schema";
import { betterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb/adapter/v1.6";
import { betterAuth } from "better-auth";
const client = new SurrealAdapter({ url: "ws://localhost:8000", namespace: "auth", database: "auth", username: "root", password: process.env.SURREAL_PASSWORD });
await client.connect();
await applyBetterAuthSchema(client);
export const auth = betterAuth({
database: betterAuthSurrealAdapter({ client }),
secret: process.env.BETTER_AUTH_SECRET,
baseURL: "http://localhost:3000",
emailAndPassword: { enabled: true },
});
```
### Direct adapter (v1.4 shape)
```ts
import { createBetterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb/adapter";
const adapter = createBetterAuthSurrealAdapter({ client });
await adapter.create({ model: "user", data: { id: "u1", email: "a@b" } });
```
### Custom link fields
```ts
createBetterAuthSurrealAdapter({
client,
linkFields: { session: { userId: "user" }, member: { userId: "user", organizationId: "tenant" } },
});
```
## Constraints
- Peer dependency on `better-auth` ^1.4.0 (type-only at package boundary)
- SurrealDB v3 required (uses `record<target>` typed links)
- Default id strategy is `record-id`; rejects ids containing `:`
- Schema version moves in lockstep with package version
- Conformance harness requires Docker SurrealDB container
## Checklist
- [ ] Verify adapter implements all 10 Better-Auth Adapter methods
- [ ] Confirm `BETTER_AUTH_SURREALDB_CODES` registered via `defineCodes`
- [ ] Check subpath exports in `package.json` align with `src/` modules
- [ ] Run `bunx turbo lint typecheck test build --filter=@suaveplan/better-auth-surrealdb`
- [ ] Confirm conformance harness passes against SurrealDB container

View File

@@ -0,0 +1,85 @@
---
name: sp-bitpacker-compressed
description: Compression pipeline plugin for bitpacker with self-describing algorithm headers, pluggable adapters, and RLE fast path.
---
# Bitpacker Compressed for Suaveplan
Compression pipeline integration for `@suaveplan/bitpacker`. Extends `BitPacker<T>` to add a configurable compression stage between bit-packing and wire bytes. Compressed bytes are framed with a self-describing algorithm header so receivers can decompress without prior knowledge of the sender's codec choice.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `CompressedBitPacker<T>` | Extends `BitPacker<T>` with async `packWithCompression` / `unpackWithDecompression` and a self-describing wire header. |
| `CompressionRegistry` | Plug-in registry for compression adapters; `defaultCompressionRegistry()` is the process-wide singleton, `createCompressionRegistry()` for isolated tests. |
| `NoCompressionAdapter` | Pass-through adapter (default). |
| `GzipCompressionAdapter` / `DeflateCompressionAdapter` | Built-in stream-backed adapters, lazily registered when `CompressionStream` is available. |
| `compressRle` / `decompressRle` | Bytes-only run-length-encoding fast path. |
| `compressData` / `decompressData` | Algorithm-by-name convenience helpers. |
## File Structure
```
foundation/bitpacker/bitpacker-compressed/
src/compressed-bit-packer/ # CompressedBitPacker class
src/adapters/ # none, gzip, deflate adapters
src/registry/ # CompressionRegistry
src/rle/ # RLE codec
src/api/ # compressData / decompressData helpers
src/types/ # CompressionAdapter, CompressionConfig types
src/otel/ # OTel instruments
src/register-schemas/ # Schema-registry integration
```
Sibling packages:
- `@suaveplan/bitpacker` — base `BitPacker<T>` class that `CompressedBitPacker` extends.
- `@suaveplan/schema-registry` — optional integration for runtime schema registration.
## Patterns
### Quick Start
```typescript
import { CompressedBitPacker } from "@suaveplan/bitpacker-compressed";
const schema = { x: 255, y: 255 } as const;
const packer = new CompressedBitPacker(schema);
const bytes = await packer.packWithCompression({ x: 42, y: 99 });
const decoded = await packer.unpackWithDecompression(bytes);
// → { x: 42, y: 99 }
```
### Custom Registry with Brotli
```typescript
import { CompressedBitPacker, createCompressionRegistry } from "@suaveplan/bitpacker-compressed";
const registry = createCompressionRegistry();
registry.register(new MyBrotliAdapter());
const packer = new CompressedBitPacker(
{ position: { x: "f32", y: "f32", z: "f32" } },
{
compression: { algorithm: "brotli", enabled: true, options: { quality: 11 } },
compressionRegistry: registry,
}
);
const wireBytes = await packer.packWithCompression({ position: { x: 1, y: 2, z: 3 } });
```
## Constraints
- Algorithm names are limited to 1–255 ASCII bytes by the `headerLen` byte in the wire format.
- The sync `pack` / `unpack` inherited from `BitPacker` do NOT add framing; use the async `packWithCompression` / `unpackWithDecompression` for compressed paths.
- `gzip` / `deflate` adapters are lazily registered on the default registry only when `CompressionStream` is available.
- RLE codec is byte-only; use it for simple repetitive byte sequences.
## Checklist
1. Use `CompressedBitPacker` when you need compression; use plain `BitPacker` when you don't.
2. Call `packWithCompression` / `unpackWithDecompression` (async) for the compressed path; the sync methods remain for uncompressed hot paths.
3. For tests or multi-tenant servers, create an isolated registry via `createCompressionRegistry()` to avoid polluting the process-wide singleton.
4. Verify the target environment supports `CompressionStream` before relying on `gzip` or `deflate`.

View File

@@ -0,0 +1,188 @@
---
id: sp-bitpacker-crypto
name: BitPacker Crypto
library: "@suaveplan/bitpacker-crypto"
description: >
Bit-packer-aware crypto primitives: checksums (crc32, adler32, xxhash32),
pluggable HashAlgorithmRegistry, SchemaFingerprint, and five composable
wire-format schemas — EnvelopeSchema, SignedBitSchema, MultiSignedBitSchema,
EncryptedBitSchema, HashChainSchema. All crypto implementations are
dependency-injected via types from @suaveplan/types; there is zero runtime
dependency on @suaveplan/crypto.
---
# `@suaveplan/bitpacker-crypto`
## Architecture
| Primitive | Role | Wire format | Key export |
|-----------|------|-------------|------------|
| **Checksums** | Fast non-cryptographic integrity | Trailing 4 bytes (big-endian) | `crc32`, `adler32`, `calculateXxhash32`, `appendCrc32`, `verifyAndStripCrc32`, `appendXxhash32`, `verifyAndStripXxhash32` |
| **HashAlgorithmRegistry** | Pluggable hash algorithm lookup | n/a (runtime registry) | `createHashRegistry`, `defaultHashRegistry`, `HashAlgorithm`, `HashAlgorithmRegistry` |
| **SchemaFingerprint** | Canonical content-hash of a `BitPackerSchema` | n/a | `schemaFingerprint`, `assertSchemaFingerprint`, `canonicalizeSchema` |
| **EnvelopeSchema** | Typed `[magic][version][body]` frame | `[magic…][u8 version][body…]` | `defineEnvelopeSchema`, `BodyCodec`, `rawBytesCodec` |
| **SignedBitSchema** | Single-authority signed payload | `[body…][signature]` | `defineSignedBitSchema`, `SignedBitSchema` |
| **MultiSignedBitSchema** | N-of-N co-signed payload | `[body…][sig_1]…[sig_N]` | `defineMultiSignedBitSchema`, `MultiVerifyResult` |
| **EncryptedBitSchema** | AEAD-encrypted payload | `[u8 version][u8 algTag][u8 nonceLen][nonce…][ciphertext…]` | `defineEncryptedBitSchema`, `ENCRYPTED_ALG_TAGS` |
| **HashChainSchema** | Append-only signed hash chain | `[u64 seq BE][prevHash…][payload…][signature]` | `defineHashChainSchema`, `HashChainEntry`, `ChainVerifyResult` |
## File Structure
```
src/
├── index.ts # Public barrel — exports all primitives
├── crc32.ts # CRC32 + Adler32 (append/verifyAndStrip)
├── xxhash32.ts # Pure-TS xxHash32 (append/verifyAndStrip)
├── envelope/
│ └── envelope-schema.ts # defineEnvelopeSchema, BodyCodec, rawBytesCodec
├── signed/
│ └── signed-bitschema.ts # defineSignedBitSchema (single signer)
├── multisigned/
│ └── multisigned-bitschema.ts # defineMultiSignedBitSchema (N-of-N)
├── encrypted/
│ └── encrypted-bitschema.ts # defineEncryptedBitSchema (AEAD envelope)
├── hashchain/
│ └── hashchain-schema.ts # defineHashChainSchema (append-only chain)
├── fingerprint/
│ └── schema-fingerprint.ts # schemaFingerprint, canonicalizeSchema
└── hash-registry/
└── hash-registry.ts # HashAlgorithmRegistry, defaultHashRegistry
```
## Patterns
### Basic — Append a checksum to bit-packed bytes
```ts
import { appendCrc32, verifyAndStripCrc32, crc32 } from "@suaveplan/bitpacker-crypto";
const body = schema.pack({ x: 1, y: 2 });
const withChecksum = appendCrc32(body); // body + 4-byte CRC32 BE
const recovered = verifyAndStripCrc32(withChecksum); // throws on mismatch
```
### Basic — Schema fingerprint for agreement handshake
```ts
import { schemaFingerprint, assertSchemaFingerprint } from "@suaveplan/bitpacker-crypto";
const fp = await schemaFingerprint(mySchema, "sha256");
// On peer: assert agreed schema before interpreting payload
await assertSchemaFingerprint(peerSchema, expectedFp, "sha256");
```
### Basic — Typed envelope with magic + version
```ts
import { defineEnvelopeSchema, rawBytesCodec } from "@suaveplan/bitpacker-crypto";
const env = defineEnvelopeSchema({
magic: new Uint8Array([0x53, 0x56, 0x43, 0x42]), // "SVCB"
formatVersion: 1,
bodyCodec: { pack: (b) => schema.pack(b), unpack: (bytes) => schema.unpack(bytes) },
});
const bytes = env.pack(payload);
const parsed = env.parse(bytes); // { magic, formatVersion, bodyBytes } — no body decode
const decoded = env.parseStrict(bytes); // validates magic+version, then decodes body
```
### Advanced — Signed payload with injected verifier
```ts
import { defineSignedBitSchema, DEFAULT_SIGN_SLOT } from "@suaveplan/bitpacker-crypto";
import type { Authority, AuthorityVerifier, DomainTag } from "@suaveplan/types";
const signed = defineSignedBitSchema(bodyCodec, {
domain: "my-domain" as DomainTag,
verifier: myVerifier, // injected — typically from @suaveplan/crypto/authority
slot: DEFAULT_SIGN_SLOT,
signatureLength: 64, // ed25519 default
});
const wrapped = await signed.pack(data, authority);
const recovered = signed.unpack(wrapped, parentPublicSlot); // throws CRYPTO_VERIFY_FAILED
const ok = signed.verify(wrapped, parentPublicSlot); // boolean, never throws
```
### Advanced — Encrypted payload with injected AEAD ops
```ts
import { defineEncryptedBitSchema, AEAD_KEY_LENGTH } from "@suaveplan/bitpacker-crypto";
const enc = defineEncryptedBitSchema(bodyCodec, {
key: new Uint8Array(AEAD_KEY_LENGTH),
aead: cryptoAead, // injected — typically `{ encrypt, decrypt }` from @suaveplan/crypto
algorithm: "XChaCha20-Poly1305", // default; also "AES-256-GCM", "ChaCha20-Poly1305"
aad: sessionIdBytes, // optional additional authenticated data
});
const ciphertext = await enc.pack(data);
const plaintext = await enc.unpack(ciphertext); // throws CRYPTO_DECRYPT_FAILED on tag mismatch
```
### Advanced — Composition: Sign(Envelope(Encrypted))
All primitives accept any `BodyCodec<T>` and compose end-to-end:
```ts
const enc = defineEncryptedBitSchema(innerCodec, { key, aead: cryptoAead });
const env = defineEnvelopeSchema({ magic, formatVersion: 1, bodyCodec: rawBytesCodec() });
const signed = defineSignedBitSchema(
{ pack: (b) => env.pack(b), unpack: (b) => env.parseStrict(b) },
{ domain, verifier: cryptoVerifier }
);
const bytes = await signed.pack(await enc.pack(data), authority);
```
### Advanced — Hash chain verification
```ts
import { defineHashChainSchema } from "@suaveplan/bitpacker-crypto";
const chain = defineHashChainSchema({
domain, verifier, payloadCodec,
hashAlgorithm: "sha256", // from HashAlgorithmRegistry
signatureLength: 64,
});
const entry = await chain.appendEntry(1n, genesisHash, payload, authority);
const packed = chain.packEntry(entry);
const result = await chain.verifyChain(entries, hostPublicSlot); // { valid, firstBadIndex? }
```
## Constraints
- **Zero runtime dependency on `@suaveplan/crypto`**. `Authority`, `AeadOps`, `DomainTag`, `Rng`, and `KeySlotPublic` are types from `@suaveplan/types`; the caller injects the implementation (typically from `@suaveplan/crypto`). This keeps the package small and environment-agnostic.
- **All options are runtime-validated with Zod** before construction. Invalid `magic`, `formatVersion`, `key` length, or `signerCount` throws immediately at `define*` time.
- **Every operation is OTel-instrumented** with spans under tracer `"bitpacker-crypto"`. Span names: `bitpacker-crypto.signed-pack`, `.envelope-pack`, `.encrypted-unpack`, `.hashchain-verify`, etc.
- **Checksum helpers throw plain `Error`**; schema primitives throw `BitpackerError` with codes from `BITPACKER_CODES` (`CRYPTO_VERIFY_FAILED`, `CRYPTO_DECRYPT_FAILED`, `CRYPTO_ENVELOPE_MAGIC_MISMATCH`, `CRYPTO_ENVELOPE_VERSION_MISMATCH`, `CRYPTO_SCHEMA_FINGERPRINT_MISMATCH`, `CRYPTO_HASH_ALGORITHM_UNKNOWN`, `CRYPTO_MULTISIGN_COUNT_MISMATCH`, etc.).
- **`defaultHashRegistry()` is a lazy singleton**. For isolated tests, use `createHashRegistry()`. There is also `_resetDefaultHashRegistryForTesting()`.
- **Schema fingerprint rejects non-finite numbers and functions** in the schema (e.g. conditional schemas). The canonicalizer sorts object keys lexicographically and strips whitespace.
- **`SUPERSEDED.md` documents the intent to slim this package to checksums+fingerprint+registry only**, but the **actual code still exports all five crypto primitives** (SignedBitSchema, EncryptedBitSchema, EnvelopeSchema, HashChainSchema, MultiSignedBitSchema). The skill reflects the actual exported surface.
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/bitpacker-crypto` |
| Signed | `@suaveplan/bitpacker-crypto/signed` |
| Encrypted | `@suaveplan/bitpacker-crypto/encrypted` |
| Envelope | `@suaveplan/bitpacker-crypto/envelope` |
| Hash chain | `@suaveplan/bitpacker-crypto/hashchain` |
| Multi-signed | `@suaveplan/bitpacker-crypto/multisigned` |
| Fingerprint | `@suaveplan/bitpacker-crypto/fingerprint` |
| Hash registry | `@suaveplan/bitpacker-crypto/hash-registry` |
## Checklist
- [ ] Need fast non-cryptographic integrity → `crc32` / `adler32` / `calculateXxhash32` + append/verify helpers
- [ ] Need pluggable hash algorithms (sha256, xxhash32, crc32, custom) → `HashAlgorithmRegistry`
- [ ] Need to agree on schema shape between peers → `schemaFingerprint` / `assertSchemaFingerprint`
- [ ] Need a typed envelope (magic + version) around arbitrary bytes → `defineEnvelopeSchema`
- [ ] Need single-authority signatures on bit-packed payloads → `defineSignedBitSchema`
- [ ] Need N-of-N multi-signer signatures → `defineMultiSignedBitSchema`
- [ ] Need AEAD encryption of small bit-packed payloads → `defineEncryptedBitSchema`
- [ ] Need append-only signed hash chain (audit log / ledger) → `defineHashChainSchema`
- [ ] Composing multiple layers (Sign→Envelope→Encrypt) → use `BodyCodec` indirection at each layer

View File

@@ -0,0 +1,154 @@
---
id: sp-bitpacker-delta
name: BitPacker Delta
library: "@suaveplan/bitpacker-delta"
description: >
Delta encoding for efficient repeated state transmission in bit-packed
objects. Provides field-level change detection (DeltaManager), state
snapshot storage (DeltaStateManager), XOR numeric delta encoding, and
prefix/suffix string diff encoding. All state is keyed and deep-cloned
so multiple peers can share one manager without cross-contamination.
---
# `@suaveplan/bitpacker-delta`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **DeltaManager** | Stateful field-level change detector between successive object states | `DeltaManager`, `DeltaConfig`, `FieldChange`, `DeltaManagerOptions` |
| **DeltaStateManager** | Per-key snapshot store with LRU eviction and deep-cloning | `DeltaStateManager` |
| **String diff** | Prefix/suffix elimination for string deltas | `calculateStringDiff`, `applyStringDiff`, `shouldUseDiff`, `StringDiff` |
| **XOR encoder** | XOR delta for integers and float64 numbers | `calculateXorDelta`, `applyXorDelta`, `numberXor`, `isXorEfficient` |
## File Structure
```
src/
├── index.ts # Public barrel
├── delta-manager/
│ ├── delta-manager.ts # DeltaManager (field-level change detection)
│ └── delta-manager.test.ts # Full test coverage
├── delta-state/
│ ├── delta-state.ts # DeltaStateManager (LRU snapshot store)
│ └── delta-state.test.ts
├── string-diff.ts # calculateStringDiff, applyStringDiff
└── xor-encoder.ts # calculateXorDelta, numberXor
```
## Patterns
### Basic — Detect which fields changed between two ticks
```ts
import { DeltaManager } from "@suaveplan/bitpacker-delta";
import type { BitPackerSchema } from "@suaveplan/types";
const dm = new DeltaManager({ mode: "field-level" });
const schema = { x: "u16", y: "u16", label: "str" } as Record<string, BitPackerSchema>;
dm.storePreviousState({ x: 10, y: 20, label: "a" });
const changes = dm.detectChanges({ x: 10, y: 25, label: "b" }, schema);
// changes → [
// { index: 1, key: "y", previousValue: 20, currentValue: 25 },
// { index: 2, key: "label", previousValue: "a", currentValue: "b" }
// ]
```
### Basic — String diff (prefix/suffix elimination)
```ts
import { calculateStringDiff, applyStringDiff } from "@suaveplan/bitpacker-delta";
const diff = calculateStringDiff("hello world", "hello there");
// diff → { prefixLen: 6, suffixLen: 0, middleLen: 5, middle: "there" }
const reconstructed = applyStringDiff("hello world", diff);
// reconstructed → "hello there"
```
### Basic — XOR delta for integers
```ts
import { calculateXorDelta, applyXorDelta } from "@suaveplan/bitpacker-delta";
const delta = calculateXorDelta(0b1010_1100, 0b1010_0000); // 0b1100
const original = applyXorDelta(0b1010_0000, delta); // 0b1010_1100
```
### Advanced — Multi-peer delta manager (per-stateKey isolation)
```ts
const dm = new DeltaManager({ mode: "field-level" });
const schema = { pos: "u32" } as Record<string, BitPackerSchema>;
dm.storePreviousState({ pos: 100 }, "peer-A");
dm.storePreviousState({ pos: 200 }, "peer-B");
dm.detectChanges({ pos: 105 }, schema, "peer-A"); // detects change
dm.detectChanges({ pos: 200 }, schema, "peer-B"); // no change
```
### Advanced — Threshold-gated delta decision
```ts
const dm = new DeltaManager({
mode: "field-level",
threshold: 30, // only use delta if it saves ≥30% of bits
maxFieldCount: 64, // schema limit for the field-level bitmask
});
const originalBits = schema.totalBits;
const deltaBits = changedFields.length * fieldSize;
if (dm.shouldUseDelta(originalBits, deltaBits)) {
// send delta + changed-field bitmask
} else {
// send full state
}
```
### Advanced — Structural vs JSON equality modes
```ts
// Default: structural (schema-order-aware, key-order-independent)
const dm = new DeltaManager({ mode: "field-level", equalityMode: "structural" });
// { x: 1, y: 2 } vs { y: 2, x: 1 } → NO change detected
// Upstream-compat: JSON.stringify equality (key-order sensitive)
const dmJson = new DeltaManager({ mode: "field-level", equalityMode: "json" });
// { x: 1, y: 2 } vs { y: 2, x: 1 } → change detected
```
### Advanced — Using DeltaStateManager directly
```ts
import { DeltaStateManager } from "@suaveplan/bitpacker-delta";
const store = new DeltaStateManager(100); // max 100 keys, LRU eviction
store.storePreviousState("peer-1", { x: 1 });
const snapshot = store.getPreviousState("peer-1"); // deep-cloned copy
store.clearState("peer-1");
```
## Constraints
- **Zod-validated config** at construction. Unknown keys rejected; `mode` must be `"none" | "xor" | "string-diff" | "field-level"`. Invalid config throws `BITPACKER_CODES.DELTA_INVALID_CONFIG`.
- **Field-level mode limits**: `maxFieldCount` defaults to 64. Schemas with more fields throw `BITPACKER_CODES.DELTA_FIELD_LIMIT_EXCEEDED`.
- **Deep cloning on store**: `storePreviousState` deep-clones the value so caller mutations don't corrupt snapshots. Handles primitives, Date, all typed-array views, arrays, and plain objects. Maps/Sets are NOT cloned.
- **LRU eviction**: `DeltaStateManager` evicts the oldest key (by insertion order) when capacity is exceeded. Re-storing an existing key updates the value but does NOT advance the LRU position.
- **Equality modes**: `"structural"` (default) does deep comparison respecting typed arrays and plain objects; `"json"` uses `JSON.stringify` for upstream byte-compat.
- **First call returns empty**: `detectChanges` returns `[]` on the first call for any state key because there is no previous snapshot to diff against.
- **No subpath exports**: everything is imported from `@suaveplan/bitpacker-delta` directly.
- **Dependencies**: `@suaveplan/bitpacker`, `@suaveplan/error`, `@suaveplan/types`, `zod`, `tslib`.
## Checklist
- [ ] Need to know which fields changed between two object snapshots → `DeltaManager` with `mode: "field-level"`
- [ ] Need to diff strings efficiently (prefix/suffix elimination) → `calculateStringDiff` / `applyStringDiff`
- [ ] Need XOR delta for numeric fields → `calculateXorDelta` / `applyXorDelta`
- [ ] Need float64 XOR (e.g. for coordinate deltas) → `numberXor`
- [ ] Need to decide whether delta is worth sending → `DeltaManager.shouldUseDelta`
- [ ] Need per-peer/state isolation in one manager instance → pass `stateKey` to `storePreviousState` / `detectChanges`
- [ ] Need key-order-independent equality for nested objects → `equalityMode: "structural"` (default)
- [ ] Need upstream byte-compat (JSON.stringify equality) → `equalityMode: "json"`

View File

@@ -0,0 +1,221 @@
---
id: sp-bitpacker-game
name: BitPacker Game
library: "@suaveplan/bitpacker-game"
description: >
Game networking toolkit built on BitPacker. Provides client-side prediction,
server reconciliation, state interpolation (9 methods), position/rotation
prediction (8 algorithms including Kalman and adaptive), float quantization,
delta state packing, and network simulation integration. The main entry point
is GameBitPacker — a BitPacker subclass that adds game-state lifecycle,
sequencing, and feature orchestration.
---
# `@suaveplan/bitpacker-game`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **GameBitPacker** | Main class — extends `BitPacker` with game-state lifecycle, sequencing, delta packing, and feature delegation | `GameBitPacker` |
| **GameStateManager** | Tracks current/last-sent state, 4-bit sequence numbers, delta computation | `GameStateManager` |
| **GameInterpolator** | 9 interpolation methods for position, rotation, and full `PlayerState` | `GameInterpolator` |
| **PositionPredictor** | 8 prediction algorithms with adaptive switching and Kalman filter | `PositionPredictor`, `PredictionAlgorithm` |
| **RotationPredictor** | Rotation extrapolation (similar algorithms) | `RotationPredictor` |
| **StateReconciliation** | Client-side prediction + server reconciliation with jitter buffer and ping tracking | `StateReconciliation` |
| **Quantization** | Float→fixed-point compression and angle interpolation | `quantizePosition`, `quantizeRotation`, `interpolateAngle` |
| **Schemas** | Pre-built game schemas (position, rotation, stats, world update, input) | `PLAYER_STATE_SCHEMA`, `WORLD_UPDATE_SCHEMA`, `PLAYER_INPUT_SCHEMA` |
| **SchemaFactory** | Configurable schema builder | `createGameSchema`, `GameSchemaFactory` |
| **IntegrationManager** | Feature orchestrator — lazy-initializes prediction/interpolation/reconciliation/network simulation | `GameIntegrationManager` |
| **RegisterSchemas** | Zod schema registration with `@suaveplan/schema-registry` | `registerBitPackerGameSchemas` |
## File Structure
```
src/
├── index.ts # Public barrel
├── types.ts # Core types: Vector3, Rotation, PlayerState,
│ # PlayerInput, PredictionOptions, InterpolationOptions,
│ # ReconciliationOptions, GameBitPackerOptions
├── bitpacker/
│ └── bitpacker.ts # GameBitPacker (extends BitPacker)
├── state-manager/
│ ├── state-manager.ts # GameStateManager (sequences, delta computation)
│ └── state-manager.types.ts
├── interpolation/
│ └── interpolation.ts # GameInterpolator (9 methods)
├── prediction/
│ ├── prediction.ts # Barrel: PositionPredictor, RotationPredictor
│ ├── position-predictor.ts # 8 algorithms + adaptive switching
│ ├── rotation-predictor.ts
│ ├── prediction-algorithms.ts # linear, quadratic, cubic, hermite,
│ │ # catmull-rom, verlet predict functions
│ ├── kalman-filter.ts
│ └── motion-math.ts # velocity, acceleration, jerk calculations
├── reconciliation/
│ └── reconciliation.ts # StateReconciliation
├── quantization/
│ └── quantization.ts # quantizePosition, quantizeRotation,
│ # interpolateAngle, validateRange
├── schemas/
│ ├── schemas.ts # Pre-built schema constants
│ └── schemas.types.ts
├── schema-factory/
│ ├── schema-factory.ts # createGameSchema
│ └── schema-factory.types.ts
├── integration-manager/
│ ├── integration-manager.ts # Feature orchestrator
│ └── integration-manager.types.ts
├── register-schemas/
│ └── register-schemas.ts # Schema registry integration
└── types.schema/
└── types.schema.ts # Zod schemas for registry
```
## Patterns
### Basic — Create a game bit packer and send state
```ts
import { GameBitPacker } from "@suaveplan/bitpacker-game";
const gbp = new GameBitPacker(PLAYER_STATE_SCHEMA, {
delta: true,
enablePrediction: true,
enableInterpolation: true,
enableReconciliation: true,
});
gbp.update({ position: { x: 1, y: 2, z: 0 }, rotation: { yaw: 0, pitch: 0 }, stats: { ... } }, true);
// Send full state (initial)
const full = gbp.packState();
// Send delta (subsequent updates)
const delta = gbp.packDelta();
```
### Basic — Apply an incoming update
```ts
// Data format: [header byte][packed payload]
// Header: bit 7 = delta flag; bits 0-3 = sequence (0-15)
const success = gbp.applyUpdate(incomingBytes);
// Returns false if sequence mismatch (for delta) or unpack fails
```
### Basic — Interpolate between two states
```ts
import { GameInterpolator } from "@suaveplan/bitpacker-game";
const interp = new GameInterpolator({ method: "hermite", tension: 0.5 });
const pos = interp.interpolatePosition(startPos, endPos, 0.5, "cubic");
const rot = interp.interpolateRotation(startRot, endRot, 0.5);
const state = interp.interpolatePlayerState(startState, endState, 0.5);
```
### Basic — Predict future position
```ts
import { PositionPredictor, PredictionAlgorithm } from "@suaveplan/bitpacker-game";
const predictor = new PositionPredictor({
algorithm: PredictionAlgorithm.ADAPTIVE,
historySize: 10,
maxPredictionTime: 200,
});
predictor.addPosition({ x: 1, y: 2, z: 0 }, Date.now());
predictor.addPosition({ x: 2, y: 3, z: 0 }, Date.now() + 16);
const future = predictor.predictPosition(33); // 33ms ahead
```
### Advanced — Client-side prediction + server reconciliation
```ts
const gbp = new GameBitPacker(schema, { enableReconciliation: true });
// On client input
const seq = gbp.processInput(input);
// Predict locally
const predicted = gbp.predictClientState(currentState, applyPhysics, dt);
// On server state received
const needsReconcile = gbp.processServerUpdate(serverState, serverSeq, serverTimestamp);
// Reconcile: replay inputs since last server state
if (needsReconcile) {
const reconciled = gbp.reconcileState(currentState, applyPhysics);
}
```
### Advanced — Quantize floats before packing
```ts
// Reduce bandwidth by snapping to grid
const qPos = gbp.quantizePosition({ x: 1.23456, y: 2.78901, z: 0.5 }, 0.01);
// → { x: 1.23, y: 2.79, z: 0.5 }
const qRot = gbp.quantizeRotation({ yaw: 45.123, pitch: -10.987 }, 0.1);
// → { yaw: 45.1, pitch: -11.0 }
```
### Advanced — Custom schema via factory
```ts
import { createGameSchema } from "@suaveplan/bitpacker-game";
const schema = createGameSchema({
maxPosition: 2048,
positionPrecision: 3,
maxRotation: 360,
rotationPrecision: 2,
maxHealth: 200,
maxSpeed: 16,
});
// position.x → "-2048.3", rotation.yaw → "-360.2", etc.
```
### Advanced — Register Zod schemas with SchemaRegistry
```ts
import { registerBitPackerGameSchemas } from "@suaveplan/bitpacker-game/register-schemas";
registerBitPackerGameSchemas(mySchemaRegistry);
```
## Constraints
- **GameBitPacker extends BitPacker**: inherits all pack/unpack methods; adds `update`, `packState`, `packDelta`, `applyUpdate`, and game feature delegation.
- **4-bit sequence numbers**: `MAX_SEQUENCE = 15`. Delta packets validate sequence ordering; full-state packets do not.
- **Delta header format**: byte 0 has bit 7 (`DELTA_FLAG = 0x80`) set for deltas, bits 0-3 carry the sequence number.
- **Deep cloning everywhere**: state snapshots and returned states are deep-cloned to prevent caller mutations from corrupting internal state.
- **Feature flags are lazy**: `GameIntegrationManager` only instantiates `PositionPredictor`, `GameInterpolator`, `StateReconciliation`, and `NetworkSimulator` when their respective `enable*` option is true.
- **Network simulation delegated to `@suaveplan/bitpacker-network`**: `NetworkSimulator` lives in the network package; `bitpacker-game` re-exports its types and wires it via the integration manager.
- **Reconciliation uses `Date.now()`** for timestamps in `predictClientState` and server time offset calculations. This is the one place where determinism is sacrificed for real-time networking.
- **Prediction algorithms**: linear, quadratic, cubic, hermite, catmull-rom, velocity-verlet, kalman, adaptive. Adaptive switches between cubic/verlet/kalman based on prediction error.
- **Interpolation methods**: linear, cubic, hermite, catmull-rom, bezier, spring, elastic, bounce, smoothstep. Not all methods are implemented for rotation (rotation falls back to linear with angle wrapping for unsupported methods).
- **Dependencies**: `@suaveplan/bitpacker`, `@suaveplan/bitpacker-network`, `@suaveplan/events`, `@suaveplan/schema-registry`, `@suaveplan/types`, `@opentelemetry/api`, `zod`, `tslib`.
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/bitpacker-game` |
| Schema registry | `@suaveplan/bitpacker-game/register-schemas` |
## Checklist
- [ ] Need to pack/unpack game state with delta compression → `GameBitPacker`
- [ ] Need smooth movement between snapshots → `GameInterpolator` (position/rotation/PlayerState)
- [ ] Need to extrapolate future position from history → `PositionPredictor`
- [ ] Need client-side prediction with server reconciliation → `StateReconciliation` via `GameBitPacker`
- [ ] Need to quantize floats to fixed-point before packing → `quantizePosition`, `quantizeRotation`
- [ ] Need a pre-built player/world schema → `PLAYER_STATE_SCHEMA`, `WORLD_UPDATE_SCHEMA`
- [ ] Need a custom schema with configurable ranges → `createGameSchema`
- [ ] Need network simulation (latency, jitter, packet loss) → enable via `GameBitPacker` options (delegates to `@suaveplan/bitpacker-network`)
- [ ] Need to register Zod schemas with SchemaRegistry → `registerBitPackerGameSchemas`

View File

@@ -0,0 +1,179 @@
---
id: sp-bitpacker-network
name: BitPacker Network
library: "@suaveplan/bitpacker-network"
description: >
Network simulation primitives for bit-packed payload pipelines. Provides a
deterministic, replayable packet transport simulator with configurable
latency, jitter, packet loss, duplication, corruption, reordering, bandwidth
throttling, burstiness, and random disconnects. Seven named presets
(PERFECT through SATELLITE) cover common network conditions. All randomness
and time are injectable for test determinism.
---
# `@suaveplan/bitpacker-network`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **NetworkSimulator** | Full packet transport simulator with latency, loss, duplication, corruption, reordering, bandwidth caps, burstiness, disconnects | `NetworkSimulator`, `NetworkSimulatorConstructorOptions`, `NetworkSimulatorEvents` |
| **Factory** | Convenience constructor for preset-based simulators | `createNetworkSimulator` |
| **Types** | Presets, options, stats, packet metadata | `NetworkCondition`, `PacketStatus`, `NetworkSimulationOptions`, `NetworkSimulationStats`, `PacketInfo`, `NETWORK_CONDITIONS`, `DEFAULT_NETWORK_SIMULATION_OPTIONS` |
| **Legacy helpers** | Standalone latency/drop functions for back-compat | `simulateLatency`, `shouldDropPacket`, `applyNetworkConditions`, `NetworkConditions` |
| **Defaults** | Determinism injection points | `defaultClock`, `defaultRng` |
| **RegisterSchemas** | Zod schema registration with `@suaveplan/schema-registry` | `registerBitPackerNetworkSchemas` |
## File Structure
```
src/
├── index.ts # Public barrel + legacy free functions
├── types/
│ ├── types.ts # NetworkCondition, PacketStatus,
│ │ # NetworkSimulationOptions, presets
│ └── types.schema.ts # Zod schemas for registry
├── simulator/
│ ├── simulator.ts # NetworkSimulator + createNetworkSimulator
│ └── simulator.schema.ts # Zod schemas for constructor options
├── defaults/
│ ├── default-clock.ts # () => Date.now()
│ └── default-rng.ts # () => cryptoRng()
├── register-schemas/
│ └── register-schemas.ts # SchemaRegistry integration
└── otel/
└── instruments.ts # OTel metrics
```
## Patterns
### Basic — Simulate a named network condition
```ts
import { NetworkSimulator, NetworkCondition } from "@suaveplan/bitpacker-network";
const sim = new NetworkSimulator({ condition: NetworkCondition.POOR });
sim.on("packet:received", (p) => console.log("got", p.id));
sim.on("packet:lost", (p) => console.log("lost", p.id));
sim.sendPacket(new Uint8Array([1, 2, 3]), (status, data) => {
// status → PacketStatus.DELIVERED | LOST | DUPLICATED | CORRUPTED | REORDERED
});
// Later
const stats = sim.getStats(); // packetsSent, packetsLost, averageLatency, throughputKbps, ...
sim.destroy();
```
### Basic — Factory with preset
```ts
import { createNetworkSimulator, NetworkCondition } from "@suaveplan/bitpacker-network";
const sim = createNetworkSimulator(NetworkCondition.MOBILE);
```
### Advanced — Deterministic tests with seeded RNG and fake clock
```ts
import { NetworkSimulator } from "@suaveplan/bitpacker-network";
import { Mulberry32Rng } from "@suaveplan/pseudo-random";
import type { Clock, Rng } from "@suaveplan/types";
const rng: Rng = new Mulberry32Rng(0x12345678);
const clock: Clock = () => 0;
const sim = new NetworkSimulator({
rng,
clock,
condition: NetworkCondition.AVERAGE,
});
// Every packet fate is reproducible across runs
```
### Advanced — Custom condition with bandwidth cap
```ts
const sim = new NetworkSimulator({
latencyMin: 20,
latencyMax: 50,
jitter: 10,
packetLoss: 2,
packetDuplication: 0.5,
corruption: 0.1,
packetReordering: 1,
bandwidthKbps: 5000,
enableThrottling: true,
enableBurstiness: true,
burstInterval: 5000,
burstPacketLoss: 25,
disconnectChance: 5, // per-minute disconnect probability
});
```
### Advanced — Disconnect/reconnect control
```ts
sim.disconnect(); // Sends return -1, in-flight packets dropped
sim.reconnect(); // Restores normal operation
sim.isConnected(); // boolean
sim.on("connection:disconnected", () => {});
sim.on("connection:reconnected", () => {});
```
### Advanced — Reconfigure a running simulator
```ts
sim.setOptions({ condition: NetworkCondition.TERRIBLE });
// Preset spreads then explicit overrides applied
sim.setOptions({ packetLoss: 0, enableBurstiness: false });
// Custom mode merges into existing options
```
### Advanced — Legacy free-function helpers
```ts
import { simulateLatency, shouldDropPacket, applyNetworkConditions } from "@suaveplan/bitpacker-network";
const delayMs = simulateLatency({ latencyMs: 50, jitterMs: 10 });
const dropped = shouldDropPacket({ packetLossRate: 0.05 });
const maybePayload = applyNetworkConditions(payload, conditions);
// All three accept an optional `Rng` for determinism
```
## Constraints
- **Injectable determinism**: `rng` defaults to `cryptoRng()` from `@suaveplan/random`; `clock` defaults to `Date.now()`. Tests should inject a seeded `Rng` and a fake `Clock`.
- **7 named presets**: `PERFECT`, `GOOD`, `AVERAGE`, `POOR`, `TERRIBLE`, `MOBILE`, `SATELLITE`. `CUSTOM` resolves to `DEFAULT_NETWORK_SIMULATION_OPTIONS`. Setting `condition` spreads the preset first, then explicit overrides apply.
- **Percentages are 0-100**: `packetLoss`, `packetDuplication`, `corruption`, `packetReordering`, `disconnectChance`, `burstPacketLoss`.
- **Bandwidth cap**: `enableThrottling: true` + `bandwidthKbps > 0`. Window is 1 second; exceeding it emits `bandwidth:throttled` and drops the packet.
- **Burstiness**: `enableBurstiness: true` enters a 1-second high-loss window every `burstInterval` ms. `burstPacketLoss` overrides `packetLoss` during bursts.
- **Random disconnects**: `disconnectChance` is per-minute probability. Disconnect time is exponentially distributed; reconnect happens after 5-15 seconds.
- **Data is deep-copied on send**: the caller's buffer is not shared with the simulator.
- **Stat overflow guard**: `packetsSent` near `MAX_SAFE_INTEGER` throws `BITPACKER_CODES.NETWORK_STAT_OVERFLOW`.
- **Error codes**: `NETWORK_BANDWIDTH_EXCEEDED`, `NETWORK_DISCONNECTED`, `NETWORK_STAT_OVERFLOW`, `NETWORK_INVALID_OPTIONS`.
- **destroy() is single-shot**: calling twice is a no-op. Clears all timers, queues, and listeners.
- **Dependencies**: `@suaveplan/bitpacker`, `@suaveplan/random`, `@suaveplan/schema-registry`, `@suaveplan/types`, `@opentelemetry/api`, `zod`, `tslib`.
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/bitpacker-network` |
| Schema registry | `@suaveplan/bitpacker-network/register-schemas` |
## Checklist
- [ ] Need to test multiplayer code under realistic network conditions → `NetworkSimulator` with a named `NetworkCondition` preset
- [ ] Need reproducible packet-loss patterns in tests → inject seeded `Rng` (e.g. `Mulberry32Rng`)
- [ ] Need to simulate bandwidth throttling → `enableThrottling: true` + `bandwidthKbps`
- [ ] Need periodic burst loss windows → `enableBurstiness: true` + `burstInterval` + `burstPacketLoss`
- [ ] Need random disconnect/reconnect cycles → `disconnectChance` (per-minute probability)
- [ ] Need packet duplication or corruption → `packetDuplication` / `corruption` (0-100)
- [ ] Need to observe packet fates → subscribe to `packet:received`, `packet:lost`, `packet:corrupted`, etc.
- [ ] Need standalone latency/jitter calculation → `simulateLatency(conditions, rng?)`
- [ ] Need to register Zod schemas with SchemaRegistry → `registerBitPackerNetworkSchemas`

View File

@@ -0,0 +1,180 @@
---
name: sp-bitpacker-schema
description: Schema registry with pluggable storage (in-memory/localStorage), VersionedSchema with migrations, runtime validation, schema matching, and multi-format visualization for @suaveplan/bitpacker.
---
# BitPacker Schema for Suaveplan
Schema lifecycle management for `@suaveplan/bitpacker`. Provides a versioned schema registry with pluggable storage backends, runtime data validation against schemas, declarative migration plans, schema-to-data matching, and visualization in JSON/HTML/Markdown/Text/DOT formats.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `SchemaRegistry` | Async registry over `SchemaRegistryStorage` — auto-increments version per `(namespace, name)`, stamps timestamps |
| `InMemorySchemaRegistryStorage` | In-process array-backed storage (default) |
| `LocalStorageSchemaRegistryStorage` | Browser `localStorage` adapter with injectable store for tests |
| `VersionedSchema` | Holds multiple schema versions, migration graph, packs/unpacks with version wrapping, auto-migrates on unpack |
| `validateData` / `assertValid` | Runtime validation: numbers, strings, booleans, objects, arrays, enums, optional, oneOf, Map, Set, Uint8Array, bitarrays, bitsets, version, conditional, refs, custom `_bp` types |
| `SchemaMatchingService` | Compares schema to data, reports additions (extra fields), subtractions (missing fields), mismatches (wrong types) |
| `MigrationPlanBuilder` | Fluent builder for 8 migration strategies: add, remove, rename, change-type, move, split, merge, custom |
| `visualizeSchema` | Generates schema docs in JSON / HTML / Markdown / Text / DOT (Graphviz) with bit-size calculation |
## File Structure
```
foundation/bitpacker/bitpacker-schema/
src/registry/registry.ts # SchemaRegistry class
src/registry/storage/in-memory/ # InMemorySchemaRegistryStorage
src/registry/storage/local-storage/# LocalStorageSchemaRegistryStorage
src/registry/factories/factories.ts# createInMemorySchemaRegistry, createLocalStorageSchemaRegistry
src/versioning/versioning.ts # VersionedSchema, extractVersion, hasVersionInfo
src/validation/validation.ts # validateData, assertValid
src/matching/matching.ts # SchemaMatchingService
src/migration/migration.ts # MigrationPlanBuilder, applyMigrationPlan, createMigrationFunction
src/visualization/visualization.ts # visualizeSchema (+ JSON/HTML/Markdown/Text/DOT helpers)
```
Sibling packages:
- `@suaveplan/bitpacker` — core packer/unpacker (peer dependency)
- `@suaveplan/bitpacker-strategies` — packing strategies used by VersionedSchema's internal BitPacker
- `@suaveplan/bitpacker-validation` — separate validation and safety limits package
## Patterns
### Schema Registry
```typescript
import { SchemaRegistry, createInMemorySchemaRegistry } from "@suaveplan/bitpacker-schema";
const registry = new SchemaRegistry(); // defaults to in-memory storage
// Register auto-versioned schema (version = 1, or latest+1)
await registry.registerSchema(
{ name: "-16", health: 255 },
{ namespace: "game", name: "player" }
);
// Resolve latest or specific version
const entry = await registry.getSchema("game", "player"); // latest
const v1 = await registry.getSchema("game", "player", 1); // exact version
// List and delete
const all = await registry.listSchemas("game");
await registry.deleteSchema("game", "player");
```
### VersionedSchema with Migrations
```typescript
import { VersionedSchema } from "@suaveplan/bitpacker-schema";
const vs = new VersionedSchema(
{ x: 255, y: 255 }, // v1 schema
{ name: "point", namespace: "game" }
);
// Add new version
vs.addVersion({ x: 255, y: 255, z: 255 }, "Added Z coordinate");
// Register migration path
vs.addMigration(1, 2, (data) => ({ ...data, z: 0 }), "Add default Z");
// Pack with version wrapping (auto-detect version on unpack + auto-migrate)
const packed = vs.pack({ x: 1, y: 2, z: 3 }); // Uint8Array
const unpacked = vs.unpack(packed); // { x: 1, y: 2, z: 3 }
// If packed with v1 and autoMigrate is on, unpack migrates to v2
```
### Runtime Validation
```typescript
import { validateData, assertValid } from "@suaveplan/bitpacker-schema";
const report = validateData(42, 255);
if (!report.valid) {
console.error(report.errors); // [{ path: "", message: "..." }]
}
assertValid(42, 255); // ok
assertValid(300, 255); // throws BitpackerError("INVALID_SCHEMA", ...)
// Object validation
const objReport = validateData(
{ x: 10, y: "hello" },
{ x: 255, y: "-8" }
);
```
### Schema Matching
```typescript
import { SchemaMatchingService } from "@suaveplan/bitpacker-schema";
const matcher = new SchemaMatchingService();
const result = matcher.match(
{ x: 255, y: "-8" },
{ x: 10, y: "hello", extra: true }
);
// result.additions → ["extra"] (field in data but not schema)
// result.subtractions → [] (field in schema but not data)
// result.mismatches → [] (type mismatches)
```
### Migration Plan Builder
```typescript
import { createMigrationPlan } from "@suaveplan/bitpacker-schema";
const plan = createMigrationPlan(1, 2, "Add health field")
.addField("health", 100)
.renameField("name", "displayName")
.build();
// Apply to data
const migrated = applyMigrationPlan({ name: "alice" }, plan);
// → { displayName: "alice", health: 100 }
```
### Schema Visualization
```typescript
import {
visualizeSchema,
SchemaVisualizationFormat,
visualizeSchemaAsHtml,
} from "@suaveplan/bitpacker-schema";
const result = visualizeSchema(
{ x: 255, y: 255, name: "-16" },
{
format: SchemaVisualizationFormat.HTML,
includeBitSizes: true,
theme: "dark",
title: "Player Schema",
}
);
// result.content → HTML string
// result.mimeType → "text/html"
// result.fileExtension → "html"
```
## Constraints
- `SchemaRegistry` is async; all read/write methods return Promises.
- `VersionedSchema` constructs an internal `BitPacker` instance unless one is injected via `options.bitPacker`.
- `VersionedSchema.unpack` with `includeVersion: true` reads the version prefix first, then unpacks the full payload. If `autoMigrate` is true and the packed version is older than latest, it runs the migration chain.
- `validateData` validates `_ref` schemas as always-valid (cannot resolve without registry context).
- `MigrationPlanBuilder.addToSchema()` is a convenience that builds the plan, creates a migration function, and registers it on a `VersionedSchema` in one call.
- `SchemaMatchingService.matchPrimitiveValue` treats integer schemas (`255`) as matching any number (integer check only, not range check). Range validation is `validateData`'s job.
- Visualization bit-size calculation is heuristic-based: string lengths use `ceil(log2(length+1))` for length prefix + `length * 8` for content; fixed-point numbers split on decimal point.
## Checklist
1. Use `createInMemorySchemaRegistry()` for tests and ephemeral registries; use `createLocalStorageSchemaRegistry()` for browser persistence.
2. Use `VersionedSchema` when schemas evolve over time and you need backward-compatible pack/unpack with automatic migration.
3. Use `validateData` before packing to catch mismatches early; use `assertValid` when validation failure is a hard error.
4. Use `SchemaMatchingService` to detect schema drift (new fields added to data that aren't in the schema yet).
5. Use `MigrationPlanBuilder` for declarative schema migrations; prefer it over hand-written migration functions.
6. Use `visualizeSchema` with `SchemaVisualizationFormat.DOT` to generate Graphviz diagrams of complex nested schemas.

View File

@@ -0,0 +1,199 @@
---
name: sp-bitpacker-strategies
description: All built-in packing strategies for @suaveplan/bitpacker — numeric, string, array, object, enum, optional, varint, conditional, version, defs, custom types, and more.
---
# BitPacker Strategies for Suaveplan
Strategy library that plugs into `@suaveplan/bitpacker`'s `PackingStrategyManager`. Register once, then all strategies are available for compact schema notation packing and unpacking.
## Architecture
| Layer | Technology |
|-------|-----------|
| Strategy manager | `PackingStrategyManager` (from `@suaveplan/bitpacker`) |
| Strategy implementations | 20+ strategies covering all common types |
| Registration | `registerDefaultStrategies(manager)` — one-shot registration |
| Custom strategies | Implement `PackingStrategy` and call `manager.register(...)` |
## File Structure
```
foundation/bitpacker/bitpacker-strategies/
src/register-default-strategies.ts # registerDefaultStrategies entry point
src/strategies/
number/ # NumberPackingStrategy — uint, signed, fixed-point
string/ # StringPackingStrategy — fixed, variable-length
boolean/ # BooleanPackingStrategy
null/ # NullPackingStrategy
bigint/ # BigIntPackingStrategy
bigint-array/ # BigIntArrayPackingStrategy
object/ # ObjectPackingStrategy
array/ # ArrayPackingStrategy
array-config/ # ArrayConfigPackingStrategy (explicit config objects)
array-notation/ # ArrayNotationPackingStrategy (string notation)
array-schema/ # ArraySchemaPackingStrategy (fixed-length tuples)
enum/ # EnumPackingStrategy
optional/ # OptionalPackingStrategy
optional-marker/ # OptionalMarkerPackingStrategy
optional-suffix/ # OptionalSuffixPackingStrategy (`?` suffix)
oneOf/ # OneOfPackingStrategy
union/ # UnionPackingStrategy
varint/ # VarIntPackingStrategy (VLQ)
datetime/ # DateTimePackingStrategy
map/ # MapPackingStrategy
set/ # SetPackingStrategy
bitarray/ # BitArrayPackingStrategy
blob/ # BlobPackingStrategy
bitset/ # BitsetPackingStrategy
conditional/ # ConditionalPackingStrategy
version/ # VersionPackingStrategy
defs/ # DefsPackingStrategy (schema definitions)
customType/ # CustomTypePackingStrategy
```
Sibling packages:
- `@suaveplan/bitpacker` — core packer/unpacker and strategy manager
- `@suaveplan/bitpacker-compressed` — compression pipeline
- `@suaveplan/bitpacker-streaming` — streaming APIs
## Patterns
### Register Default Strategies
```typescript
import { BitPacker } from "@suaveplan/bitpacker";
import { registerDefaultStrategies } from "@suaveplan/bitpacker-strategies";
const bp = new BitPacker();
registerDefaultStrategies(bp.strategyManager);
// Pack a number
const packed = bp.pack(42, 255); // uint8
const unpacked = bp.unpack(packed, 255); // 42
```
### Number Strategies
```typescript
bp.pack(42, 255); // unsigned 8-bit
bp.pack(-42, -127); // signed (range -127..127)
bp.pack(4.2, 4.2); // fixed-point float
bp.pack(12345, "varint"); // variable-length unsigned integer
bp.pack(-12345, "varint-signed"); // signed varint
bp.pack(9007199254740993n, "bigint-u64"); // unsigned 64-bit bigint
bp.pack(-9007199254740993n, "bigint-i64"); // signed 64-bit bigint
```
### String Strategies
```typescript
bp.pack("hello", "5"); // fixed 5 bytes (padded)
bp.pack("hello", "-16"); // variable length, max 16 bytes
bp.pack("uuid", "uuid"); // 16-byte UUID
```
### Object and Array Strategies
```typescript
// Object
const obj = { x: 42, y: 99 };
const packed = bp.pack(obj, { x: 255, y: 255 });
// Array of numbers
const arr = [1, 2, 3];
const packedArr = bp.pack(arr, [255]);
// Array notation
const packedNot = bp.pack([10, 20, 30], "-5[255]");
// Fixed-length tuple
const packedTuple = bp.pack([1, "a"], { _bp: "arraySchema", schemas: [255, "1"] });
```
### Enum, Optional, OneOf
```typescript
// Enum
bp.pack("red", "enum:red|green|blue");
// Optional (suffix)
bp.pack("hello", "8?"); // optional fixed 8-byte string
bp.pack(undefined, "8?"); // single bit: absent
// oneOf (discriminated union)
bp.pack(
{ kind: "circle", radius: 5 },
{ _bp: "oneOf", schemas: [
{ _bp: "custom", discriminator: "circle", schema: { radius: 255 } },
{ _bp: "custom", discriminator: "rect", schema: { width: 255, height: 255 } },
]}
);
```
### Advanced: Conditional, Version, Defs
```typescript
// Conditional packing
bp.pack(
{ flag: true, value: 42 },
{ _bp: "conditional", condition: "flag", then: { value: 255 }, else: null }
);
// Versioned schema
bp.pack(
{ version: 1, data: { x: 42 } },
{ _bp: "version", versions: [
{ version: 1, schema: { x: 255 } },
{ version: 2, schema: { x: 255, y: 255 } },
]}
);
// Schema definitions (defs)
bp.pack(
{ name: "alice", items: [{ name: "bob" }] },
{ _bp: "defs", definitions: { person: { name: "-16" } }, schema: { name: "-16", items: "-4[person]" } }
);
```
### Custom Strategy
```typescript
import { PackingStrategy } from "@suaveplan/bitpacker";
class MyStrategy implements PackingStrategy {
canHandle(schema: unknown): boolean {
return schema === "my-type";
}
pack(data: unknown, schema: unknown, context: unknown): void {
// write via context.writer
}
unpack(schema: unknown, context: unknown): unknown {
// read via context.reader
return data;
}
}
bp.strategyManager.register(new MyStrategy());
```
## Constraints
- `registerDefaultStrategies` is idempotent — calling it multiple times on the same manager is safe.
- Strategy order matters: the first strategy whose `canHandle()` returns `true` wins. Register custom strategies after defaults if they override standard schemas.
- `ArrayConfigPackingStrategy` and `ArrayNotationPackingStrategy` are separate from the basic `ArrayPackingStrategy`; use the one that matches your schema style.
- `OptionalSuffixPackingStrategy` uses a `?` suffix only on primitive schema strings, not on object schemas.
- `ConditionalPackingStrategy` evaluates a field from the object to decide which branch to pack.
- `VersionPackingStrategy` packs a version discriminator, then the matching schema branch.
- `DefsPackingStrategy` allows recursive schema references; define named schemas and reference them in the main schema.
## Checklist
1. Call `registerDefaultStrategies(bp.strategyManager)` before packing.
2. Choose the right number strategy: literal integer for fixed-width, `-N` for signed, `"varint"` for compact integers.
3. Use `"-length[schema]"` for array notation; `{ type: "array", ... }` for explicit config.
4. Use `"enum:a|b|c"` for enums; `"8?"` for optional values.
5. Use `{ _bp: "oneOf", schemas: [...] }` for discriminated unions with custom discriminators.
6. Use `{ _bp: "conditional", condition, then, else }` for conditional field presence.
7. Use `{ _bp: "defs", definitions, schema }` for recursive or reusable schema definitions.
8. Implement `PackingStrategy` for custom types, then register with `bp.strategyManager.register(...)`.

View File

@@ -0,0 +1,197 @@
---
id: sp-bitpacker-streaming
name: BitPacker Streaming
library: "@suaveplan/bitpacker-streaming"
description: >
Streaming pack/unpack primitives for BitPacker. Provides chunked packing
(packToChunks), WHATWG ReadableStream/WritableStream wrappers,
TransformStream pipelines, and length-prefix/delimiter/fixed-size framing.
All entry points support per-chunk processors (e.g. compress, encrypt),
progress callbacks, AbortSignal, and injectable Clock for test determinism.
---
# `@suaveplan/bitpacker-streaming`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **StreamingBitPacker** | Class façade over chunked pack/unpack with 5 streaming entry points | `StreamingBitPacker`, `StreamingPackOptions`, `StreamingUnpackOptions` |
| **Simple chunks** | Synchronous chunking without streams | `packToChunks`, `unpackFromChunks`, `unpackFromChunksAsync` |
| **Transform streams** | WHATWG TransformStream pipelines for pack/unpack | `createPackTransformStream`, `createUnpackTransformStream`, `createFramedPackStream`, `createFramedUnpackStream` |
| **Framing** | Frame encoding/decoding for stream delimiting | `addFrame`, `parseFrames`, `FramingConfig` |
| **Types** | Option bundles and re-exports from `@suaveplan/types` | `PackTransformOptions`, `UnpackTransformOptions`, `TransformProgress` |
| **RegisterSchemas** | Zod schema registration with `@suaveplan/schema-registry` | `registerBitPackerStreamingSchemas` |
## File Structure
```
src/
├── index.ts # Public barrel + simple chunk functions
├── streaming-bit-packer/
│ ├── streaming-bit-packer.ts # StreamingBitPacker class
│ └── streaming-bit-packer.schema.ts # Zod schemas
├── transform/
│ ├── transform.ts # TransformStream factories + framing
│ ├── transform.types.ts # Option bundles
│ └── transform.schema.ts # Zod schemas
├── defaults/
│ └── default-clock.ts # () => Date.now()
└── register-schemas/
└── register-schemas.ts # SchemaRegistry integration
```
## Patterns
### Basic — Pack to chunks and reassemble
```ts
import { packToChunks, unpackFromChunks } from "@suaveplan/bitpacker-streaming";
const chunks = packToChunks(data, schema, { chunkSize: 512 });
const recovered = unpackFromChunks<MyType>(chunks, schema);
```
### Basic — StreamingBitPacker pack/unpack with progress
```ts
import { StreamingBitPacker } from "@suaveplan/bitpacker-streaming";
const sp = new StreamingBitPacker(schema);
const bytes = await sp.packStream(data, {
chunkSize: 1024,
onProgress: (p) => console.log(p.itemsProcessed, p.bytesProcessed, p.throughput),
});
const recovered = await sp.unpackStream<MyType>(bytes);
```
### Basic — ReadableStream from packed bytes
```ts
const stream = sp.createReadableStream(packedBytes, { chunkSize: 256 });
// Pipe to compression, network, etc.
for await (const chunk of stream) {
socket.send(chunk);
}
```
### Advanced — Writable pack stream (collect multiple items)
```ts
const { stream, result } = sp.createPackingStream();
const writer = stream.getWriter();
await writer.write(item1);
await writer.write(item2);
await writer.close();
const allBytes = await result; // concatenated packed bytes
```
### Advanced — Writable unpack stream (collect bytes, unpack on close)
```ts
const { stream, result } = sp.createUnpackingStream<MyType>();
const writer = stream.getWriter();
await writer.write(chunk1);
await writer.write(chunk2);
await writer.close();
const recovered = await result;
```
### Advanced — TransformStream pipeline
```ts
import {
createPackTransformStream,
createUnpackTransformStream,
} from "@suaveplan/bitpacker-streaming";
const packStream = createPackTransformStream(bitPacker, schema, { batchSize: 10 });
const unpackStream = createUnpackTransformStream(bitPacker, schema);
packStream.readable.pipeTo(unpackStream.writable);
const writer = packStream.writable.getWriter();
await writer.write(data);
await writer.close();
const reader = unpackStream.readable.getReader();
const { value } = await reader.read();
```
### Advanced — Length-prefix framed streams
```ts
import {
createFramedPackStream,
createFramedUnpackStream,
} from "@suaveplan/bitpacker-streaming";
const packStream = createFramedPackStream(bp, schema, {
type: "length-prefix",
lengthPrefixSize: 4,
});
const unpackStream = createFramedUnpackStream(bp, schema, {
type: "length-prefix",
lengthPrefixSize: 4,
});
packStream.readable.pipeTo(unpackStream.writable);
```
### Advanced — Chunk processor (e.g. XOR encryption)
```ts
const xor = (chunk: Uint8Array): Uint8Array => {
const out = new Uint8Array(chunk.length);
for (let i = 0; i < chunk.length; i++) out[i] = (chunk[i] ?? 0) ^ 0xff;
return out;
};
const wire = await sp.packStream(data, { chunkProcessor: xor });
const recovered = await sp.unpackStream(wire, { chunkProcessor: xor });
```
### Advanced — Framing utilities directly
```ts
import { addFrame, parseFrames } from "@suaveplan/bitpacker-streaming";
const framed = addFrame(data, { type: "length-prefix", lengthPrefixSize: 2 });
const { frames, remainder } = parseFrames(buffer, { type: "delimiter", delimiter: new Uint8Array([0xff]) });
```
## Constraints
- **StreamingBitPacker registers default strategies by default**. Pass `{ registerDefaults: false }` to the constructor if you are registering custom strategies yourself.
- **chunkProcessor runs on every chunk** in both pack and unpack directions. For symmetric transforms (e.g. XOR, AES), use the same processor in both directions. For asymmetric transforms (e.g. compress/decompress), provide the inverse processor on unpack.
- **AbortSignal support**: `packStream`, `unpackStream`, and `unpackFromChunksAsync` accept an `AbortSignal`. Aborted operations throw `BITPACKER_CODES.STREAM_ABORTED`.
- **chunkProcessor errors** are wrapped as `BITPACKER_CODES.STREAM_CHUNK_PROCESSOR_FAILED` with the original cause and chunk byte length in context.
- **Empty unpacking stream** (close with no bytes) rejects with `BITPACKER_CODES.STREAM_INCOMPLETE_DATA`.
- **Injectable Clock**: all progress callbacks accept an optional `clock` parameter. Default is `defaultClock` (`Date.now()`). Tests should inject a fake clock for determinism.
- **Framing types**: `length-prefix` (1/2/4 bytes, big-endian), `delimiter` (custom byte sequence), `fixed-size` (pad/truncate to fixed length), `none` (pass-through).
- **parseFrames returns `{ frames, remainder }`** — incomplete frames stay in remainder for the next buffer append.
- **TransformStream factories require a BitPacker instance** — they do not create one internally. You must pass your own `BitPacker` (with strategies registered).
- **Dependencies**: `@suaveplan/bitpacker`, `@suaveplan/bitpacker-strategies`, `@suaveplan/schema-registry`, `@suaveplan/types`, `@opentelemetry/api`, `zod`, `tslib`.
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/bitpacker-streaming` |
| Schema registry | `@suaveplan/bitpacker-streaming/register-schemas` |
## Checklist
- [ ] Need to split packed bytes into chunks → `packToChunks` / `unpackFromChunks`
- [ ] Need async chunk reassembly from an async iterator → `unpackFromChunksAsync`
- [ ] Need a WHATWG ReadableStream of packed bytes → `StreamingBitPacker.createReadableStream`
- [ ] Need to write items to a stream and get packed bytes on close → `createPackingStream`
- [ ] Need to write raw bytes to a stream and get unpacked data on close → `createUnpackingStream`
- [ ] Need a TransformStream pipeline for pack/unpack → `createPackTransformStream` / `createUnpackTransformStream`
- [ ] Need length-prefix or delimiter framing for stream delimiting → `createFramedPackStream` / `createFramedUnpackStream`
- [ ] Need to process each chunk (encrypt, compress) → `chunkProcessor` option
- [ ] Need progress tracking (items/bytes/throughput) → `onProgress` callback
- [ ] Need to cancel an in-flight operation → pass `AbortSignal`
- [ ] Need to register Zod schemas with SchemaRegistry → `registerBitPackerStreamingSchemas`

View File

@@ -0,0 +1,152 @@
---
name: sp-bitpacker-validation
description: Schema validation and safety limits for @suaveplan/bitpacker — validate data before packing, enforce recursion/array/object/float safety limits, and produce typed error reports.
---
# BitPacker Validation for Suaveplan
Validation and safety layer for `@suaveplan/bitpacker`. Validate that data matches a schema before packing, enforce production safety limits against pathological inputs, and get typed, path-aware error reports.
## Architecture
| Layer | Technology |
|-------|-----------|
| Stateful validator | `BitPackerValidator<T>` — bind once, validate many |
| Free-function validation | `validateData(schema, data, path, ctx)` — recursive workhorse |
| Safety checks | `SafetyChecker` — limits on buffer, recursion, arrays, objects, floats |
| Error reporting | `ValidationReport` — `{ valid, errors, warnings }` |
| Error codes | `ErrorCodes` — stable typed codes (`INVALID_TYPE`, `OUT_OF_RANGE`, `MISSING_FIELD`, `EXTRA_FIELD`, `ARRAY_TOO_LONG`) |
## File Structure
```
foundation/bitpacker/bitpacker-validation/
src/validation/
validator.ts # BitPackerValidator<T>
validate-data.ts # validateData recursive walker
find-extra-fields.ts # Extra-field detector
find-array-length-errors.ts # Array length-rule detector
src/safety/
safety-checks.ts # SafetyChecker, RecursionGuard, DEFAULT_SAFETY_LIMITS
src/types/
types.ts # ValidationReport, ValidationReportItem, ErrorCodes, BitPackerValidationError
```
Sibling packages:
- `@suaveplan/bitpacker` — core packer/unpacker
- `@suaveplan/bitpacker-strategies` — packing strategies
## Patterns
### Stateful Validator (High Throughput)
```typescript
import { BitPackerValidator } from "@suaveplan/bitpacker-validation";
const validator = new BitPackerValidator(
{ x: 255, name: "-32" },
{ strictMode: true, checkExtraFields: true, maxErrors: 5 }
);
// Boolean fast path
if (validator.isValid(data)) {
bp.pack(data, schema);
}
// Detailed report
const report = validator.validate(data);
if (!report.valid) {
console.warn(report.errors);
}
// Throw on first error
validator.validateOrThrow(data);
```
### Free-Function Validation
```typescript
import { validateData, type ValidationReportItem } from "@suaveplan/bitpacker-validation";
const errors: ValidationReportItem[] = [];
validateData(
{ x: 255, name: "-32" },
{ x: 100, name: "ok" },
"", // root path
{ errors, warnings: [], maxErrors: 10, checkExtraFields: true, strictMode: false }
);
```
### Safety Checks
```typescript
import { SafetyChecker, DEFAULT_SAFETY_LIMITS } from "@suaveplan/bitpacker-validation";
const checker = new SafetyChecker(DEFAULT_SAFETY_LIMITS);
// Default limits: maxBufferSize=10MB, maxRecursionDepth=100, maxArrayLength=10_000,
// maxObjectKeys=1_000, allowInfiniteFloats=false, allowNaNFloats=false
checker.checkArray(new Array(5000)); // ok
checker.checkArray(new Array(50_000)); // throws SafetyError(ARRAY_TOO_LARGE)
checker.checkFloat(NaN); // throws SafetyError(INVALID_FLOAT)
```
### Recursion Guard
```typescript
import { SafetyChecker, createRecursionGuard } from "@suaveplan/bitpacker-validation";
const checker = new SafetyChecker({ maxRecursionDepth: 32 });
const guard = createRecursionGuard(checker);
const result = guard.withGuard(() => {
return deeplyNestedUnpack(schema, data);
});
```
### Typed Error Handling
```typescript
import { BitPackerValidationError, ErrorCodes } from "@suaveplan/bitpacker-validation";
try {
validator.validateOrThrow(data);
} catch (err) {
if (err instanceof BitPackerValidationError) {
switch (err.code) {
case ErrorCodes.MISSING_FIELD:
console.error(`Missing field: ${err.path}`);
break;
case ErrorCodes.OUT_OF_RANGE:
console.error(`Out of range: ${err.path} (expected ${err.expected}, got ${err.received})`);
break;
case ErrorCodes.EXTRA_FIELD:
console.warn(`Extra field: ${err.path}`);
break;
}
}
}
```
## Constraints
- `BitPackerValidator` binds a schema at construction time; construct a new instance to switch schemas.
- `validateData` never throws — it accumulates errors into mutable arrays. Callers decide whether to throw based on results.
- `maxErrors` short-circuits traversal once the limit is reached; data is NOT fully walked on heavily malformed input.
- `strictMode` promotes warnings to errors.
- `checkExtraFields` defaults to `true`; object schemas reject keys not declared in the schema.
- `SafetyChecker` metrics are aggregate across the instance lifetime; reset or create new instances for per-call metrics.
- `RecursionGuard.withGuard` is exception-safe — depth counter always decrements via `try/finally`.
- Six published `SafetyError` codes: `BUFFER_OVERFLOW`, `RECURSION_OVERFLOW`, `ARRAY_TOO_LARGE`, `OBJECT_TOO_LARGE`, `INVALID_FLOAT`, `CUSTOM_VALIDATION_FAILED`.
## Checklist
1. Use `BitPackerValidator` for high-throughput pipelines validating the same schema repeatedly.
2. Use `validateData` directly for one-off validation or custom error accumulation.
3. Use `validator.isValid(data)` for boolean-only fast paths.
4. Use `validator.validateOrThrow(data)` when validation failure is a hard error.
5. Enable `checkExtraFields` to catch undeclared object keys.
6. Set `maxErrors` to cap cost on malformed input.
7. Use `SafetyChecker` at public boundaries to guard against pathological schemas and data.
8. Use `createRecursionGuard` around unpack paths that may encounter attacker-controlled recursive schemas.
9. Assert on `ErrorCodes` constants, not string literals, for forward compatibility.

View File

@@ -0,0 +1,151 @@
---
name: sp-bitpacker
description: Core bit-level binary packing library. BitWriter, BitReader, BitPacker with pluggable strategy manager, schema registry, and legacy BasePacker hierarchy.
---
# BitPacker for Suaveplan
Schema-driven binary serialization at the bit level. `BitPacker` is the façade: you hand it data and a compact schema, it returns a `Uint8Array`. The reverse unpacks back to data. Underneath, `BitWriter` and `BitReader` handle sub-byte precision, and a pluggable `PackingStrategyManager` resolves schemas to strategies.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `BitPacker` | Façade — `pack(data, schema)` → `Uint8Array`, `unpack(data, schema)` → unknown |
| `BitWriter` | Append-only bit buffer — `writeUint`, `writeInt`, `writeBool`, `writeBytes`, `flush` |
| `BitReader` | Sequential bit reader — `readUint`, `readInt`, `readBool`, `readBytes`, `peek`, `seek` |
| `PackingStrategyManager` | Chain-of-responsibility resolver — `find(schema)` returns first matching `PackingStrategy` |
| `BitOperations` | Static bit-twiddling helpers — `bitsRequired`, `popcount`, `getBits`, `setBits` |
| `BasePacker` | Abstract legacy base class for per-type packers (IntegerPacker, StringPacker, etc.) |
| Schema Registry | Named schema store for `$ref` resolution at pack/unpack time |
## File Structure
```
foundation/bitpacker/bitpacker/
src/core/bitpacker.ts # BitPacker class
src/core/strategies/manager.ts # PackingStrategyManager
src/core/strategies/types.ts # PackingStrategy, PackingContext interfaces
src/bit-writer/bit-writer.ts # BitWriter
src/bit-reader/bit-reader.ts # BitReader
src/packers/bit-operations/ # BitOperations static helpers
src/packers/base/ # BasePacker (legacy abstract base)
src/packers/*/ # Legacy per-type packers
src/performance/ # BufferPool, LRUCache, PerformanceMonitor
src/types/ # Schema type system, type guards
src/bit-schema.ts # Legacy defineBitSchema
```
Sibling packages:
- `@suaveplan/bitpacker-strategies` — 20+ modern packing strategies (numbers, strings, arrays, enums, etc.)
- `@suaveplan/bitpacker-schema` — SchemaRegistry with versioning and migrations
- `@suaveplan/bitpacker-validation` — Safety limits and validation reports
## Patterns
### Quick Start
```typescript
import { BitPacker } from "@suaveplan/bitpacker";
import { registerDefaultStrategies } from "@suaveplan/bitpacker-strategies";
const bp = new BitPacker();
registerDefaultStrategies(bp.strategyManager);
const schema = { x: 255, y: "-16", active: true };
const packed = bp.pack({ x: 100, y: "hello", active: false }, schema);
const unpacked = bp.unpack(packed, schema);
```
### BitWriter / BitReader (Low-Level)
```typescript
import { BitWriter, BitReader } from "@suaveplan/bitpacker";
const writer = new BitWriter();
writer.writeUint(42, 8); // 8 bits
writer.writeBool(true); // 1 bit
writer.writeInt(-3, 4); // 4 bits (two's complement)
writer.writeBytes(new Uint8Array([1, 2, 3]));
const bytes = writer.flush(); // Uint8Array
const reader = new BitReader(bytes);
const n = reader.readUint(8); // 42
const b = reader.readBool(); // true
const i = reader.readInt(4); // -3
const buf = reader.readBytes(); // [1, 2, 3]
```
### Custom Strategy
```typescript
import { PackingStrategy, PackingContext } from "@suaveplan/bitpacker";
class U24Strategy implements PackingStrategy {
canHandle(schema: unknown): boolean {
return schema === "u24";
}
pack(data: unknown, schema: unknown, ctx: PackingContext): void {
ctx.writer.writeUint(data as number, 24);
}
unpack(schema: unknown, ctx: PackingContext): unknown {
return ctx.reader.readUint(24);
}
}
bp.strategyManager.registerStrategy(new U24Strategy());
const packed = bp.pack(0xabcdef, "u24");
```
### Schema Registry ($ref)
```typescript
bp.registerSchema("point", { x: 255, y: 255 });
const packed = bp.pack({ x: 1, y: 2 }, { $ref: "point" });
```
### BitOperations Helpers
```typescript
import { BitOperations } from "@suaveplan/bitpacker";
BitOperations.bitsRequired(255); // 8
BitOperations.bitsRequired(256); // 9
BitOperations.popcount(0xff); // 8
BitOperations.getBits(0b1101_0011, 4, 4); // 13 (high nibble)
```
### Legacy BasePacker
```typescript
import { BasePacker, BitWriter, BitReader } from "@suaveplan/bitpacker";
class CustomU24Packer extends BasePacker {
pack(data: number, writer: BitWriter): void {
writer.writeUint(data, 24);
}
unpack(reader: BitReader): number {
return reader.readUint(24);
}
}
```
## Constraints
- `BitWriter` uses a 53-bit safe internal register. Values requiring more than 53 bits must be split across multiple writes.
- `BitReader` validates bounds on every read; attempting to read past `byteLength` throws `BitpackerError`.
- Strategy order matters: `PackingStrategyManager.find()` returns the first match. Register custom strategies before the catch-all `InvalidSchemaStrategy`.
- `BasePacker` is legacy — new code should implement `PackingStrategy` and register it with the manager.
- `BitOperations` methods are 32-bit safe (JS bitwise operators coerce to int32). Use bigint-specific helpers for wider operations.
- `bitsRequired(0)` returns 1 (zero is representable in one bit).
- Schema registry is in-memory only; persist named schemas externally if needed across sessions.
## Checklist
1. Import `BitPacker` and register strategies via `registerDefaultStrategies(bp.strategyManager)` before packing.
2. Use compact schema notation: `255` (uint8), `-127` (signed), `4.2` (fixed-point), `"-16"` (var-length string), `true` (boolean).
3. For sub-byte precision, use `BitWriter` / `BitReader` directly.
4. For custom types, implement `PackingStrategy` and call `bp.strategyManager.registerStrategy(...)`.
5. For reusable schemas, use `bp.registerSchema(name, schema)` and reference with `{ $ref: "name" }`.
6. For bit-twiddling utilities, use `BitOperations` (static, allocation-free).
7. Prefer the strategy chain over `BasePacker` for new packing logic.

View File

@@ -0,0 +1,223 @@
---
name: sp-booking
description: Universal booking engine with pluggable availability, conflict detection, pricing, rules, and six domain presets.
---
# Booking for Suaveplan
Universal, JSON-configurable booking engine that orchestrates availability resolution, conflict detection, pricing calculation, rule evaluation, and status lifecycle management. Ships six built-in domain presets (seats, accommodation, appointments, resources, activities, transport), a typed event bus, and an in-package `Hooks<TMap>` data-transform pipeline.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `BookingEngine` | Orchestrator — composes bookable registry, bookings, holds, waitlist, state machine, event bus, hooks, and pricing pipeline. Entry points: `configure()`, `registerBookable()`, `checkAvailability()`, `confirmBooking()`, `cancelBooking()`, `modifyBooking()`, `createHold()`, `joinWaitlist()` |
| `StateMachine` | Configurable status transitions — `canTransition()` enforces role guards, reason requirements, and auto-transition metadata. JSON-driven via `StatusTransition[]` |
| `EventBus` | Typed publish/subscribe for 15 standard `BookingEventType` events — `on()`, `off()`, `emit()`, `listenerCount()` |
| `Hooks<TMap>` | In-package transform pipeline — handlers sorted by priority, each can mutate or abort data. Sync (`applyHooks`) and async (`applyHooksAsync`, `applyHooksWithResult`) variants |
| `AvailabilityResolver` | Pluggable registry — `registerAvailabilityStrategy()` + `resolveAvailability()` delegates to registered `AvailabilityStrategyFn` |
| `ConflictDetector` | Mode-based registry — `registerDetector()` + `detectConflicts()` dispatches to exact_match, range_overlap, slot_capacity, or provider_overlap strategies |
| `PricingCalculator` | Pipeline — `calculateBasePrice()` → `applyTiers()` → `applyModifiers()` → `calculateTax()` → rounding. Supports fixed, per_unit, per_duration, tiered models |
| `RuleEvaluator` | Walks `BookingRule[]` against `EvaluationContext` — 11 operators (eq, neq, gt, gte, lt, lte, in, not_in, between, regex), `all`/`any` logic, deny or apply_modifier actions |
| `BookingError` | Typed error hierarchy extending `SuaveplanError` — codes are `BOOKING.ENGINE_NOT_CONFIGURED`, `BOOKING.BOOKING_CONFLICT`, `BOOKING.TRANSITION_NOT_ALLOWED`, etc. via `defineCodes` |
| Domain factories (`createSeatConfig`, `createAccommodationConfig`, …) | Pre-built `BookingSystemConfig` presets with domain-specific conflict modes, attribute schemas, and pricing defaults |
## File Structure
```
commerce/booking/booking/
src/core/engine/ # BookingEngine orchestrator
src/core/state-machine/ # Configurable status transitions
src/core/event-bus/ # Typed lifecycle event bus
src/availability/resolver/ # Pluggable strategy registry
src/availability/strategies/ # Built-in strategies (exactMatch, rangeOverlap, capacity)
src/conflict/detector/ # Mode-based conflict detection registry
src/conflict/exact-match/ # Seat-style exact time conflict
src/conflict/range-overlap/ # Accommodation-style any overlap conflict
src/conflict/slot-capacity/ # Group booking capacity conflict
src/conflict/provider-overlap/ # Appointment-style provider calendar conflict
src/pricing/calculator/ # Price calculation pipeline
src/pricing/dynamic/ # Dynamic pricing rules
src/pricing/seasonal/ # Seasonal rate multipliers
src/rules/evaluator/ # Rule engine evaluator
src/rules/conditions/ # Pre-built condition factories
src/domains/ # Six domain presets (seats, accommodation, appointments, resources, activities, transport)
src/temporal/ # TimeWindow, slots, recurrence utilities
src/schemas/ # Zod validation schemas
src/hooks/ # Data-transform hook pipeline
src/error-codes/ # BOOKING_CODES and BookingError
src/types/ # TypeScript type definitions
src/utils/ # Shared pure helpers
```
Sibling packages:
- `@suaveplan/contacts` — Contact/audience management within the commerce vertical.
- `@suaveplan/error` — Typed error hierarchy foundation; `BookingError` extends `SuaveplanError`.
## Patterns
### Quick Start
```typescript
import { createBookingEngine, createSeatConfig } from "@suaveplan/booking";
const engine = createBookingEngine();
engine.configure(createSeatConfig("arena-1", "Main Arena"));
engine.registerBookable({
identifierRef: { id: "seat-A12", tenantId: "arena-1", categoryId: "vip" },
name: "Seat A12",
description: "Aisle seat",
bookableType: "seat",
status: "active",
layoutNodeRef: null,
capacityRef: "cap-A12",
priceRuleRefs: [],
timeSlotRefs: [],
attributes: {},
});
const result = engine.checkAvailability({
bookableRef: "seat-A12",
windowRef: {
startTime: "2026-06-01T19:00:00Z",
endTime: "2026-06-01T22:00:00Z",
timezone: "UTC",
},
quantity: 1,
});
console.log(result.available);
```
### Domain Preset Configuration
```typescript
import { createAccommodationConfig, createAppointmentConfig } from "@suaveplan/booking";
const hotelCfg = createAccommodationConfig("hotel-1", "Grand Hotel", {
pricingRef: { defaultCurrency: "EUR" },
});
// conflictRef.mode is "range_overlap", defaultPricingModel is "per_duration"
const clinicCfg = createAppointmentConfig("clinic-1", "City Clinic");
// conflictRef.mode is "provider_overlap", suited for provider calendars
```
### Pricing Pipeline
```typescript
import { calculatePrice } from "@suaveplan/booking/pricing/calculator/calculator";
import type { PriceCalculationContext } from "@suaveplan/booking/pricing/calculator/calculator.types";
const ctx: PriceCalculationContext = {
bookableRef: "room-101",
windowRef: {
startTime: "2026-06-01T14:00:00Z",
endTime: "2026-06-03T14:00:00Z",
timezone: "UTC",
},
quantity: 2,
rules: [{ ruleId: "r1", amount: 50, currency: "USD", pricingModel: "per_unit", unitDuration: "" }],
tiers: [],
modifiers: [],
currency: "USD",
taxStrategy: "exclusive",
roundingMode: "round",
roundingPrecision: 2,
};
const price = calculatePrice(ctx);
// price.subtotal === 100, price.tax === 10, price.total === 110
```
### Rule Evaluation
```typescript
import { evaluateRules } from "@suaveplan/booking/rules/evaluator/evaluator";
import type { EvaluationContext } from "@suaveplan/booking/rules/evaluator/evaluator.types";
import type { BookingRule, RuleCondition } from "@suaveplan/booking/types/rules.types";
const context: EvaluationContext = {
booking: { quantity: 5, status: "pending" },
bookable: { attributes: { tier: "vip" } },
window: { durationHours: 4, isWeekend: true },
customer: { bookingCount: 12 },
};
const conditions: RuleCondition[] = [
{ conditionId: "c1", field: "bookable.attributes.tier", operator: "eq", value: "vip" },
{ conditionId: "c2", field: "booking.quantity", operator: "gt", value: 100 },
];
const rule: BookingRule = {
ruleId: "r1",
name: "r1",
priority: 1,
enabled: true,
conditionRefs: ["c1", "c2"],
conditionLogic: "any",
actionRef: { actionType: "deny", message: "Not allowed" },
};
const result = evaluateRules([rule], conditions, context);
// result.passed === true because "any" logic and c1 is true
```
### Hooks Transform
```typescript
import { createBookingEngine, createSeatConfig } from "@suaveplan/booking";
const engine = createBookingEngine();
engine.configure(createSeatConfig("t1", "T1"));
engine.bookingHooks.hook("booking:creating", (data) => ({
...data,
customerRef: data.customerRef.toUpperCase(),
notes: "hook-set",
}));
const b = engine.confirmBooking({
bookableRef: "b1",
customerRef: "c1",
windowRef: { startTime: "2026-06-01T10:00:00Z", endTime: "2026-06-01T11:00:00Z", timezone: "UTC" },
quantity: 1,
});
// b.customerRef === "C1", b.notes === "hook-set"
```
### Temporal Window Operations
```typescript
import { windowsOverlap, mergeWindows, splitWindow } from "@suaveplan/booking/temporal/windows/windows";
const a = { startTime: "2026-06-01T10:00:00Z", endTime: "2026-06-01T12:00:00Z", timezone: "UTC" };
const b = { startTime: "2026-06-01T11:00:00Z", endTime: "2026-06-01T13:00:00Z", timezone: "UTC" };
windowsOverlap(a, b); // true
mergeWindows([a, b]); // merged into single window
splitWindow(a, 3600000); // splits into 1-hour chunks
```
## Constraints
- **Engine must be configured before use** — Any call to `registerBookable()`, `checkAvailability()`, `confirmBooking()`, etc. before `configure()` throws `BookingError` with code `BOOKING.ENGINE_NOT_CONFIGURED`.
- **All error codes are namespaced `BOOKING.*`** — Defined via `defineCodes("BOOKING", [...])` from `@suaveplan/error`. Never hand-write literal strings.
- **Hooks sync `applyHooks()` throws on Promise return** — If a handler returns a thenable, `BOOKING.HOOK_RETURNED_PROMISE_IN_SYNC` is raised. Use `applyHooksAsync()` for async handlers.
- **State machine transitions are JSON-configured** — No hard-coded statuses; every valid transition must appear in `BookingSystemConfig.statusTransitions` with `from`, `to`, `allowedRoles`, and optional `requiresReason`.
- **Tree-shaking requires subpath imports** — Full barrel import pulls the entire package. Use `@suaveplan/booking/pricing/calculator/calculator`, `@suaveplan/booking/temporal/windows/windows`, etc. for granular bundles.
- **Pricing defaults to 10% exclusive tax** — When no tax modifiers are present and `taxStrategy` is `"exclusive"`, `calculateTax()` returns `subtotal * 0.1`. Use `"inclusive"` for zero tax.
- **Domain presets merge over `createBaseConfig`** — Factories like `createSeatConfig` override `conflictRef`, `engineRef`, and `bookableTypeRegistry` but accept a `Partial<BookingSystemConfig>` as final overrides.
- **Conflict detection is registry-based** — Calling `detectConflicts()` with an unregistered mode throws `BOOKING.UNKNOWN_CONFLICT_MODE`. Built-in modes are registered automatically by the engine.
## Checklist
1. Call `engine.configure()` with a `BookingSystemConfig` (from a domain factory or manually built) before any operation.
2. Use domain factories (`createSeatConfig`, `createAccommodationConfig`, `createAppointmentConfig`, etc.) for ready-made presets with correct conflict modes and attributes.
3. Catch `BookingError` and dispatch on `err.code` using the `BOOKING_CODES` constant — never compare raw strings.
4. Register custom availability strategies via `registerAvailabilityStrategy()` before `resolveAvailability()` is invoked.
5. Register custom conflict detectors via `registerDetector()` before `detectConflicts()` is invoked with a custom mode.
6. Use subpath imports (e.g. `@suaveplan/booking/pricing/calculator/calculator`) for tree-shaking in production bundles.
7. Apply pre-persistence transforms via `engine.bookingHooks.hook("booking:creating", …)` or `"booking:cancelling"`.
8. Validate external inputs at boundaries with `validateBookingRequest()`, `validateBooking()`, or the Zod schemas from `@suaveplan/booking/schemas/booking.schema`.
9. Emit and observe lifecycle events via `engine.on("booking:confirmed", …)` — 15 event types are available including `hold:created`, `booking:cancelled`, `waitlist:joined`.

View File

@@ -0,0 +1,108 @@
---
name: sp-broadcast-channel
description: Typed cross-tab messaging with leader election, tab presence, Zod-validated boundaries, and StorageLike fallback for Safari < 15.4
---
# sp-broadcast-channel
## Architecture
| Class / Function | Responsibility |
|---|---|
| `TypedBroadcastChannel` | High-level cross-tab messaging object wrapping `ChannelLike` with leader election, peer presence, and typed `send`/`on` surface. |
| `HeartbeatRunner` | Manages heartbeat and cleanup interval pair for tab presence tracking. |
| `AskController` | Request-side engine for cross-tab request/response primitive. |
| `AskResponderRegistry` | Response-side registry mapping inbound `handlerKey` to a single handler. |
| `nativeChannelFactory` | Thin adapter exposing global `BroadcastChannel` as `ChannelLike`. |
| `createLocalStorageChannelFactory` | `StorageLike`-backed fallback transport for Safari < 15.4 and sandboxed environments. |
| `BROADCAST_CHANNEL_CODES` | Stable error registry (`CHANNEL_CLOSED`, `MESSAGE_MALFORMED`, `PAYLOAD_TOO_LARGE`, `STORAGE_FAILED`). |
| `ChannelMessageSchema` | Zod boundary schema validating every inbound frame before dispatch. |
## File Structure
```
networking/events/broadcast-channel/src/
ask/ — AskController + AskResponderRegistry
channel/ — TypedBroadcastChannel + HeartbeatRunner
errors/ — BroadcastChannelError + BROADCAST_CHANNEL_CODES
id/ — generateId (16-char hex)
logger/ — noopLogger default
otel/ — OTel counters (message.sent, tab.joined, leader.elected, etc.)
schemas/ — Zod schemas for ChannelMessage, AskFrame, ChannelConfig
transport/ — nativeChannelFactory + createLocalStorageChannelFactory
types/ — BroadcastChannelEventMap, ChannelConfig, TabInfo, etc.
```
## Sibling packages
- `sp-event-sourcing` — event sourcing patterns in same networking/events domain
- `sp-events` — typed event-bus substrate
- `sp-events-metrics` — metrics interceptor for events
- `sp-events-otel` — OTel interceptor for events
- `sp-events-schema` — schema-validated envelopes for events
- `sp-events-transport` — transport bridge for events
## Patterns
### Quick Start — typed cross-tab messaging
```ts
import { nativeChannelFactory, TypedBroadcastChannel } from "@suaveplan/broadcast-channel";
const channel = new TypedBroadcastChannel({
config: { name: "app-bus" },
factory: nativeChannelFactory,
});
channel.open();
channel.on<{ userId: string }>("user:signed-in", (msg) => {
console.log(`peer tab ${msg.senderId} signed in as ${msg.payload.userId}`);
});
channel.send("user:signed-in", { userId: "u_123" });
```
### Fallback transport for Safari < 15.4
```ts
import {
createLocalStorageChannelFactory,
isNativeBroadcastChannelAvailable,
nativeChannelFactory,
TypedBroadcastChannel,
} from "@suaveplan/broadcast-channel";
const factory = isNativeBroadcastChannelAvailable()
? nativeChannelFactory
: createLocalStorageChannelFactory(yourStorageLike, { pollIntervalMs: 50, retentionMs: 30_000 });
const channel = new TypedBroadcastChannel({ config: { name: "app-bus" }, factory });
```
### Ask pattern — request/response across tabs
```ts
import { AskController, AskResponderRegistry } from "@suaveplan/broadcast-channel";
const controller = new AskController(channel, { timeoutMs: 5000 });
const responder = new AskResponderRegistry(channel);
responder.register("get-user", async (ctx) => ({ user: await fetchUser(ctx.payload.id) }));
const response = await controller.ask("get-user", { id: "u1" });
```
## Constraints
- Browser-tier package — requires `BroadcastChannel` API or `StorageLike` fallback.
- `send()` throws `BroadcastChannelClosedError` before `open()` or after `close()`.
- Payloads exceeding `config.maxMessageBytes` (default 1 MiB) throw `BroadcastChannelPayloadTooLargeError`.
- Malformed inbound frames are dropped, warned, and counted — handlers are never invoked.
- Leader election uses lowest-tab-ID algorithm; only `non-leader -> leader` transitions emit `leader:elected`.
- Injectable `Clock`, `Rng`, and `LoggerLike` for deterministic tests.
- 100% statement + branch + function + line coverage required.
## Checklist
- [ ] `bunx turbo lint typecheck test --filter=@suaveplan/broadcast-channel` passes
- [ ] `bunx turbo test:coverage --filter=@suaveplan/broadcast-channel` reports 100%
- [ ] `bun run docs:check --filter=@suaveplan/broadcast-channel` passes
- [ ] Every `src/**/*.md` has a sibling `.ts` module with matching exports

View File

@@ -0,0 +1,113 @@
---
name: sp-bytes
description: Low-level byte utilities for Suaveplan — hex/base64/utf8 encoding, Uint8Array helpers, and UUID conversion.
---
# Byte utilities for Suaveplan
Low-level byte utilities for Suaveplan: lossless hex/base64/UTF-8 encoding, `Uint8Array` manipulation, and UUID conversion. No native addons; depends only on platform globals (`TextEncoder`, `atob`/`btoa`, `crypto.randomUUID`) and `@suaveplan/error` for typed errors.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `bytesToHex` / `hexToBytes` | Lowercase hex ↔ `Uint8Array` conversion with strict validation (odd length or non-hex chars throw `BYTES.ENCODE.INVALID_HEX`) |
| `bytesToBase64` / `base64ToBytes` | Standard base64 ↔ `Uint8Array` via platform `btoa`/`atob`; regex pre-validation before decode |
| `bytesToUtf8` / `utf8ToBytes` | UTF-8 ↔ `Uint8Array` via platform `TextDecoder`/`TextEncoder`; handles multi-byte Unicode correctly |
| `convertDataFormat` | Unified cross-format conversion façade; auto-detects input format for strings via `detectInputFormat` |
| `concatUint8Arrays` / `sliceUint8Array` / `uint8ArraysEqual` | `Uint8Array` manipulation: concatenation, copying slice, and linear-scan equality |
| `generateUuid` / `uuidToUint8Array` / `uint8ArrayToUuid` | UUIDv4 generation via `crypto.randomUUID()` and canonical 16-byte binary conversion |
| `BytesError` | Typed error class extending `SuaveplanError<BytesCode>`; codes registered via `defineCodes("BYTES", [...])` |
## File Structure
```
foundation/data/bytes/
src/encoding/ # hex/base64/utf8 encode/decode with strict validation
src/uint8/ # Uint8Array concat, slice, equality, format detection/conversion
src/uuid/ # UUIDv4 generation and canonical string ↔ 16-byte conversion
src/errors/ # BytesError and BYTES_CODES registry
src/types/ # InputFormat, OutputFormat, BytesInput type aliases
```
Sibling packages:
- `@suaveplan/validation` — runtime Zod-compatible schema validation
- `@suaveplan/serialize` — structured serialization (tagged JSON, MessagePack)
- `@suaveplan/mime` — MIME-type detection and Content-Type parsing
- `@suaveplan/packer` — binary packing with encryption and signatures
## Patterns
### Quick Start
```typescript
import {
bytesToHex, hexToBytes,
bytesToBase64, base64ToBytes,
bytesToUtf8, utf8ToBytes,
concatUint8Arrays,
convertDataFormat,
generateUuid, uuidToUint8Array, uint8ArrayToUuid,
} from "@suaveplan/bytes";
// Encoding
const raw = utf8ToBytes("Hello"); // Uint8Array [72,101,108,108,111]
const hex = bytesToHex(raw); // "48656c6c6f"
const b64 = bytesToBase64(raw); // "SGVsbG8="
const text = bytesToUtf8(raw); // "Hello"
// Uint8 helpers
const frame = concatUint8Arrays(raw, new Uint8Array([0x21]));
// Unified conversion
const asBase64 = convertDataFormat("48656c6c6f", "base64", "hex"); // "SGVsbG8="
// UUID
const id = generateUuid(); // "550e8400-e29b-41d4-a716-446655440000"
const bytes = uuidToUint8Array(id); // Uint8Array (16 bytes)
const back = uint8ArrayToUuid(bytes); // → id
```
### Cross-Format Conversion with Auto-Detection
```typescript
import { convertDataFormat, detectInputFormat } from "@suaveplan/bytes/uint8";
// Auto-detects hex string and converts to utf8
const text = convertDataFormat("48656c6c6f", "utf8"); // "Hello"
// Explicit input format
const hex = convertDataFormat("Hello", "hex", "utf8"); // "48656c6c6f"
// Verify detection heuristic
const fmt = detectInputFormat("deadbeef"); // "hex" (even-length, all hex chars)
```
### Typed Error Handling
```typescript
import { BytesError, BYTES_CODES } from "@suaveplan/bytes/errors";
import { hexToBytes } from "@suaveplan/bytes/encoding";
try {
hexToBytes("abc"); // odd length
} catch (err) {
if (err instanceof BytesError) {
console.error(err.code); // "BYTES.ENCODE.INVALID_HEX"
}
}
```
## Constraints
- Every exported function wraps its body in a named OTel active span under the `@suaveplan/bytes` tracer; spans are always ended in `finally` blocks
- `uint8ArraysEqual` is **not** constant-time — do not use it for cryptographic MAC verification (use `@suaveplan/crypto-utils` instead)
- `detectInputFormat` uses a conservative heuristic: even-length all-hex strings are classified as `"hex"`, which may misclassify legitimate UTF-8 strings like `"deadbeef"`
- The `./schemas` subpath in `package.json` maps to `src/types/index.ts` and only re-exports type declarations; actual Zod schemas (`HexStringSchema`, `Base64StringSchema`, `UuidStringSchema`, etc.) live in `./encoding`, `./uint8`, and `./uuid`
- All codecs throw `BytesError` with namespaced codes from `BYTES_CODES` on malformed input; never hand-write a raw `"BYTES.ENCODE.INVALID_HEX"` string — always use `BYTES_CODES["ENCODE.INVALID_HEX"]`
## Checklist
1. Verify the `exports` map in `package.json` includes all subpaths (`encoding`, `uint8`, `uuid`, `errors`, `schemas`) before adding new modules
2. Run `bunx turbo lint typecheck test build --filter=@suaveplan/bytes` and ensure 100% coverage before declaring changes complete
3. When adding a new error code, register it in `src/errors/bytes-error.ts` via `defineCodes("BYTES", [...])` and grep for hand-typed literals to update

View File

@@ -0,0 +1,106 @@
---
name: sp-cache-redis
description: AbstractCache backend over a minimal RedisLike interface with native TTL, SCAN-based clear, and pluggable codecs.
---
# Redis Cache Backend for Suaveplan
`@suaveplan/cache-redis` is the server-tier cache backend that plugs any Redis-compatible client (`ioredis`, `node-redis`, KeyDB, Dragonfly) into the unified `AbstractCache<T>` surface. TTL is owned by the Redis server via native `SET ... EX`, so there are no client-side header bytes or eviction tables. A side-effect import auto-registers `RedisCache` as the `"redis"` provider on `CacheFactory`.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `RedisCache<T>` | `AbstractCache<T>` subclass that speaks to Redis via `RedisLike`; implements `get`, `set`, `delete`, `has`, `clear`, `getStats` with native TTL and SCAN-based namespaced clearing. |
| `RedisLike` | Minimal 7-method interface (`set`, `get`, `del`, `exists`, `ttl`, `expire`, `scan`) plus optional `mget`; adapters bridge `ioredis`, `node-redis`, or custom RESP3 implementations. |
| `CACHE_REDIS_CODES` | `defineCodes("CACHE_REDIS", [...])` registry with 5 codes: `CONNECTION_FAILED`, `COMMAND_FAILED`, `CODEC_ERROR`, `KEY_TOO_LONG`, `INVALID_TTL`. |
| `RedisCacheError` | Structured error class extending `SuaveplanError<CacheRedisCode>`; carries `key` and `command` discriminants for programmatic failure classification. |
| `registerRedisProvider()` | Idempotently registers `RedisCache` as the `"redis"` provider on the shared `CacheFactory` registry; runs automatically on package import because `package.json` lists the module in `sideEffects`. |
## File Structure
```
persistence/cache/cache-redis/
src/errors/ # CACHE_REDIS_CODES + RedisCacheError
src/redis-cache/ # RedisCache class + RedisCacheOptions type
src/redis-like/ # RedisLike interface + RedisScanResult / RedisSetOptions / RedisScanOptions
src/registry-augment/ # CacheProviderRegistry augmentation + side-effect auto-registration
```
Sibling packages:
- `@suaveplan/cache` — `AbstractCache<T>`, `CacheFactory`, memory caches (LRU/LFU/FIFO/TTL), and lifecycle events.
- `@suaveplan/cache-storage` — storage-backed cache with a fixed-width TTL header and shared `Codec<T>` contracts (`JsonCodec`, `IdentityCodec`).
## Patterns
### Quick Start
```typescript
import { RedisCache } from "@suaveplan/cache-redis";
import { Redis } from "ioredis";
const cache = new RedisCache<{ id: string; email: string }>({
redis: new Redis({ host: "redis.example.com" }),
namespace: "users",
defaultTtl: 5 * 60_000,
});
await cache.set("alice", { id: "1", email: "alice@example.com" });
const user = await cache.get("alice");
```
### Factory Registration
```typescript
import "@suaveplan/cache-redis"; // side-effect: register provider
import { createCache } from "@suaveplan/cache";
import { Redis } from "ioredis";
const cache = createCache("redis", { redis: new Redis() });
```
### Binary Data with IdentityCodec
```typescript
import { RedisCache } from "@suaveplan/cache-redis";
import { IdentityCodec } from "@suaveplan/cache-storage";
const cache = new RedisCache<Uint8Array>({
redis: new Redis(),
codec: new IdentityCodec(),
});
await cache.set("k", new Uint8Array([1, 2, 3]));
const out = await cache.get("k");
```
### Error Classification
```typescript
import { CACHE_REDIS_CODES, RedisCacheError } from "@suaveplan/cache-redis/errors";
try {
await cache.set("k", value);
} catch (err) {
if (err instanceof RedisCacheError && err.code === CACHE_REDIS_CODES.CONNECTION_FAILED) {
metrics.increment("cache.redis.connection_failure");
}
}
```
## Constraints
- TTL values in milliseconds are converted to whole seconds via `Math.ceil(ttl / 1000)`; sub-second TTLs round up to `EX: 1` so a positive TTL is never silently dropped.
- `clear()` requires a non-empty `namespace`; calling it without one throws `CACHE_REDIS_CODES.COMMAND_FAILED` to prevent accidental whole-DB wipes.
- Keys are capped at 1024 characters; oversize keys throw `CACHE_REDIS_CODES.KEY_TOO_LONG`.
- `getStats().size` is always `0` because Redis `DBSIZE` is whole-DB, and per-namespace counting via `SCAN` is O(N).
- `IdentityCodec` values are base64-encoded on the wire; `JsonCodec` values are sent as UTF-8 strings to avoid the 33% base64 overhead.
- No `node:` or `bun:` imports in `src/` — the package is runtime-portable.
## Checklist
1. Verify `package.json` `exports` includes `.`, `./errors`, `./redis-like`, and `./redis-cache`.
2. Confirm `sideEffects` lists both `./src/index.ts` and `./src/registry-augment/registry-augment.ts` so bundlers preserve the provider registration.
3. When adding a new `RedisLike` adapter, ensure `scan` returns `{ cursor: string; keys: string[] }` with `cursor === "0"` signalling end of iteration.
4. Check that every new error code is added to `CACHE_REDIS_CODES` and exercised by a real call site in `errors.test.ts`.
5. Run `bunx turbo lint typecheck test build --filter=@suaveplan/cache-redis` plus `test:coverage` before marking changes complete.

View File

@@ -0,0 +1,133 @@
---
name: sp-cache-storage
description: AbstractCache backend bridging any AbstractStorage into a typed cache via a 32-byte TTL header and pluggable Codec<T>.
---
# Cache-Storage Backend for Suaveplan
`@suaveplan/cache-storage` turns any byte-oriented `@suaveplan/storage` adapter into a typed `AbstractCache<T>` with TTL, namespacing, lifecycle events, and structured errors. It is the universal cache backend for browser, file, S3, and memory stores, and auto-registers itself as the `"storage"` provider on `CacheFactory`.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `StorageCache<T>` | Extends `AbstractCache<T>`; reads/writes entries through `AbstractStorage` with a 32-byte header prepended for TTL metadata. |
| `JsonCodec<T>` | Default `Codec<T>`; encodes via `JSON.stringify` → `TextEncoder`, decodes via `TextDecoder` (fatal) → `JSON.parse`. |
| `IdentityCodec` | Pass-through `Codec<Uint8Array>`; copies slices on decode to prevent buffer mutation. |
| `encodeHeader` / `decodeHeader` | Big-endian 32-byte fixed-width header codec carrying `createdAt`, `expiresAt`, `flags`, and `hitCount`. |
| `StorageCacheError` | Structured error extending `SuaveplanError<CacheStorageCode>` with optional `key` discriminant. |
| `registerStorageProvider` | Idempotently registers `StorageCache` as the `"storage"` provider on `CacheFactory`; runs on side-effect import. |
## File Structure
```
persistence/cache/cache-storage/
src/storage-cache/ # StorageCache class, options interface, and main tests
src/codec/ # Codec<T> contract, JsonCodec, IdentityCodec
src/header/ # 32-byte TTL header encoder/decoder and flags
src/errors/ # CACHE_STORAGE_CODES registry and StorageCacheError
src/registry-augment/# CacheProviderRegistry augmentation + auto-registration
```
Sibling packages:
- `@suaveplan/cache` — provides `AbstractCache<T>`, `CacheFactory`, `CacheProviderRegistry`, and `CacheEvents` consumed by this package.
- `@suaveplan/cache-redis` — server-tier `AbstractCache` backend using Redis native TTL; reuses `JsonCodec` / `IdentityCodec` from this package.
- `@suaveplan/storage` — defines the `AbstractStorage` contract that `StorageCache` requires as its backend.
## Patterns
### Quick Start
```typescript
import { StorageCache } from "@suaveplan/cache-storage";
import { MemoryStorage } from "@suaveplan/storage/memory";
const cache = new StorageCache<{ id: string; email: string }>({
storage: new MemoryStorage(),
defaultTtl: 5 * 60_000,
});
await cache.set("user:1", { id: "1", email: "alice@example.com" });
const user = await cache.get("user:1");
```
### Factory Registration
```typescript
import "@suaveplan/cache-storage"; // side-effect: registers the "storage" provider
import { createCache } from "@suaveplan/cache";
import { MemoryStorage } from "@suaveplan/storage/memory";
const cache = createCache("storage", { storage: new MemoryStorage() });
```
### Binary Payloads with IdentityCodec
```typescript
import { StorageCache, IdentityCodec } from "@suaveplan/cache-storage";
import { MemoryStorage } from "@suaveplan/storage/memory";
const cache = new StorageCache<Uint8Array>({
storage: new MemoryStorage(),
codec: new IdentityCodec(),
});
const value = new Uint8Array([1, 2, 3, 4, 5]);
await cache.set("k", value);
const out = await cache.get("k");
// out is a fresh Uint8Array copy, not the original buffer
```
### Namespace Isolation and Scoped Clear
```typescript
import { StorageCache } from "@suaveplan/cache-storage";
import { MemoryStorage } from "@suaveplan/storage/memory";
const storage = new MemoryStorage();
const cache = new StorageCache<string>({
storage,
namespace: "users",
});
await cache.set("alice", "v");
// Underlying storage key is "users:alice"
await cache.clear(); // deletes only keys starting with "users:"
```
### Structured Error Handling
```typescript
import {
CACHE_STORAGE_CODES,
StorageCacheError,
} from "@suaveplan/cache-storage/errors";
try {
await cache.get("k");
} catch (err) {
if (
err instanceof StorageCacheError &&
err.code === CACHE_STORAGE_CODES.ENTRY_CORRUPT
) {
await cache.delete("k"); // safe recovery path
}
}
```
## Constraints
- Keys are hard-capped at 1024 characters; `set` throws `StorageCacheError(CACHE_STORAGE_CODES.KEY_TOO_LONG)` otherwise.
- Encoded values are hard-capped at 10 MiB; `set` throws `StorageCacheError(CACHE_STORAGE_CODES.VALUE_TOO_LARGE)` otherwise.
- TTL is lazy — entries are only evicted on `get` or `has`; there is no background sweeper.
- `clear()` without a namespace delegates to `storage.clear()`; with a namespace it paginates through `storage.list({ prefix })`, which is O(n) in matching keys.
- `hitCount` in the header is reserved but never rewritten on read — updating it would turn every get into a read-modify-write.
- The `size` stat is tracked client-side via insert/delete deltas and is not authoritative when the underlying storage is shared with another writer.
## Checklist
1. Confirm the chosen `AbstractStorage` backend implements `get`, `set`, `delete`, `exists`, `list`, and `clear`.
2. Choose `JsonCodec` (default) for JSON-safe values or `IdentityCodec` for raw `Uint8Array` payloads.
3. Set `namespace` when the same storage backend is shared across multiple cache instances.
4. Wire an `EventBusLike` to `events` if you need to observe `hit`, `miss`, `set`, `delete`, `clear`, or `expire` lifecycle events.
5. Handle `StorageCacheError` by branching on `error.code` rather than parsing message strings.

View File

@@ -0,0 +1,162 @@
---
name: sp-cache
description: Type-safe caching abstraction with LRU/LFU/FIFO/TTL MemoryCache, NamespacedCache, CacheFactory registry, lifecycle events, and structured CACHE.* error codes.
---
# Cache for Suaveplan
`@suaveplan/cache` is the central caching abstraction for the Suaveplan stack. It defines the `AbstractCache<T>` contract, provides an in-memory `MemoryCache` with pluggable eviction policies, a `NamespacedCache` wrapper for key isolation, and a `CacheFactory` with module-augmentation-based provider registration. Lifecycle events, an injectable clock, and Zod-schematised options make it suitable for both browser and server environments.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `AbstractCache<T>` | Base class defining the six primitives (`get`, `set`, `delete`, `clear`, `has`, `getStats`) and composite helpers (`wrap`, `getMultiple`, `setMultiple`). |
| `MemoryCache<T>` | In-memory implementation supporting `lru`, `lfu`, `fifo`, and `ttl` eviction policies with O(1) amortised operations, lazy expiry, and optional background cleanup timer. |
| `NamespacedCache<T>` | Decorator that prefixes every key with `namespace:` (or custom separator) before delegating to an inner `AbstractCache`, enabling shared-storage isolation. |
| `CacheFactory` | Module-level provider registry with `create`, `register`, `unregister`, `has`, and `providers`. Built-in `"memory"` provider is auto-registered via side-effect import. |
| `CACHE_CODES` | `defineCodes("CACHE", [...])` registry: `KEY_INVALID`, `CAPACITY_EXCEEDED`, `PROVIDER_NOT_FOUND`, `SET_FAILED`, `GET_FAILED`, `SERIALIZATION_FAILED`. |
| `CacheError` | Structured error extending `SuaveplanError<CacheCode>`. |
| `CacheProviderRegistry` | Empty interface intended for module augmentation by downstream packages (`cache-redis`, `cache-storage`) to add typed provider names. |
| Zod schemas | `CacheOptionsSchema`, `CacheSetOptionsSchema`, `CacheWrapOptionsSchema`, `CacheFactoryConfigSchema`, etc. — validate configuration at runtime. |
## File Structure
```
persistence/cache/cache/
src/abstract/abstract-cache/ # AbstractCache<T> base class
src/abstract/namespaced-cache/ # NamespacedCache<T> key-prefix wrapper
src/memory/memory-cache/ # MemoryCache<T> with LRU/LFU/FIFO/TTL
src/factory/cache/ # CacheFactory + createCache / registerCacheProvider helpers
src/factory/registry/ # Side-effect registration of built-in "memory" provider
src/errors/ # CACHE_CODES + CacheError
src/types/ # TypeScript types + Zod schemas
```
Sibling packages:
- `@suaveplan/cache-redis` — `AbstractCache<T>` backend over a minimal `RedisLike` interface with native `SET ... EX` TTL.
- `@suaveplan/cache-storage` — `AbstractCache<T>` backend bridging any `@suaveplan/storage` `AbstractStorage` via a 32-byte TTL header and pluggable `Codec<T>`.
- `@suaveplan/storage` — `AbstractStorage` contract consumed by `cache-storage`.
## Patterns
### Quick Start
```typescript
import { MemoryCache } from "@suaveplan/cache";
const cache = new MemoryCache<string>({ maxSize: 500, defaultTTL: 60_000 });
await cache.set("user:1", "Alice");
const alice = await cache.get("user:1"); // "Alice"
// get-or-compute
const data = await cache.wrap("expensive:key", async () => {
return await fetchSomeData();
}, { ttl: 30_000 });
```
### Factory + Provider Registry
```typescript
import { createCache, createCacheWithConfig, registerCacheProvider } from "@suaveplan/cache";
// Built-in memory provider (auto-registered)
const mem = createCache<string>("memory", { maxSize: 100 });
// Config-object style
const cache = createCacheWithConfig({ type: "memory", options: { maxSize: 50 } });
// Register a custom provider
registerCacheProvider("custom", MyCustomCache as any);
```
### Namespaced Isolation
```typescript
import { MemoryCache, NamespacedCache } from "@suaveplan/cache";
const shared = new MemoryCache<string>();
const users = new NamespacedCache(shared, "user");
const sessions = new NamespacedCache(shared, "session");
await users.set("alice", "user-data");
await sessions.set("alice", "sess-data");
// Keys are isolated via prefix
expect(await users.get("alice")).toBe("user-data");
expect(await sessions.get("alice")).toBe("sess-data");
```
### Multi-Get / Multi-Set
```typescript
await cache.setMultiple([
{ key: "a", value: 1 },
{ key: "b", value: 2 },
]);
const result = await cache.getMultiple(["a", "b", "missing"]);
// result.found => Map { "a" => 1, "b" => 2 }
// result.missing => ["missing"]
```
### Injectable Clock + Events
```typescript
import type { EventBusLike } from "@suaveplan/types";
let now = 0;
const clock = () => now;
const bus: EventBusLike = {
on: () => () => {},
emit: (type, payload) => console.log(type, payload),
};
const cache = new MemoryCache<string>({ defaultTTL: 100 }, clock, bus);
await cache.set("k", "v");
now += 200;
expect(await cache.get("k")).toBeUndefined(); // expired, emits "expire" + "miss"
```
### Wrap with forceRefresh and shouldCache
```typescript
const value = await cache.wrap("key", async () => compute(), {
forceRefresh: true, // always call compute
shouldCache: (v) => v !== null, // skip caching nulls
signal: AbortSignal.timeout(5000),
});
```
### Error Handling
```typescript
import { CACHE_CODES, CacheError } from "@suaveplan/cache";
// Empty key throws KEY_INVALID
try {
await cache.get("");
} catch (err) {
if (err instanceof CacheError && err.code === CACHE_CODES.KEY_INVALID) {
// handle
}
}
```
## Constraints
- `MemoryCache` key validation rejects empty strings and keys longer than `MAX_KEY_LENGTH` (1024 chars), throwing `CACHE_CODES.KEY_INVALID`.
- `MemoryCache.destroy()` must be called when `checkExpirationInterval` is used to avoid leaking the background `setInterval` timer.
- TTL is evaluated lazily on `get`/`has`; no proactive eviction occurs unless `checkExpirationInterval > 0` is configured.
- `CacheFactory.create` throws `CACHE_CODES.PROVIDER_NOT_FOUND` if the requested type is not in the registry.
- `NamespacedCache.clear()` delegates directly to the inner cache — it does **not** perform selective namespaced deletion.
- `MemoryCache.getStats().size` reflects the count of entries in the active policy's storage map, including expired entries that have not yet been accessed.
- `wrap` stores the computed value only if `shouldCache` is undefined or returns `true` for the result.
## Checklist
1. Verify `package.json` `exports` includes `.`, `./errors/cache-error`, `./types/common`, `./types/schemas`, `./abstract/abstract-cache`, `./abstract/namespaced-cache`, `./memory/memory-cache`, and `./factory/cache`.
2. Confirm every new error code is added to `CACHE_CODES` via `defineCodes("CACHE", [...])` and exercised in a test.
3. When adding a new eviction policy to `MemoryCache`, update the switch statements in `get`, `set`, `delete`, `has`, `_peekExpiry`, and `_cleanupExpired`.
4. If augmenting `CacheProviderRegistry`, add the side-effect registration module and list it in `package.json` `sideEffects` so bundlers preserve it.
5. Run `bunx turbo lint typecheck test build --filter=@suaveplan/cache` plus `test:coverage` before marking changes complete.

View File

@@ -0,0 +1,174 @@
---
id: sp-caddy-imgproxy
name: Caddy Imgproxy
library: "@suaveplan/caddy-imgproxy"
description: >
Universal TypeScript port of the Caddy imgproxy URL rewriter. Parses compact
`/u<op><gravity><dims><modifiers>/<path>.<format>` grammar, resolves
dimensions, translates gravity and operations into imgproxy's URL DSL, and
signs URLs with HMAC-SHA256. Exposes `ImgproxyHandler` with Zod-validated
config and OTel telemetry.
---
# `@suaveplan/caddy-imgproxy`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **Parse** | Compact grammar parser | `parseImgproxyUrl(path)` → `ParsedImgproxyUrl` |
| **Dimensions** | Aspect/pixel resolution | `calculateDimensions(parsed, defaultWidth)` |
| **Gravity** | Grammar → imgproxy compass | `gravityToImgproxy(code)`, `GRAVITY_MAP` |
| **Operation** | Operation + dims → processing string | `operationToImgproxy(op, w, h, gravity)` |
| **URL Builder** | Composes final imgproxy path | `buildImgproxyUrl({ parsed, domain, config, hash })` |
| **Domain** | Host header → S3 sub-path | `extractDomain(host)` |
| **Signing** | HMAC-SHA256 URL signature | `signImgproxyPath({ path, key, salt })` |
| **Handler** | High-level orchestrator | `ImgproxyHandler.rewrite({ path, host })` |
| **Config** | Zod schema + parser | `parseConfig(raw)`, `ImgproxyConfigSchema` |
| **Errors** | Typed errors | `CaddyImgproxyError`, `CADDY_IMGPROXY_CODES` |
| **Telemetry** | OTel counters/histograms | `rewriteTotalCounter`, `rewriteDurationHistogram` |
## File Structure
```
src/
├── index.ts # Public barrel
├── parse/
│ └── parse-url.ts # parseImgproxyUrl() — regex grammar parser
├── dimensions/
│ └── calculate-dimensions.ts # Aspect ratio → pixel resolution
├── gravity/
│ └── gravity.ts # Gravity code translation
├── operation/
│ └── operation.ts # Operation → imgproxy processing DSL
├── url-builder/
│ └── url-builder.ts # buildImgproxyUrl() — final path composer
├── domain/
│ └── extract-domain.ts # Host → S3 sub-path segment
├── signing/
│ └── sign-path.ts # HMAC-SHA256 signature
├── handler/
│ └── handler.ts # ImgproxyHandler orchestrator
├── config/
│ └── config.ts # Zod schema and parseConfig()
├── types/
│ └── types.ts # ParsedImgproxyUrl, ImgproxyConfig, etc.
├── error-codes/
│ └── error-codes.ts # CADDY_IMGPROXY_CODES, CaddyImgproxyError
└── telemetry/
└── telemetry.ts # OTel instruments
```
## Patterns
### Basic — Rewrite a compact imgproxy URL
```ts
import { ImgproxyHandler, parseConfig } from "@suaveplan/caddy-imgproxy";
const handler = new ImgproxyHandler({
config: parseConfig({
upstream: "imgproxy:8080",
s3Bucket: "media",
defaultWidth: 1200,
key: "aa11bbcc",
salt: "deadbeef",
}),
});
const result = handler.rewrite({
path: "/uvt9:16h1080/photos/hero.webp",
host: "images.example.com",
});
if (result.rewritten) {
console.log(result.targetPath); // signed imgproxy path
console.log(result.operation); // "cover"
} else if (result.reason === "passthrough") {
// forward to imgproxy as-is
}
```
### Basic — Parse a compact URL manually
```ts
import { parseImgproxyUrl } from "@suaveplan/caddy-imgproxy";
const parsed = parseImgproxyUrl("/uv16:9/avatar.png");
console.log(parsed.operation); // "cover"
console.log(parsed.aspectW, parsed.aspectH); // 16, 9
console.log(parsed.signed); // false (u = unsigned)
```
### Basic — Sign an imgproxy path
```ts
import { signImgproxyPath } from "@suaveplan/caddy-imgproxy";
const signed = signImgproxyPath({
path: "/rs:fill:800:450/plain/s3://bucket/image.png",
key: "aa11bbcc",
salt: "deadbeef",
});
// signed → "/<signature>/rs:fill:800:450/plain/s3://bucket/image.png"
```
### Advanced — Inject custom clock and logger
```ts
import { ImgproxyHandler, parseConfig } from "@suaveplan/caddy-imgproxy";
const handler = new ImgproxyHandler({
config: parseConfig({}),
clock: () => performance.now(),
logger: { debug, info, warn, error },
hash: customHmacFn, // optional injectable signer
});
```
### Advanced — Passthrough detection
```ts
const result = handler.rewrite({ path: "/unsafe/foo.png", host: "x.com" });
// result.rewritten === false, result.reason === "passthrough"
```
## Constraints
- **Universal tier**: no DOM or Node-only APIs. Runs in Bun, Node, and browsers.
- **Grammar**: URLs must match `/[su][vcfsr]?<gravity>?<dims><modifiers>/<path>.<format>`.
- **Dimensions required**: Operations (`v|c|f|s|r`) require dimensions; missing dims throw `MISSING_DIMENSIONS`.
- **Key/salt validation**: Even-length lower-hex strings only; malformed values throw `INVALID_KEY` / `INVALID_SALT`.
- **Passthrough paths**: `/unsafe/...` and `/health*` bypass rewriting without error.
- **Empty host**: Returns `INVALID_HOST` with logger warning.
- **Error codes**: `INVALID_URL`, `MISSING_DIMENSIONS`, `INVALID_OPTIONS`, `INVALID_KEY`, `INVALID_SALT`, `INVALID_HOST`.
- **OTel**: `caddy_imgproxy_rewrite_total`, `caddy_imgproxy_rewrite_duration_ms`, `caddy_imgproxy_parse_total`, `caddy_imgproxy_sign_total`.
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/caddy-imgproxy` |
| Parse | `@suaveplan/caddy-imgproxy/parse` |
| Dimensions | `@suaveplan/caddy-imgproxy/dimensions` |
| Gravity | `@suaveplan/caddy-imgproxy/gravity` |
| Operation | `@suaveplan/caddy-imgproxy/operation` |
| Signing | `@suaveplan/caddy-imgproxy/signing` |
| URL Builder | `@suaveplan/caddy-imgproxy/url-builder` |
| Domain | `@suaveplan/caddy-imgproxy/domain` |
| Handler | `@suaveplan/caddy-imgproxy/handler` |
| Config | `@suaveplan/caddy-imgproxy/config` |
| Error Codes | `@suaveplan/caddy-imgproxy/error-codes` |
| Telemetry | `@suaveplan/caddy-imgproxy/telemetry` |
| Types | `@suaveplan/caddy-imgproxy/types` |
## Checklist
- [ ] Need to rewrite compact imgproxy URLs → `ImgproxyHandler`
- [ ] Need to parse the compact grammar → `parseImgproxyUrl(path)`
- [ ] Need HMAC-SHA256 signing → `signImgproxyPath({ path, key, salt })`
- [ ] Need dimension resolution → `calculateDimensions(parsed, defaultWidth)`
- [ ] Need gravity translation → `gravityToImgproxy(code)`
- [ ] Need operation DSL → `operationToImgproxy(op, w, h, gravity)`
- [ ] Need Zod config validation → `parseConfig(raw)`
- [ ] Need typed errors → `CaddyImgproxyError`, `CADDY_IMGPROXY_CODES`

View File

@@ -0,0 +1,139 @@
---
name: sp-campaigns
description: Campaign orchestration engine with state machine, batch planner, workflow pipeline, and pluggable store with OTel telemetry.
---
# Campaigns for Suaveplan
Campaign orchestration library that models the full lifecycle of a marketing or transactional campaign — from draft creation through scheduling, sending, pausing, and completion. Provides a strict state machine, throttle-aware batch planner, composable 6-step send workflow, and a store-agnostic persistence contract.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `CampaignEngine` | Top-level CRUD + lifecycle orchestrator. Validates input with Zod, delegates state transitions to `CampaignStateMachine`, persists through `CampaignStore`, and emits OTel counters. |
| `CampaignStateMachine` | Owns the nine legal transitions between seven `CampaignStatus` values. Exposes `transition`, `canTransition`, and `getAvailableEvents` for UI affordances. |
| `BatchPlanner` | Splits contact lists into sized batches with optional throttle-aware `scheduledAt` timestamps. Computes total send-window duration. Clock is injectable for determinism. |
| `CampaignWorkflow` | Builds a canonical 6-step send pipeline (resolve-audience → apply-suppression → plan-batches → send-batches → update-stats → complete) and runs steps sequentially with error wrapping. |
| `CampaignStore` / `InMemoryCampaignStore` | Persistence contract with create/get/update/delete/list/updateStats/updateStatus. Reference implementation is Map-backed and returns defensive clones. |
| `CAMPAIGNS_CODES` / `CampaignsError` | Stable error registry via `@suaveplan/error`'s `defineCodes("CAMPAIGNS", [...])`. Every failure path surfaces a typed `CampaignsError` with a discriminating `code`. |
| `telemetry` | OTel counters (`campaigns_created_total`, `campaigns_transitions_total`, `campaigns_sends_total`, `campaigns_failures_total`, `campaigns_batches_planned_total`) and histograms (`campaigns_batch_plan_size`, `campaigns_send_duration_ms`). |
## File Structure
```
communication/emails/campaigns/
src/engine/ # CampaignEngine — CRUD, lifecycle, scheduling, duplication
src/state-machine/ # CampaignStateMachine — legal transitions and predicates
src/batch-planner/ # BatchPlanner — contact splitting + throttle-aware timing
src/workflow/ # CampaignWorkflow — 6-step pipeline builder + sequential runner
src/store/ # CampaignStore interface + InMemoryCampaignStore reference
src/types/ # Campaign, CampaignStatus, CampaignStats, ThrottleConfig, etc.
src/error-codes/ # CAMPAIGNS_CODES and CampaignsError
src/telemetry/ # OTel meter, counters, histograms, and record helpers
```
Sibling packages:
- `@suaveplan/emailer` — dispatches actual email messages via SMTP/Gmail/Dummy transports; campaigns orchestrate *when* to send, emailer handles *how*
- `@suaveplan/email-templates` — React-free template engine that produces email-safe HTML; campaigns reference templates by `templateId`
- `@suaveplan/delivery-tracking` — channel-agnostic delivery event tracking; consumes campaign send results and aggregates lifecycle stats
## Patterns
### Quick Start
```typescript
import { CampaignEngine, InMemoryCampaignStore } from "@suaveplan/campaigns";
const engine = new CampaignEngine(new InMemoryCampaignStore());
const campaign = await engine.create({
tenantId: "tenant-1",
name: "Spring Sale Announcement",
type: "broadcast",
channel: "email",
audienceConfig: {
groupIds: ["all-subscribers"],
excludeGroupIds: ["unsubscribed"],
},
templateId: "spring-sale-v1",
});
await engine.schedule(campaign.id, {
type: "scheduled",
sendAt: new Date("2026-05-01T09:00:00Z"),
timezone: "America/New_York",
});
await engine.send(campaign.id);
```
### Throttle-Aware Batch Planning
```typescript
import { BatchPlanner, type ThrottleConfig } from "@suaveplan/campaigns";
const planner = new BatchPlanner();
const contactIds = Array.from({ length: 500 }, (_, i) => `contact-${i}`);
const throttle: ThrottleConfig = { messagesPerSecond: 50 };
const batches = planner.planBatches(contactIds, { batchSize: 200 }, throttle);
const etaMs = planner.calculateTotalDuration(contactIds.length, { batchSize: 200 }, throttle);
// batches[1].scheduledAt is derived from max(baseDelay, throttle-derived delay)
```
### Custom Workflow Steps
```typescript
import { CampaignWorkflow, type WorkflowStep, type WorkflowContext } from "@suaveplan/campaigns";
const wf = new CampaignWorkflow();
const defaultSteps = wf.buildSendPipeline(campaign);
const customStep: WorkflowStep = {
id: "enrich-contacts",
name: "Enrich Contacts",
description: "Fetch extra metadata per contact",
execute: async (ctx: WorkflowContext) => {
const enriched = await fetchEnrichment(ctx.contactIds ?? []);
return { ...ctx, results: { ...ctx.results, enriched } };
},
};
const finalCtx = await wf.run([...defaultSteps.slice(0, 3), customStep, ...defaultSteps.slice(3)], {
campaign,
});
```
### State-Machine Predicates for UI
```typescript
import { CampaignStateMachine } from "@suaveplan/campaigns";
const sm = new CampaignStateMachine();
// Drive action-button visibility
const available = sm.getAvailableEvents("sending");
// => ["pause", "complete", "fail"]
// Guard before calling engine methods
if (sm.canTransition("draft", "send")) {
await engine.send(campaignId);
}
```
## Constraints
- `CampaignEngine` requires an injectable `CampaignStore`; it does not ship with a default production store — callers must provide one or use `InMemoryCampaignStore` for tests.
- All boundary inputs (`CampaignEngineOptions`, `CreateCampaignInput`, `BatchPlannerOptions`, batch config, throttle config) are validated with Zod `.strict()` — extra keys throw `CAMPAIGNS_CODES.INVALID_OPTIONS` or `INVALID_INPUT`.
- The state machine has exactly nine legal transitions; terminal states (`completed`, `cancelled`, `failed`) accept no events. Invalid transitions throw `CAMPAIGNS_CODES.INVALID_TRANSITION`.
- `BatchPlanner` uses `DEFAULT_BATCH_CONFIG = { batchSize: 100, delayBetweenBatches: 0 }` when no config is supplied. Throttle-derived delay wins over `baseDelay` only when it is larger (`Math.max`).
- `InMemoryCampaignStore` returns defensive clones on every read and write so callers cannot mutate internal records by reference.
- Every failure path in the package throws `CampaignsError` with a code from `CAMPAIGNS_CODES` — never raw `Error` or string literals. Assert against `CAMPAIGNS_CODES.REASON` in tests, not hand-typed strings.
## Checklist
1. Verify `package.json` `exports` map includes all subpaths (`engine`, `state-machine`, `batch-planner`, `workflow`, `store`, `types`, `error-codes`, `telemetry`) when adding new public modules.
2. When adding a new `CampaignEvent` or `CampaignStatus`, update `TRANSITIONS` in `state-machine.ts`, add corresponding `engine.ts` lifecycle method, and emit telemetry via `recordTransition`.
3. Run `bunx turbo lint test typecheck build --filter=@suaveplan/campaigns` and `test:coverage` after any change — coverage must stay at 100%.
4. If introducing a new error reason, add it to `defineCodes("CAMPAIGNS", [...])` in `error-codes.ts` and throw `new CampaignsError(CODES.REASON, ...)` — never type the raw string literal.

View File

@@ -0,0 +1,164 @@
---
name: sp-cbor
description: RFC 8949 CBOR codec with streaming, COSE, CWT, CDDL, and deterministic encoding — zero runtime deps.
---
# CBOR Codec for Suaveplan
`@suaveplan/cbor` is the foundational RFC 8949 CBOR encoder/decoder, plus RFC 8742 sequences, pull-style streaming, RFC 9052 COSE structures (Sign1, Mac0, Encrypt, etc.), RFC 8392 CWT verification, and RFC 8610 CDDL parsing/validation. It runs unmodified in Bun, Node, Deno, and the browser with zero platform dependencies.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `encode()` | Serialises JS values to CBOR bytes with preferred serialization (shortest integers, shortest-exact floats). |
| `decode()` / `decodeOne()` | Materialises CBOR bytes into JS values; strict mode enforces RFC 8949 §5.1 well-formedness including trailing-byte rejection. |
| `Tagged<T>` / `isTagged()` | Value object for RFC 8949 §3.4 tagged items; encoder emits tag-then-value, decoder surfaces unknown tags as `Tagged`. |
| `TagRegistry` | Mutable handler registry consumed by the decoder via `snapshot()`. Double-registration throws to prevent silent shadowing. |
| `defaultTagHandlers()` | Returns a `TagRegistry` pre-populated with IANA built-ins (tag 0→Date, 1→Date, 2/3→bigint, 24→re-decode, 32→URL, 35→RegExp, 55799→self-describe). |
| `PullDecoder` | Pull-style streaming decoder over `AsyncIterable<Uint8Array>` or `ReadableStream<Uint8Array>`; yields whole items with bounded memory. |
| `encodeSequence()` / `decodeSequence()` / `decodeSequenceStream()` | RFC 8742 CBOR Sequences: concatenated items with no framing. |
| `encodeDeterministic()` | Convenience wrapper that pins `deterministic: true` for byte-stable output (sorted map keys, no indefinite-length, no half-float, rejects `undefined`). |
| `toDiagnostic()` / `bytesToDiagnostic()` | RFC 8949 §8 diagnostic notation emitter for debug/logging. |
| `isWellFormed()` | Predicate that returns `true` iff bytes are a single well-formed CBOR item; delegates to strict-mode `decode()`. |
| `encodeCoseSign1()` / `decodeCoseSign1()` / `verifyCoseSign1()` | RFC 9052 §4.2 single-signer COSE message encode/decode/verify (ES256, ES384, ES512, EdDSA). |
| `buildSign1Structure()` / `buildMacStructure()` / `buildEncStructure()` | Canonical Sig_structure builders consumed by COSE sign/MAC/AEAD computations. |
| `encodeCwt()` / `decodeCwt()` / `verifyCwtSign1()` / `verifyCwtMac0()` | RFC 8392 CBOR Web Token claim encode/decode plus time-window, audience, and issuer validation. |
| `tokenize()` / `parseCddl()` / `validateCbor()` | RFC 8610 CDDL lexer, parser, and validator for schema-driven CBOR validation. |
| `CBOR_CODES` / `CborError` / `cborError()` | Stable error-code registry via `@suaveplan/error`'s `defineCodes`; every thrown error carries a typed `CBOR.*` code. |
## File Structure
```
packages/foundation/flow/cbor/
src/encoder/ # encode() + half-float + EncoderOptions
src/decoder/ # decode() / decodeOne() + half-float + well-formed
src/tags/ # Tagged, TagRegistry, defaultTagHandlers
src/sequences/ # RFC 8742 sequence encode/decode/stream
src/streaming/ # PullDecoder + ChunkDecoder
src/deterministic/ # encodeDeterministic shortcut
src/diagnostic/ # toDiagnostic / bytesToDiagnostic
src/cose/ # COSE_Sign1, COSE_Mac0, COSE_Encrypt, COSE_Key, JWK, headers, sig-structure
src/cwt/ # CWT claims encode/decode + verify
src/cddl/ # CDDL lexer, parser, AST, validator
src/errors/ # CBOR_CODES, CborError, cborError
```
Sibling packages:
- `@suaveplan/retry` — completed
- `@suaveplan/fsm` — completed
- `@suaveplan/rate-limiter` — completed
- `@suaveplan/streams` — completed
- `@suaveplan/rules` — completed
- `@suaveplan/idempotency` — completed
- `@suaveplan/queue`
- `@suaveplan/workflow`
- `@suaveplan/crdt`
- `@suaveplan/undo`
- `@suaveplan/job-scheduler`
## Patterns
### Quick Start
```typescript
import { encode, decode } from "@suaveplan/cbor";
const bytes = encode({ a: 1, b: [true, "two"] });
const value = decode(bytes);
```
### Deterministic Encoding for Signatures
```typescript
import { encodeDeterministic } from "@suaveplan/cbor/deterministic";
const a = encodeDeterministic({ z: 1, a: 2 });
const b = encodeDeterministic({ a: 2, z: 1 });
// a and b are byte-identical because key order is canonical, not insertion-order.
```
### Decode with Built-in Tag Handlers
```typescript
import { decode } from "@suaveplan/cbor/decoder";
import { defaultTagHandlers } from "@suaveplan/cbor/tags";
const tags = defaultTagHandlers().snapshot();
const value = decode(bytes, { mode: "strict" }, tags);
// Tag 0 → Date, tag 1 → Date, tag 32 → URL, etc.
```
### Streaming Pull Decoder
```typescript
import { PullDecoder } from "@suaveplan/cbor/streaming";
const decoder = new PullDecoder(stream);
for await (const item of decoder) {
handle(item);
}
```
### COSE Sign1 Encode / Decode / Verify
```typescript
import { decodeCoseSign1, encodeCoseSign1, verifyCoseSign1 } from "@suaveplan/cbor/cose";
import { buildSign1Structure } from "@suaveplan/cbor/cose";
import * as ed25519 from "@suaveplan/crypto-curve/ed25519";
const draft = decodeCoseSign1(
encodeCoseSign1({ protectedHeader: { alg: "EdDSA" }, payload, signature: new Uint8Array(64) }),
);
const tbs = buildSign1Structure(draft.protectedRaw, new Uint8Array(0), payload);
const sig = ed25519.sign(tbs, privateKey);
const wire = encodeCoseSign1({ protectedHeader: { alg: "EdDSA" }, payload, signature: sig });
const msg = decodeCoseSign1(wire);
const ok = verifyCoseSign1(msg, publicKey);
```
### CWT Claims and Verification
```typescript
import { encodeCwt, decodeCwt } from "@suaveplan/cbor/cwt";
import { verifyCwtMac0 } from "@suaveplan/cbor/cwt";
import { createCoseMac0 } from "@suaveplan/cbor/cose";
const claims = { iss: "me", aud: "you", exp: new Date(Date.now() + 60_000) };
const cwt = createCoseMac0({
protectedHeader: { alg: "HS256" },
payload: encodeCwt(claims),
key,
});
const r = verifyCwtMac0(cwt, key);
// r.valid === true, r.claims.iss === "me"
```
### Diagnostic Notation
```typescript
import { bytesToDiagnostic } from "@suaveplan/cbor/diagnostic";
import { encode } from "@suaveplan/cbor/encoder";
import { Tagged } from "@suaveplan/cbor/tags";
const wire = encode([new Tagged(0, "2026-05-10T12:00:00Z"), { id: 1 }]);
console.log(bytesToDiagnostic(wire));
// '[0("2026-05-10T12:00:00Z"), {"id": 1}]'
```
## Constraints
- `decode()` defaults to strict mode and rejects trailing bytes after the top-level item; pass `allowTrailingBytes: true` to permit them.
- `TagRegistry` double-registration throws `CBOR.UNEXPECTED_TYPE` — callers must `unregister` first to replace a handler.
- Deterministic mode disables half-float and rejects `undefined` with `CBOR.NON_DETERMINISTIC`.
- Unknown tags NEVER cause decode failure regardless of mode; they surface as `Tagged` instances.
- `decode()` returns `Map<DecodedValue, DecodedValue>` for CBOR maps, not plain objects; plain objects are only produced by the encoder's input side.
- Every error code is declared via `defineCodes("CBOR", [...])` in `src/errors/error-codes.ts`; hand-typed string literals are forbidden in source and tests.
## Checklist
1. Verify `package.json` `exports` map includes every subpath (`encoder`, `decoder`, `streaming`, `sequences`, `tags`, `deterministic`, `diagnostic`, `cose`, `cwt`, `cddl`, `errors`).
2. Confirm every thrown error uses `CBOR_CODES.*` via `cborError()` — grep for raw `"CBOR.` strings in `src/` and reject any hits.
3. When adding a new tag handler, register it in `src/tags/builtin.ts` and add a test in `src/tags/builtin.test.ts`.
4. When modifying COSE structures, run the full pre-publish gate: `bunx turbo lint typecheck test build --filter=@suaveplan/cbor` plus `test:coverage`.

View File

@@ -0,0 +1,56 @@
---
name: sp-cli
description: Zero-dependency CLI toolkit with argument parser, command router, ANSI styling, output formatting, spinner, progress bar, and interactive prompts.
---
| Class / Function | Responsibility |
| --- | --- |
| `cli()` / `App` | Application entry point and root command with `--version` / `--help` interception and `AbortSignal` support |
| `Command` | Subcommand builder with fluent API for options, arguments, aliases, examples, and nested commands |
| `parseArgs` | Argv parser supporting options (short/long flags), positionals, choices, defaults, and type coercion |
| `style` / `createStyler` | Chainable ANSI color/formatting proxy with automatic color-support detection |
| `heading`, `list`, `tree`, `keyValue`, `columns`, `json`, `divider`, `indent` | Text formatters for terminal output |
| `table` | ASCII/Unicode bordered table renderer with alignment and schema validation |
| `createSpinner` | TTY-aware animated spinner with `start` / `succeed` / `fail` lifecycle |
| `createProgress` | ASCII progress bar with in-place TTY updates |
| `text`, `password`, `confirm`, `select`, `multiselect` | Interactive terminal prompts with Zod-validated options |
| `CLI_CODES` / `CliError` | Stable error code registry and typed error class |
**File Structure:** `packages/devtools/tooling/cli/src/{app,command,errors,output/{format,table},parser,progress,prompt,spinner,style}/`
**Sibling packages:**
- `@suaveplan/electrobun` — desktop-app generator that consumes `@suaveplan/cli` for its command surface
- `@suaveplan/readline` — readline primitives used by interactive prompts
- `@suaveplan/error` — `defineCodes` + `SuaveplanError` base
- `@suaveplan/types` — `Clock` + `IdGenerator` injectables
**Patterns**
Quick Start:
```ts
import { cli } from "@suaveplan/cli";
const app = cli("myapp", "1.0.0");
app.description("My enterprise CLI tool");
app.command("build").description("Build the project").option("--output <dir>, -o", "output directory", "./dist").option("--watch, -w", "watch for changes").action(async (ctx) => {
const { output, watch } = ctx.options;
console.log(`Building to ${output}${watch ? " (watch mode)" : ""}...`);
});
await app.run();
```
- **Subpath imports:** Deep imports via `package.json` exports (`./app/app`, `./command/command`, `./parser/parser`, `./style/style`, `./output/format/format`, `./output/table/table`, `./spinner/spinner`, `./progress/progress`, `./prompt/prompt`, `./errors/errors`).
- **Nested subcommands:** Commands can be nested arbitrarily deep via `app.command("db").command("migrate")`.
- **OTel tracing:** Every `App.run()` and `Command.execute()` call is wrapped in an OpenTelemetry span; no-op tracer by default.
- **Error codes:** `CLI_CODES` registry with codes for parse failures, missing arguments/options, unknown subcommands, and run-time aborts.
**Constraints**
- Zero runtime dependencies beyond workspace packages (`@suaveplan/readline`, `@suaveplan/error`, `@suaveplan/types`) and `@opentelemetry/api`.
- Every public surface validates inputs with Zod schemas (`AppConfigSchema`, `CommandInfoSchema`, `ParserConfigSchema`, etc.).
- TTY detection guards spinner/progress rendering; falls back gracefully on non-TTY.
**Checklist**
- [ ] Read `src/app/app.md` for `App` lifecycle and `cli()` factory
- [ ] Read `src/command/command.md` for fluent subcommand API
- [ ] Read `src/parser/parser.md` for flag/positional parsing behavior
- [ ] Read `src/errors/errors.md` for `CLI_CODES` registry
- [ ] Verify `package.json` exports map for subpath deep imports

View File

@@ -0,0 +1,132 @@
---
name: sp-clipboard
description: Enterprise Clipboard API abstraction with typed errors, OTel metrics, async serialization, and browser fallbacks for text, HTML, images, and blobs.
---
# Clipboard for Suaveplan
Enterprise Clipboard API abstraction for browser-tier consumers. Wraps the async `navigator.clipboard` API with a hidden-textarea + `execCommand` fallback, paste-event read fallback, image/blob copy via `ClipboardItem`, FIFO serialization, secure-context guards, permission change subscriptions, focus-aware retry, optional HTML sanitization, and OpenTelemetry instrumentation.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `ClipboardManager` | Orchestrator that composes navigator/window seams, `AsyncQueue`, observability bundle, and fallback paths; exposes `copyText`, `readText`, `copyRichContent`, `readRichContent`, `checkPermissions`, and `onPermissionChange`. |
| `AsyncQueue` | Minimal FIFO asynchronous queue that serializes clipboard operations on a single tail promise; rejections do not stall later operations. |
| `runCopyText` / `runReadText` / `runCopyRich` / `runReadRich` | Pure operation functions invoked by `ClipboardManager`; handle API invocation, focus-error retry, secure-context checks, and fallback delegation. |
| `copyTextFallback` / `readTextFallback` | Legacy fallback paths: hidden-textarea + `document.execCommand("copy")` for writes, and paste-event interception for reads when the async API is unavailable. |
| `checkPermissions` / `onPermissionChange` | Probe `clipboard-read` and `clipboard-write` permission states via the Permissions API; `onPermissionChange` returns an unsubscribe function. |
| `CLIPBOARD_CODES` / `ClipboardError` | Stable `CLIPBOARD.*` error registry and concrete error class with per-code metadata (HTTP status, severity, retryable). |
| `createClipboardObservability` | Wires four OTel counters (`copyTotal`, `readTotal`, `fallbackUsedTotal`, `errorTotal`) and one histogram (`opLatencyMs`) on a `Meter`. |
| `isSecure` / `waitForNextFocus` | Secure-context guard and focus-aware retry utility: waits for the next `window` `focus` event before re-attempting a `NotAllowedError`. |
| `createTagAllowlistSanitizer` / `identitySanitizer` | Sanitizer hooks for inbound `text/html`; the package does not bundle a real sanitizer to avoid pinning consumers. |
## File Structure
```
foundation/data/clipboard/
src/manager/ # ClipboardManager orchestrator + pure operation functions + helpers
src/errors/ # CLIPBOARD_CODES, ClipboardError, per-code metadata
src/fallback/ # textarea + execCommand and paste-event fallback paths
src/queue/ # AsyncQueue FIFO serializer
src/permissions/ # Permission probe and live change subscription
src/observability/ # OpenTelemetry counters + histogram bundle
src/security/ # Secure-context guards + focus retry utilities
src/sanitizer/ # Sanitizer interface + tag-allowlist helper
src/types/ # ClipboardData, ClipboardResult, ClipboardOptions, etc.
```
Sibling packages:
- `@suaveplan/bytes` — low-level byte utilities (hex/base64/utf8 encoding) for converting clipboard blob payloads
- `@suaveplan/mime` — MIME-type detection and parsing for classifying arbitrary clipboard blob entries
- `@suaveplan/serialize` — structured serialization for persisting clipboard data across process boundaries
## Patterns
### Quick Start
```typescript
import { ClipboardManager } from "@suaveplan/clipboard";
const clipboard = new ClipboardManager({
useFallback: true,
requireSecureContext: false,
serialize: true,
});
const result = await clipboard.copyText("hello");
if (!result.success) {
console.error("copy failed:", result.code, result.error);
}
```
### Copy Rich Content (Text, HTML, Image, Blobs)
```typescript
const clipboard = new ClipboardManager();
const result = await clipboard.copyRichContent({
text: "plain text fallback",
html: "<p>Hello</p>",
image: new Blob([pngBytes], { type: "image/png" }),
});
if (result.success) {
console.log("copied via", result.method);
}
```
### Read with HTML Sanitization
```typescript
import { ClipboardManager, createTagAllowlistSanitizer } from "@suaveplan/clipboard";
const clipboard = new ClipboardManager({
sanitizer: createTagAllowlistSanitizer(["p", "b", "i", "a"]),
});
const result = await clipboard.readRichContent();
if (result.success && result.data.html) {
console.log("sanitized html:", result.data.html);
}
```
### Monitor Permission Changes
```typescript
import { ClipboardManager } from "@suaveplan/clipboard";
const clipboard = new ClipboardManager();
const unsub = await clipboard.onPermissionChange((snapshot, name) => {
console.log(`${name} changed to read=${snapshot.read} write=${snapshot.write}`);
});
// Later...
unsub();
```
### Standalone Permission Check
```typescript
import { checkPermissions } from "@suaveplan/clipboard/permissions/permissions";
const perms = await checkPermissions(navigator as ClipboardNavigator);
if (perms.read === "denied" || perms.write === "denied") {
console.warn("Clipboard permissions denied");
}
```
## Constraints
- All clipboard operations return `ClipboardResult<T>` — they never throw. Check `result.success` before accessing `result.data`.
- `ClipboardManager` is SSR-safe: when `document` / `navigator` / `window` are undefined, it degrades gracefully and returns `API_UNAVAILABLE` or `DOCUMENT_UNAVAILABLE` codes.
- Rich-content read (`readRichContent`) requires the async Clipboard API — there is no fallback path for it.
- The package ships only an identity sanitizer and a regex-based tag allowlist; consumers must supply a real HTML sanitizer (e.g. DOMPurify) for untrusted clipboard HTML.
## Checklist
1. Import from the correct subpath export when tree-shaking matters (`@suaveplan/clipboard/manager/manager`, `@suaveplan/clipboard/permissions/permissions`, etc.)
2. Always check `result.success` before accessing `result.data` — failures carry `code` and `error`, not exceptions
3. Call `clipboard.dispose()` before tearing down a component to drain the internal AsyncQueue
4. Inject a real sanitizer via `ClipboardOptions.sanitizer` when reading untrusted HTML from the clipboard

View File

@@ -0,0 +1,85 @@
---
id: sp-colour-convert-adapt
name: Colour Convert Adapt
library: "@suaveplan/colour-convert-adapt"
description: >
Bradford chromatic adaptation for XYZ colour coordinates. Adjusts a colour's
appearance across different light sources (illuminants) using the industry-
standard Bradford transform, used in ICC colour management.
---
# `@suaveplan/colour-convert-adapt`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **Adapt** | Bradford chromatic adaptation | `bradfordAdapt(xyz, source, dest?)` |
| **Illuminants** | Standard CIE illuminant table | `ILLUMINANTS` — 11 named reference whites |
| **Types** | XYZ coordinate type | `XYZ` interface |
## File Structure
```
src/
├── index.ts # Public barrel
└── adapt/
└── adapt.ts # bradfordAdapt(), ILLUMINANTS, XYZ type
```
## Patterns
### Basic — Adapt between standard illuminants
```ts
import { bradfordAdapt } from "@suaveplan/colour-convert-adapt";
const d50 = bradfordAdapt({ x: 50, y: 50, z: 50 }, "D65", "D50");
console.log(d50); // { x: ~52.9, y: ~50, z: ~37.7 }
```
### Basic — Default destination is D65
```ts
const adapted = bradfordAdapt({ x: 50, y: 50, z: 50 }, "D50");
// equivalent to bradfordAdapt(xyz, "D50", "D65")
```
### Advanced — Use custom XYZ illuminants
```ts
const customSrc = { x: 95, y: 100, z: 108 };
const customDst = { x: 96.4, y: 100, z: 82.5 };
const adapted = bradfordAdapt({ x: 50, y: 50, z: 50 }, customSrc, customDst);
```
### Advanced — Iterate over all named illuminants
```ts
import { ILLUMINANTS } from "@suaveplan/colour-convert-adapt";
const names = Object.keys(ILLUMINANTS); // ["A", "B", "C", "D50", "D55", "D65", "D75", "E", "F2", "F7", "F11"]
const d65 = ILLUMINANTS.D65; // { x: 95.047, y: 100.0, z: 108.883 }
```
## Constraints
- **Universal tier**: pure TypeScript, zero runtime dependencies beyond `tslib`.
- **Bradford matrices**: hard-coded ICC-standard Bradford MA / MA_INV coefficients.
- **11 standard illuminants**: A, B, C, D50, D55, D65, D75, E, F2, F7, F11.
- **Invalid illuminant**: throws `"Invalid illuminant"` for unknown string keys or missing custom XYZ.
- **Identity adaptation**: adapting from an illuminant to itself returns approximately the input XYZ.
- **Sibling packages**: works with `@suaveplan/colours` (RGB↔XYZ) and `@suaveplan/colour-convert-cieluv` (extended XYZ family).
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/colour-convert-adapt` |
| Adapt | `@suaveplan/colour-convert-adapt/adapt` |
## Checklist
- [ ] Need to adapt XYZ between illuminants → `bradfordAdapt(xyz, source, dest)`
- [ ] Need standard illuminant values → `ILLUMINANTS`
- [ ] Need custom illuminant support → pass `XYZ` objects instead of strings

View File

@@ -0,0 +1,110 @@
---
id: sp-colour-convert-cieluv
name: Colour Convert CIELUV
library: "@suaveplan/colour-convert-cieluv"
description: >
CIELUV, xyY, LMS cone response, and Bradford chromatic adaptation conversions.
Transforms between RGB, CIELUV, xyY chromaticity, and LMS cone-response space,
including Bradford adaptation across 11 standard illuminants.
---
# `@suaveplan/colour-convert-cieluv`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **CIELUV** | RGB ↔ CIELUV via XYZ | `rgbToLuv`, `luvToRgb`, `xyzToLuv`, `luvToXyz` |
| **xyY** | Chromaticity coordinates | `rgbToxyY`, `xyYToRgb`, `xyzToxyY`, `xyYToXyz` |
| **LMS** | Cone-response space | `rgbToLms`, `lmsToRgb`, `xyzToLms`, `lmsToXyz` |
| **Bradford** | Chromatic adaptation | `bradfordAdapt(xyz, source, dest?)`, `ILLUMINANTS` |
| **Types** | Coordinate interfaces | `XYZ`, `LUV`, `LUVA`, `xyY`, `LMS` |
## File Structure
```
src/
├── index.ts # Public barrel
└── colour-convert-cieluv/
└── colour-convert-cieluv.ts # All conversions + illuminants
```
## Patterns
### Basic — RGB to CIELUV and back
```ts
import { rgbToLuv, luvToRgb } from "@suaveplan/colour-convert-cieluv";
const luv = rgbToLuv({ r: 128, g: 64, b: 32 });
console.log(luv); // { l: ~32.9, u: ~40.6, v: ~37.8 }
const back = luvToRgb(luv); // → { r: ~128, g: ~64, b: ~32, a: 1 }
```
### Basic — Chromaticity xyY
```ts
import { rgbToxyY, xyYToRgb } from "@suaveplan/colour-convert-cieluv";
const xyY = rgbToxyY({ r: 128, g: 64, b: 32 });
console.log(xyY); // { x: ~0.52, y: ~0.34, Y: ~12.9 }
const back = xyYToRgb(xyY);
```
### Basic — LMS cone response
```ts
import { rgbToLms, lmsToRgb } from "@suaveplan/colour-convert-cieluv";
const lms = rgbToLms({ r: 128, g: 64, b: 32 });
const back = lmsToRgb(lms);
```
### Advanced — Bradford adaptation (D65 → D50)
```ts
import { bradfordAdapt, ILLUMINANTS } from "@suaveplan/colour-convert-cieluv";
const adapted = bradfordAdapt(
{ x: 95.047, y: 100, z: 108.883 },
"D65",
"D50"
);
// → { x: ~96.4, y: 100, z: ~82.5 }
```
### Advanced — Custom illuminant adaptation
```ts
const customSrc = { x: 95, y: 100, z: 108 };
const customDst = { x: 96.4, y: 100, z: 82.5 };
const adapted = bradfordAdapt({ x: 50, y: 50, z: 50 }, customSrc, customDst);
```
## Constraints
- **Depends on `@suaveplan/colours`**: imports `RGB` / `RGBA` types from the core colours package.
- **D65 reference white**: all CIELUV conversions use D65 as the reference illuminant.
- **Black handling**: `luvToXyz({ l: 0, u: 0, v: 0 })` returns `{ x: 0, y: 0, z: 0 }`.
- **xyY zero-sum guard**: `xyzToxyY({ x: 0, y: 0, z: 0 })` returns `{ x: 0, y: 0, Y: 0 }`.
- **xyY y=0 guard**: `xyYToXyz({ x: 0.3, y: 0, Y: 0 })` returns `{ x: 0, y: 0, z: 0 }`.
- **Round-trip tolerance**: RGB→LUV→RGB and RGB→LMS→RGB may differ by ~1 channel unit due to quantisation.
- **11 illuminants**: A, B, C, D50, D55, D65, D75, E, F2, F7, F11.
- **Invalid illuminant**: `bradfordAdapt` throws for unknown string keys.
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/colour-convert-cieluv` |
| Module | `@suaveplan/colour-convert-cieluv/colour-convert-cieluv` |
## Checklist
- [ ] Need CIELUV conversions → `rgbToLuv`, `luvToRgb`, `xyzToLuv`, `luvToXyz`
- [ ] Need chromaticity xyY → `rgbToxyY`, `xyYToRgb`, `xyzToxyY`, `xyYToXyz`
- [ ] Need LMS cone response → `rgbToLms`, `lmsToRgb`, `xyzToLms`, `lmsToXyz`
- [ ] Need Bradford adaptation → `bradfordAdapt(xyz, source, dest)`
- [ ] Need illuminant constants → `ILLUMINANTS`

View File

@@ -0,0 +1,90 @@
---
id: sp-colour-convert-cmyk
name: Colour Convert CMYK
library: "@suaveplan/colour-convert-cmyk"
description: >
Bidirectional RGB ↔ CMYK colour space conversion with black generation for
print workflows. Uses the standard Euclidean algorithm essential for print
preview, pre-press, and bridging screen colours with physical output.
---
# `@suaveplan/colour-convert-cmyk`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **CMYK** | RGB ↔ CMYK conversion | `rgbToCmyk(rgb)`, `cmykToRgb(cmyk)` |
| **Types** | CMYK ink value type | `CMYK` interface |
## File Structure
```
src/
├── index.ts # Public barrel
└── cmyk/
└── cmyk.ts # rgbToCmyk(), cmykToRgb(), CMYK type
```
## Patterns
### Basic — Convert RGB to CMYK
```ts
import { rgbToCmyk } from "@suaveplan/colour-convert-cmyk";
const ink = rgbToCmyk({ r: 59, g: 130, b: 246 });
console.log(ink); // { c: 76, m: 47, y: 0, k: 4 }
```
### Basic — Convert CMYK back to RGB
```ts
import { cmykToRgb } from "@suaveplan/colour-convert-cmyk";
const screen = cmykToRgb({ c: 76, m: 47, y: 0, k: 4 });
console.log(screen); // { r: 59, g: 130, b: 246, a: 1 }
```
### Basic — Round-trip conversion
```ts
import { rgbToCmyk, cmykToRgb } from "@suaveplan/colour-convert-cmyk";
const original = { r: 128, g: 64, b: 32 };
const cmyk = rgbToCmyk(original);
const back = cmykToRgb(cmyk);
// back ≈ original (within ±1 due to integer rounding)
```
### Advanced — Pure colour analysis
```ts
rgbToCmyk({ r: 255, g: 0, b: 0 }); // → { c: 0, m: 100, y: 100, k: 0 } — pure red
rgbToCmyk({ r: 0, g: 255, b: 0 }); // → { c: 100, m: 0, y: 100, k: 0 } — pure green
rgbToCmyk({ r: 0, g: 0, b: 255 }); // → { c: 100, m: 100, y: 0, k: 0 } — pure blue
rgbToCmyk({ r: 255, g: 255, b: 255 }); // → { c: 0, m: 0, y: 0, k: 0 } — white
rgbToCmyk({ r: 0, g: 0, b: 0 }); // → { c: 0, m: 0, y: 0, k: 100 } — black
```
## Constraints
- **Universal tier**: pure TypeScript, zero runtime dependencies beyond `@suaveplan/colours` types and `tslib`.
- **CMYK values**: integers 0–100 (percentage of ink coverage).
- **Black generation**: `k = 1 - max(r, g, b)`; when `k === 1`, c/m/y are all 0 (pure black).
- **Round-trip loss**: `rgbToCmyk` → `cmykToRgb` may differ by ~1 due to integer rounding of CMYK percentages.
- **RGB input**: uses `@suaveplan/colours` `RGB` type (`{ r, g, b }` with 0–255 channels).
- **RGBA output**: `cmykToRgb` returns `RGBA` with alpha pinned to 1.
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/colour-convert-cmyk` |
| CMYK | `@suaveplan/colour-convert-cmyk/cmyk` |
## Checklist
- [ ] Need RGB to CMYK → `rgbToCmyk(rgb)`
- [ ] Need CMYK to RGB → `cmykToRgb(cmyk)`
- [ ] Need round-trip print preview → `cmykToRgb(rgbToCmyk(rgb))`

View File

@@ -0,0 +1,97 @@
---
id: sp-colour-convert-hsluv
name: Colour Convert HSLuv
library: "@suaveplan/colour-convert-hsluv"
description: >
HSLuv perceptually uniform colour space with gamut-mapped ray intersection
for sRGB. Provides bidirectional RGB↔HSLuv/HPLuv conversions and uniform
colour harmonies (complementary, triad, tetrad) that preserve perceptual
lightness across hue rotations.
---
# `@suaveplan/colour-convert-hsluv`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **HSLuv** | Perceptually uniform RGB↔HSLuv | `rgbToHsluv`, `hsluvToRgb` |
| **HPLuv** | Variant with fixed L=50% saturation | `rgbToHpluv`, `hpluvToRgb` |
| **Harmonies** | Uniform colour schemes | `uniformComplementary`, `uniformTriad`, `uniformTetrad` |
| **Types** | Coordinate interfaces | `HSLUV`, `HPLUV` |
## File Structure
```
src/
├── index.ts # Public barrel
└── colour-convert-hsluv/
└── colour-convert-hsluv.ts # HSLuv/HPLuv conversions + harmonies
```
## Patterns
### Basic — RGB to HSLuv and back
```ts
import { rgbToHsluv, hsluvToRgb } from "@suaveplan/colour-convert-hsluv";
const hsluv = rgbToHsluv({ r: 255, g: 0, b: 0 });
console.log(hsluv); // { h: ~12.2, s: ~100, l: ~53.2 }
const back = hsluvToRgb(hsluv); // → { r: ~255, g: ~0, b: ~0, a: 1 }
```
### Basic — HPLuv variant
```ts
import { rgbToHpluv, hpluvToRgb } from "@suaveplan/colour-convert-hsluv";
const hpluv = rgbToHpluv({ r: 255, g: 0, b: 0 });
const back = hpluvToRgb(hpluv);
```
### Advanced — Uniform colour harmonies
```ts
import { uniformComplementary, uniformTriad, uniformTetrad } from "@suaveplan/colour-convert-hsluv";
const primary = { r: 59, g: 130, b: 246 };
const comp = uniformComplementary(primary); // 2 colours: [complement, primary]
const triad = uniformTriad(primary); // 3 colours: [primary, +120°, +240°]
const tetrad = uniformTetrad(primary); // 4 colours: [primary, +90°, +180°, +270°]
```
### Advanced — Construct HSLuv directly
```ts
import { hsluvToRgb } from "@suaveplan/colour-convert-hsluv";
const rgb = hsluvToRgb({ h: 120, s: 100, l: 50 });
// Gamut-mapped to valid sRGB; out-of-gamut hues clamp automatically
```
## Constraints
- **Depends on `@suaveplan/colours`**: imports `RGB` / `RGBA` types from the core colours package.
- **Perceptually uniform**: unlike HSL, HSLuv preserves perceptual lightness (L) across hue rotations.
- **Gamut mapping**: `maxChromaForLh` computes the maximum valid chroma for a given lightness/hue via ray intersection with the sRGB cube; out-of-gamut values are clamped.
- **HPLuv difference**: HPLuv uses a fixed reference lightness of 50% for saturation computation, making it a cylindrical projection rather than a cone.
- **Black/white handling**: `l=0` returns black, `l=100` returns white regardless of hue/chroma.
- **Round-trip tolerance**: RGB→HSLuv→RGB may differ by ~1 channel unit due to floating-point math and gamut clamping.
- **Harmony outputs**: all harmony functions return `RGB[]` arrays with the input colour included.
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/colour-convert-hsluv` |
| Module | `@suaveplan/colour-convert-hsluv/colour-convert-hsluv` |
## Checklist
- [ ] Need perceptually uniform HSL → `rgbToHsluv`, `hsluvToRgb`
- [ ] Need HPLuv variant → `rgbToHpluv`, `hpluvToRgb`
- [ ] Need uniform harmonies → `uniformComplementary`, `uniformTriad`, `uniformTetrad`
- [ ] Need gamut-safe colour construction → `hsluvToRgb({ h, s, l })`

View File

@@ -0,0 +1,81 @@
---
id: sp-colour-filter-cct
name: Colour Filter CCT
library: "@suaveplan/colour-filter-cct"
description: >
Correlated Colour Temperature (CCT) calculation and approximation using the
McCamy formula and black-body RGB approximation. Convert RGB to Kelvin or
generate RGB from a temperature for white-balance and lighting design.
---
# `@suaveplan/colour-filter-cct`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **CCT** | RGB ↔ Kelvin conversions | `cctFromRgb(rgb)`, `cctToRgb(kelvin)` |
## File Structure
```
src/
├── index.ts # Public barrel
└── colour-filter-cct/
└── colour-filter-cct.ts # cctFromRgb(), cctToRgb()
```
## Patterns
### Basic — Get colour temperature from RGB
```ts
import { cctFromRgb } from "@suaveplan/colour-filter-cct";
const kelvin = cctFromRgb({ r: 255, g: 240, b: 220 });
console.log(kelvin); // ~6500
```
### Basic — Generate RGB from colour temperature
```ts
import { cctToRgb } from "@suaveplan/colour-filter-cct";
const warmWhite = cctToRgb(2700);
console.log(warmWhite); // { r: ~255, g: ~169, b: ~87, a: 1 }
const daylight = cctToRgb(6500);
console.log(daylight); // { r: ~255, g: ~249, b: ~253, a: 1 }
```
### Advanced — Round-trip approximation
```ts
import { cctFromRgb, cctToRgb } from "@suaveplan/colour-filter-cct";
const cct = cctFromRgb({ r: 255, g: 255, b: 255 });
const back = cctToRgb(cct); // approximate white
```
## Constraints
- **Depends on `@suaveplan/colours`**: imports `RGB` / `RGBA` types.
- **McCamy formula**: `cctFromRgb` uses the McCamy approximation from xyY chromaticity.
- **Black-body approximation**: `cctToRgb` uses piecewise logarithmic/power curves fitted to Planckian locus.
- **Clamped range**: `cctToRgb` clamps input to 1000–40000 K.
- **Boundary behaviour**: at 1900 K blue channel is 0; at 6600 K+ blue channel is 255.
- **Black handling**: `cctFromRgb({ r: 0, g: 0, b: 0 })` returns a finite number (avoids divide-by-zero).
- **Universal tier**: pure TypeScript, zero runtime dependencies beyond `@suaveplan/colours` and `tslib`.
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/colour-filter-cct` |
| Module | `@suaveplan/colour-filter-cct/colour-filter-cct` |
## Checklist
- [ ] Need colour temperature from RGB → `cctFromRgb(rgb)`
- [ ] Need RGB from colour temperature → `cctToRgb(kelvin)`
- [ ] Need white-balance correction → combine `cctFromRgb` + `cctToRgb`

View File

@@ -0,0 +1,77 @@
---
id: sp-colour-filter-cmc
name: Colour Filter CMC
library: "@suaveplan/colour-filter-cmc"
description: >
CMC l:c Delta-E colour difference metric with hue-dependent chroma scaling
and low-luminance branch handling. Computes perceptual distance between two
colours using the Colour Measurement Committee's formula, widely used in
textile and industrial colour matching.
---
# `@suaveplan/colour-filter-cmc`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **CMC** | Delta-E distance metric | `cmcDistance(a, b, luminanceWeight?, chromaWeight?)` |
## File Structure
```
src/
├── index.ts # Public barrel
└── colour-filter-cmc/
└── colour-filter-cmc.ts # cmcDistance()
```
## Patterns
### Basic — Perceptual distance between two colours
```ts
import { cmcDistance } from "@suaveplan/colour-filter-cmc";
const d = cmcDistance({ r: 255, g: 0, b: 0 }, { r: 0, g: 0, b: 255 });
console.log(d); // perceptual distance between red and blue
```
### Basic — Same colour is zero distance
```ts
const rgb = { r: 128, g: 64, b: 32 };
cmcDistance(rgb, rgb); // → 0
```
### Advanced — Custom l:c weights
```ts
// Default weights: l=2, c=1 (textile standard)
const textile = cmcDistance(red, blue, 2, 1);
// Equal weighting for graphic arts
const graphic = cmcDistance(red, blue, 1, 1);
```
## Constraints
- **Depends on `@suaveplan/colours`**: imports `RGB` type.
- **CMC l:c formula**: hue-dependent `t` factor, low-luminance `sl` branch, chroma scaling `sc`, hue scaling `sh`.
- **Default weights**: `luminanceWeight = 2`, `chromaWeight = 1` (textile industry standard).
- **Same colour**: returns exactly 0.
- **Lab internal**: converts RGB → LAB internally using D65 reference white.
- **Universal tier**: pure TypeScript, zero runtime dependencies beyond `@suaveplan/colours` and `tslib`.
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/colour-filter-cmc` |
| Module | `@suaveplan/colour-filter-cmc/colour-filter-cmc` |
## Checklist
- [ ] Need perceptual colour distance → `cmcDistance(a, b)`
- [ ] Need custom weighting → `cmcDistance(a, b, lWeight, cWeight)`
- [ ] Need textile-quality control metric → default weights (2, 1)

View File

@@ -0,0 +1,66 @@
---
id: sp-colour-filter-greyscale
name: Colour Filter Greyscale
library: "@suaveplan/colour-filter-greyscale"
description: >
Perceptual RGB-to-greyscale filter using the ITU-R BT.709 luminance
coefficients. Weights green most heavily to match human visual sensitivity,
producing correctly balanced greyscale results.
---
# `@suaveplan/colour-filter-greyscale`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **Greyscale** | Perceptual luminance filter | `greyscale(rgb)` → `RGBA` |
## File Structure
```
src/
├── index.ts # Public barrel
└── greyscale/
└── greyscale.ts # greyscale()
```
## Patterns
### Basic — Convert to perceptual greyscale
```ts
import { greyscale } from "@suaveplan/colour-filter-greyscale";
const mono = greyscale({ r: 255, g: 128, b: 0 });
console.log(mono); // { r: 150, g: 150, b: 150, a: 1 }
```
### Advanced — Compare channel luminance
```ts
const red = greyscale({ r: 255, g: 0, b: 0 }); // { r: 54, ... }
const green = greyscale({ r: 0, g: 255, b: 0 }); // { r: 182, ... }
const blue = greyscale({ r: 0, g: 0, b: 255 }); // { r: 18, ... }
// green appears brightest to human eyes
```
## Constraints
- **Depends on `@suaveplan/colours`**: imports `RGB` / `RGBA` types.
- **ITU-R BT.709 coefficients**: `Y = 0.2126*R + 0.7152*G + 0.0722*B`.
- **Alpha pinned**: output alpha is always 1.
- **Identity**: greyscale of already-grey colours returns the same value.
- **Universal tier**: pure TypeScript, zero runtime dependencies beyond `@suaveplan/colours` and `tslib`.
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/colour-filter-greyscale` |
| Greyscale | `@suaveplan/colour-filter-greyscale/greyscale` |
## Checklist
- [ ] Need perceptual greyscale → `greyscale(rgb)`
- [ ] Need luminance value extraction → read `result.r` (all channels equal)

View File

@@ -0,0 +1,69 @@
---
id: sp-colour-filter-sepia
name: Colour Filter Sepia
library: "@suaveplan/colour-filter-sepia"
description: >
Photographic sepia tone filter for RGB colours using the standard W3C CSS
Filters 3×3 matrix. Converts any colour into a warm brownish tone that
simulates the look of aged photographs.
---
# `@suaveplan/colour-filter-sepia`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **Sepia** | CSS matrix sepia filter | `sepia(rgb)` → `RGBA` |
## File Structure
```
src/
├── index.ts # Public barrel
└── sepia/
└── sepia.ts # sepia()
```
## Patterns
### Basic — Apply sepia tone
```ts
import { sepia } from "@suaveplan/colour-filter-sepia";
const vintage = sepia({ r: 255, g: 0, b: 0 });
console.log(vintage); // { r: 100, g: 89, b: 69, a: 1 }
```
### Advanced — Batch process palette
```ts
const colours = [{ r: 255, g: 0, b: 0 }, { r: 0, g: 128, b: 255 }];
const sepiaTones = colours.map(sepia);
```
## Constraints
- **Depends on `@suaveplan/colours`**: imports `RGB` / `RGBA` types.
- **W3C CSS Filters matrix**: standard 3×3 sepia coefficients.
- `R' = 0.393*R + 0.769*G + 0.189*B`
- `G' = 0.349*R + 0.686*G + 0.168*B`
- `B' = 0.272*R + 0.534*G + 0.131*B`
- **Clamped output**: channels clamped to 255 maximum (no overflow).
- **Black identity**: `sepia({ r: 0, g: 0, b: 0 })` returns black.
- **White warm**: `sepia({ r: 255, g: 255, b: 255 })` returns warm white `{ r: 255, g: 255, b: 239 }`.
- **Alpha pinned**: output alpha is always 1.
- **Universal tier**: pure TypeScript, zero runtime dependencies beyond `@suaveplan/colours` and `tslib`.
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/colour-filter-sepia` |
| Sepia | `@suaveplan/colour-filter-sepia/sepia` |
## Checklist
- [ ] Need sepia tone filter → `sepia(rgb)`
- [ ] Need vintage photo effect → apply to entire palette with `.map(sepia)`

View File

@@ -0,0 +1,166 @@
---
id: sp-colours
name: Colours
library: "@suaveplan/colours"
description: >
Universal colour manipulation library: parsing, conversion, analysis, mixing,
blending, and palette generation. Parses hex, CSS rgb()/hsl()/lab()/lch()/oklab()/oklch(),
named colours, and objects. Converts between RGB, HSL, HSV, LAB, LCH, OKLAB,
OKLCH, XYZ, and CMYK. Immutable chainable Colour class with luminance,
contrast, distance, and blend modes.
---
# `@suaveplan/colours`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **Colour** | Immutable chainable value object | `Colour` class |
| **Formatters** | String output for all spaces | `formatHex`, `formatRgb`, `formatHsl`, `formatLab`, … |
| **Factories** | Generators and blenders | `randomColour`, `colourFromTemperature`, `blendColours`, `colourSteps`, `generatePalette` |
| **Conversions** | Colour-space transforms | `rgbToHsl`, `hslToRgb`, `rgbToLab`, `labToRgb`, `rgbToOklab`, `oklabToRgb`, … |
| **Parser** | Input normalisation | `parseColour(input)` → `RGBA` |
| **Types** | TypeScript + Zod schemas | `RGBA`, `HSLA`, `LAB`, `LCH`, `OKLAB`, `OKLCH`, `CMYK`, schemas |
| **Utils** | Math helpers | `clamp`, `roundChannel`, `roundAlpha`, `isFiniteNumber` |
| **Errors** | Typed errors | `ColourError`, `COLOUR_CODES` |
## File Structure
```
src/
├── index.ts # Public barrel
├── colour/
│ ├── colour.ts # Colour class (immutable, chainable)
│ └── colour-formatters.ts # formatHex, formatRgb, formatHsl, …
├── colour-factories/
│ └── colour-factories.ts # randomColour, blendColours, generatePalette
├── conversions/
│ └── conversions.ts # rgbToHsl, hslToRgb, rgbToLab, labToRgb, …
├── parser/
│ └── parser.ts # parseColour(input)
├── types/
│ └── colour-types.ts # TypeScript types + Zod schemas + NAMED_COLOURS
├── utils/
│ └── utils.ts # clamp, roundChannel, roundAlpha
├── errors/
│ └── errors.ts # ColourError factory functions
└── error-codes/
└── error-codes.ts # COLOUR_CODES registry
```
## Patterns
### Basic — Create and manipulate a colour
```ts
import { Colour } from "@suaveplan/colours";
const c = new Colour("#ff9900");
console.log(c.lighten(0.2).toHex()); // "#ffb34d"
console.log(c.isDark()); // false
console.log(c.contrast("#ffffff")); // WCAG contrast ratio
```
### Basic — Parse arbitrary input
```ts
import { parseColour } from "@suaveplan/colours";
const rgba = parseColour("hsl(120 50% 50%)");
// → { r: 64, g: 191, b: 64, a: 1 }
const rgba2 = parseColour({ r: 255, g: 128, b: 0 });
// → { r: 255, g: 128, b: 0, a: 1 }
```
### Basic — Convert between colour spaces
```ts
import { rgbToHsl, hslToRgb, rgbToLab, labToRgb } from "@suaveplan/colours";
const hsl = rgbToHsl({ r: 255, g: 0, b: 0, a: 1 });
// → { h: 0, s: 100, l: 50, a: 1 }
const rgb = hslToRgb({ h: 120, s: 100, l: 50, a: 1 });
// → { r: 0, g: 255, b: 0, a: 1 }
```
### Advanced — Mix colours in perceptual space
```ts
import { Colour } from "@suaveplan/colours";
const red = new Colour("#ff0000");
const blue = new Colour("#0000ff");
const mixed = red.mix(blue, 50, "oklab");
// 50% mix in OKLAB perceptual uniform space
```
### Advanced — Generate palettes and gradients
```ts
import { generatePalette, colourSteps, colourFromTemperature } from "@suaveplan/colours";
// Harmonic palette
const palette = generatePalette("#ff0000", "analogous");
// Gradient steps
const steps = colourSteps("#ff0000", "#0000ff", 5);
// Black-body temperature
const warm = colourFromTemperature(3000);
```
### Advanced — Colour distance and accessibility
```ts
import { Colour } from "@suaveplan/colours";
const bg = new Colour("#ffffff");
const fg1 = new Colour("#333333");
const fg2 = new Colour("#666666");
const best = bg.bestContrast(fg1, fg2);
// Returns whichever has higher WCAG contrast ratio
const dist = bg.distance(fg1, "oklab");
// Perceptual distance in OKLAB space
```
## Constraints
- **Universal tier**: pure TypeScript, zero runtime dependencies beyond `@suaveplan/error` and `zod`.
- **Immutable**: all `Colour` methods return new instances; internal `_rgba` is never mutated.
- **Parser supports**: hex (3/4/6/8 digit), CSS `rgb()`, `hsl()`, `lab()`, `lch()`, `oklab()`, `oklch()`, named colours, plain objects.
- **Mix modes**: `rgb`, `hsl`, `lab`, `oklab`, `lch`, `oklch`. Default is `oklab`.
- **Distance modes**: `euclidean`, `cie76`, `cie94`, `oklab`. Default is `oklab`.
- **Luminance**: uses the WCAG 2.1 relative luminance formula.
- **Error codes**: `COLOUR.INVALID_INPUT`, `COLOUR.UNSUPPORTED_FORMAT`, `COLOUR.CONVERSION_FAILED`.
- **Named colours**: 147 CSS named colours shipped in `NAMED_COLOURS` map.
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/colours` |
| Colour | `@suaveplan/colours/colour` |
| Parser | `@suaveplan/colours/parser` |
| Conversions | `@suaveplan/colours/conversions` |
| Types | `@suaveplan/colours/types` |
| Utils | `@suaveplan/colours/utils` |
| Errors | `@suaveplan/colours/errors` |
| Error Codes | `@suaveplan/colours/error-codes` |
## Checklist
- [ ] Need to parse colour input → `parseColour(input)`
- [ ] Need immutable colour manipulation → `Colour` class
- [ ] Need colour-space conversion → `rgbToHsl`, `hslToRgb`, `rgbToLab`, …
- [ ] Need to mix/blend colours → `colour.mix(other, weight, mode)`
- [ ] Need contrast checking → `colour.contrast(other)`
- [ ] Need accessibility best-contrast → `colour.bestContrast(...options)`
- [ ] Need colour distance → `colour.distance(other, mode)`
- [ ] Need palette generation → `generatePalette(base, type)`
- [ ] Need gradient steps → `colourSteps(start, end, count)`
- [ ] Need temperature-based colours → `colourFromTemperature(kelvin)`

View File

@@ -0,0 +1,158 @@
---
id: sp-compress-image
name: Compress Image
library: "@suaveplan/compress-image"
description: >
Runtime-agnostic image compression utilities with pluggable adapters.
Supports resize, quality reduction, format conversion (JPEG, PNG, WebP, AVIF),
and metadata stripping. Core is pure TypeScript; Sharp adapter requires the
native `sharp` library.
---
# `@suaveplan/compress-image`
## Architecture
| Module | Role | Key exports |
|--------|------|-------------|
| **Compressor** | Orchestrator + validation + OTel | `ImageCompressor`, `createCompressor(adapter?)` |
| **Sharp Adapter** | Production Node.js adapter | `SharpAdapter` — wraps `sharp` library |
| **Memory Adapter** | Test / no-op adapter | `MemoryAdapter` — passthrough with format detection |
| **Schemas** | Zod validation | `CompressOptionsSchema` |
| **Types** | TypeScript contracts | `ImageAdapter`, `CompressOptions`, `CompressResult`, `ImageInfo` |
| **Utils** | Pure helpers | `detectFormat`, `parseJpegDimensions`, `parsePngDimensions`, `validateDimensions` |
| **Errors** | Typed errors | `CompressImageError`, `COMPRESS_IMAGE_CODES` |
## File Structure
```
src/
├── index.ts # Public barrel
├── core/
│ └── compressor/
│ └── compressor.ts # ImageCompressor, createCompressor()
├── adapters/
│ ├── sharp/
│ │ └── sharp.ts # SharpAdapter (production)
│ └── memory/
│ └── memory.ts # MemoryAdapter (testing)
├── schemas/
│ └── compress-options.schema.ts # Zod schema for options
├── types/
│ └── compress-image.types.ts # TypeScript interfaces
├── utils/
│ └── image-utils.ts # detectFormat, validateDimensions
└── errors/
└── compress-image-error.ts # CompressImageError, COMPRESS_IMAGE_CODES
```
## Patterns
### Basic — Compress with Sharp adapter
```ts
import { createCompressor } from "@suaveplan/compress-image/compressor";
import { SharpAdapter } from "@suaveplan/compress-image/adapters/sharp";
import sharp from "sharp";
const compressor = createCompressor(new SharpAdapter(sharp));
const result = await compressor.compress(imageBuffer, {
format: "webp",
quality: 75,
width: 1280,
height: 720,
resizeMode: "cover",
stripMetadata: true,
});
console.log(result.compressionRatio, result.compressedSize);
```
### Basic — Compress only if larger than threshold
```ts
const result = await compressor.compressIfLarger(imageBuffer, 500_000, {
format: "jpeg",
quality: 80,
});
// If input ≤ 500KB: returns original with ratio = 1
// If input > 500KB: compresses and returns compressed data
```
### Basic — Extract metadata
```ts
const info = await compressor.getInfo(imageBuffer);
console.log(info.format, info.width, info.height, info.size);
```
### Basic — Resize
```ts
const resized = await compressor.resize(imageBuffer, 400, 300, "contain");
// Returns Uint8Array of resized image
```
### Advanced — Swap adapter at runtime
```ts
const compressor = createCompressor(new MemoryAdapter());
// Later, swap to Sharp for production
compressor.setAdapter(new SharpAdapter(sharp));
```
### Advanced — Write a custom adapter
```ts
import type { ImageAdapter, CompressOptions, CompressResult } from "@suaveplan/compress-image";
class MyAdapter implements ImageAdapter {
async compress(input: Uint8Array, options: CompressOptions): Promise<CompressResult> {
// … implementation …
}
async getInfo(input: Uint8Array): Promise<ImageInfo> {
// … implementation …
}
async resize(input: Uint8Array, w: number, h: number, mode?: ResizeMode): Promise<Uint8Array> {
// … implementation …
}
}
```
## Constraints
- **Adapter pattern**: core is pure TypeScript; actual image processing delegated to pluggable `ImageAdapter`.
- **Sharp is optional**: only required when using `SharpAdapter`. Package has zero direct dependency on `sharp`.
- **Zod validation**: all options validated before adapter call; invalid options throw `CompressImageError(INVALID_OPTIONS)`.
- **Dimension limits**: width/height must be 1–32767 inclusive.
- **Quality range**: 1–100 inclusive, integer only.
- **Effort range**: 0–6 inclusive (Sharp-specific encoding effort).
- **Supported formats**: `jpeg`, `png`, `webp`, `avif`.
- **Resize modes**: `cover`, `contain`, `fill`, `inside`, `outside`.
- **OTel**: `compress_image.compress.total` (counter), `compress_image.compress.duration_ms` (histogram).
- **Error codes**: `COMPRESS_IMAGE.UNSUPPORTED_FORMAT`, `COMPRESS_IMAGE.COMPRESS_FAILED`, `COMPRESS_IMAGE.INVALID_OPTIONS`, `COMPRESS_IMAGE.ADAPTER_NOT_SET`.
- **MemoryAdapter**: safe for tests; detects format and returns passthrough data with synthetic metadata.
## Subpath Exports
| Export | Path |
|--------|------|
| Main | `@suaveplan/compress-image` |
| Compressor | `@suaveplan/compress-image/compressor` |
| Sharp Adapter | `@suaveplan/compress-image/adapters/sharp` |
| Memory Adapter | `@suaveplan/compress-image/adapters/memory` |
| Errors | `@suaveplan/compress-image/errors` |
## Checklist
- [ ] Need to compress images → `compressor.compress(input, options)`
- [ ] Need conditional compression → `compressor.compressIfLarger(input, maxBytes, options)`
- [ ] Need to resize → `compressor.resize(input, width, height, mode)`
- [ ] Need metadata extraction → `compressor.getInfo(input)`
- [ ] Need Sharp integration → `new SharpAdapter(sharp)`
- [ ] Need test adapter → `new MemoryAdapter()`
- [ ] Need custom adapter → implement `ImageAdapter` interface
- [ ] Need Zod option validation → `CompressOptionsSchema`
- [ ] Need typed errors → `CompressImageError`, `COMPRESS_IMAGE_CODES`

View File

@@ -0,0 +1,126 @@
---
name: sp-compress-svg
description: SVG optimisation and minification with pure-TypeScript parser, plugin pipeline, and SVGO-compatible presets.
---
# Compress SVG for Suaveplan
Pure-TypeScript SVG minifier with no DOM dependencies. Parses SVG strings into an AST, runs a plugin pipeline with optional multipass, and serializes back to compact markup. Ships with 8 built-in plugins and 3 presets, plus OpenTelemetry instrumentation.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `compressSvg` | Orchestrates parse → plugin pipeline → multipass loop → serialize, returning `CompressResult` with size metrics |
| `parseSvg` | Hand-written push parser that tokenizes raw SVG into `SvgNode` AST (elements, text, comments, CDATA, DOCTYPE) |
| `serializeSvg` | Compact serializer that renders `SvgNode` back to XML, collapsing empty elements to self-closing tags |
| `applyPlugins` | Recursively walks the AST depth-first, passing each node through every plugin in order |
| `SvgPlugin` | Contract object `{ name, apply(node, options) }` — returns mutated node, `null` to remove |
| `cleanupAttrs` | Strips redundant default attributes (e.g. `fill="black"`, `opacity="1"`) and empty values |
| `convertPathData` | Rounds numeric coordinates in `path@d` to `floatPrecision` decimal places |
| `convertTransforms` | Rounds numeric values in `transform` attributes to `transformPrecision` decimal places |
| `collapseGroups` | Removes single-child `<g>` elements by merging their attributes onto the child (skips if `id` present or attr conflicts) |
| `mangleIds` / `createMangleIds` | Shortens `id` attributes to generated short IDs and rewrites `href` / `xlink:href` fragment references |
| `removeComments` | Drops `<!-- … -->` comment nodes from the AST |
| `removeMetadata` | Drops `<metadata>` element nodes |
| `removeDoctype` | Drops `<!DOCTYPE …>` doctype nodes |
## File Structure
```
devtools/docs/compress-svg/
src/core/compress-svg/ # compressSvg entry point and CompressResult types
src/parser/svg/ # parseSvg / serializeSvg push-parser and compact serializer
src/plugins/*/ # 8 built-in plugins (cleanup-attrs, collapse-groups, convert-path-data, convert-transforms, mangle-ids, remove-comments, remove-doctype, remove-metadata)
src/presets/ # PRESET_DEFAULT, PRESET_SAFE, PRESET_AGGRESSIVE
src/types/ # SvgNode, SvgPlugin, SvgPluginOptions, CompressOptions, CompressResult, Zod schemas
src/error-codes/ # SVG_COMPRESS_CODES and SvgCompressError via @suaveplan/error
src/constants/ # DEFAULT_FLOAT_PRECISION (3), DEFAULT_TRANSFORM_PRECISION (5), MAX_MULTIPASS_ITERATIONS (10), default SVG attribute map
```
Sibling packages:
- `@suaveplan/pdf` — Server-tier PDF 1.4 generation toolkit for document output
- `@suaveplan/sitemap` — Sitemap.xml generation and parsing per sitemaps.org protocol
- `@suaveplan/docs` — Markdown documentation toolkit with monorepo discovery and search
## Patterns
### Quick Start
```typescript
import { compressSvg } from '@suaveplan/compress-svg';
const result = compressSvg(svgString);
console.log(result.data); // optimised SVG
console.log(result.ratio); // e.g. 0.78
console.log(result.passCount); // multipass iterations used
```
### Using Presets
```typescript
import { compressSvg, PRESET_DEFAULT, PRESET_SAFE, PRESET_AGGRESSIVE } from '@suaveplan/compress-svg';
// Default: removeComments + metadata + doctype + collapseGroups + cleanupAttrs + convertPathData
compressSvg(svg, { plugins: PRESET_DEFAULT });
// Safe: no ID mangling, no transform conversion
compressSvg(svg, { plugins: PRESET_SAFE });
// Aggressive: all plugins including mangleIds and convertTransforms
compressSvg(svg, { plugins: PRESET_AGGRESSIVE });
```
### Custom Plugin
```typescript
import type { SvgPlugin } from '@suaveplan/compress-svg';
const removeTitle: SvgPlugin = {
name: 'removeTitle',
apply(node) {
return node.type === 'element' && node.name === 'title' ? null : node;
},
};
compressSvg(svg, { plugins: [removeTitle, ...PRESET_DEFAULT] });
```
### Standalone Parser / Serializer
```typescript
import { parseSvg, serializeSvg } from '@suaveplan/compress-svg/parser/svg';
const node = parseSvg('<svg><circle r="5"/></svg>');
const serialized = serializeSvg(node);
```
### Subpath Export for a Single Plugin
```typescript
import { convertPathData } from '@suaveplan/compress-svg/plugins/convert-path-data';
import { mangleIds, createMangleIds } from '@suaveplan/compress-svg/plugins/mangle-ids';
// createMangleIds returns a fresh plugin instance with isolated ID mapping
const freshMangle = createMangleIds();
```
## Constraints
- **Pure TypeScript — no DOMParser or external SVG libraries.** The parser is hand-written and runs in Bun, Node.js, browsers, and edge runtimes.
- **Plugins must be pure functions.** A plugin's `apply` must not mutate the input node in place; it should return a new object or `null` to remove the node.
- **`compressSvg` defaults to multipass with a hard ceiling of 10 iterations.** Passes stop early when serialized byte size stabilizes.
- **`mangleIds` is stateful per instance.** Reuse the same `mangleIds` export across multiple SVGs and IDs will collide; use `createMangleIds()` for isolated mappings.
- **`collapseGroups` never collapses a `<g>` with an `id` attribute** or when group and child have conflicting attributes, to preserve selector targets and rendering.
- **Default attribute allowlist is fixed.** `cleanupAttrs` only strips the 11 hard-coded defaults (e.g. `fill="black"`, `opacity="1"`); custom defaults are not configurable.
- **Error codes are namespaced via `defineCodes`.** Always assert against `SVG_COMPRESS_CODES.PARSE_FAILED`, `INVALID_SVG`, or `PLUGIN_FAILED` — never raw strings.
## Checklist
1. Import `compressSvg` from the root or `@suaveplan/compress-svg/core/compress-svg` subpath.
2. Decide on a preset (DEFAULT / SAFE / AGGRESSIVE) or build a custom `SvgPlugin[]` pipeline.
3. If using `mangleIds` across multiple unrelated SVGs, instantiate via `createMangleIds()` instead of the shared export.
4. Validate inputs are non-empty strings; `parseSvg` throws `SVG_COMPRESS_CODES.PARSE_FAILED` on empty or malformed XML.
5. Check that `CompressResult.ratio` is computed as `compressedSize / originalSize` (not a percentage).
6. If adding a custom plugin, ensure it returns `null` to prune nodes and does not mutate the input node in place.
7. Run `bunx turbo lint test typecheck build --filter=@suaveplan/compress-svg` before declaring changes complete.

View File

@@ -0,0 +1,176 @@
---
name: sp-config
description: Layered, universal configuration manager. Merges defaults, files, env vars, CLI args, and programmatic objects into an immutable, validated, deep-frozen store with secrets masking.
---
# Config for Suaveplan
Zero-dependency layered configuration system. `createConfig` resolves config layers in priority order, applies environment-specific overrides, validates, and returns an immutable `ConfigStore` with dot-path access and optional secrets masking.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `createConfig` | Main factory — resolves layers, sorts by priority, deep-merges, applies env overrides, validates, and freezes into a `ConfigStore` |
| `createConfigStore` | Lower-level factory — wraps already-merged data in an immutable, deep-frozen store with `.get`, `.has`, `.toJSON`, `.toMasked` |
| `defaultsLayer` | Priority 0 layer — base values with lowest precedence |
| `fileLayer` | Priority 10 layer — accepts pre-parsed file content (consumer reads/parses JSON/YAML/TOML) |
| `objectLayer` | Priority 20 layer — programmatic overrides with optional custom priority |
| `envLayer` | Priority 30 layer — maps env vars to dot-path keys with automatic string coercion |
| `cliLayer` | Priority 40 layer — parses `--key value`, `--key=value`, and boolean flags with coercion |
| `deepMerge` | Deep-merge utility — arrays replaced, `undefined` skipped, `null` overwrites |
| `coerceValue` | String coercion — `"true"` → `true`, `"0"` → `false`, numeric strings → `number`, JSON → parsed object |
| `maskSensitive` | Clone-and-mask sensitive dot-paths (default mask `"****"`) |
## File Structure
```
foundation/core/config/
src/core/config-builder/config-builder.ts # createConfig(), layer resolution, validation wrapping
src/core/config-store/config-store.ts # createConfigStore(), .get, .has, .toJSON, .toMasked
src/layers/defaults-layer/defaults-layer.ts # defaultsLayer() — priority 0
src/layers/file-layer/file-layer.ts # fileLayer() — priority 10, accepts parsed content
src/layers/object-layer/object-layer.ts # objectLayer() — priority 20, custom priority supported
src/layers/env-layer/env-layer.ts # envLayer() + coerceValue()
src/layers/cli-layer/cli-layer.ts # cliLayer() + parseArgs()
src/utils/merge.ts # deepMerge() + mergeLayers()
src/utils/mask.ts # getByPath, hasByPath, setByPath, maskSensitive
src/error-codes.ts # CONFIG_MISSING, CONFIG_INVALID_TYPE, CONFIG_PARSE_FAILED, etc.
```
Sibling packages:
- `@suaveplan/env` — Field-level env var schema validation with 9 built-in types. Lower-level and schema-driven; complements `config`.
- `@suaveplan/validation` — Often used in the `validate` option for schema validation.
- `@suaveplan/utils` — Provides the underlying `deepMerge` implementation.
## Patterns
### Quick Start
```typescript
import { createConfig, defaultsLayer, envLayer } from "@suaveplan/config";
const config = createConfig({
layers: [
defaultsLayer({ port: 3000, host: "localhost" }),
envLayer({ source: process.env, mapping: { PORT: "port", HOST: "host" } }),
],
});
config.get("port"); // 3000 or overridden by PORT env var
config.get("host"); // "localhost" or overridden by HOST env var
```
### Full Layer Stack (CLI Wins)
```typescript
import {
createConfig,
defaultsLayer,
fileLayer,
objectLayer,
envLayer,
cliLayer,
} from "@suaveplan/config";
const config = createConfig({
layers: [
defaultsLayer({ port: 3000, debug: false }),
fileLayer({ content: { port: 8080 } }),
objectLayer({ debug: true }),
envLayer({ source: { PORT: "9090" }, mapping: { PORT: "port" } }),
cliLayer({ args: ["--port", "4000"], mapping: { "--port": "port" } }),
],
});
config.get("port"); // 4000 (CLI priority 40 wins)
```
### Environment-Specific Overrides
```typescript
const config = createConfig({
layers: [defaultsLayer({ debug: false, logLevel: "info" })],
environments: {
development: { debug: true, logLevel: "debug" },
production: { host: "0.0.0.0" },
},
environment: "development",
});
config.get("debug"); // true
config.get("logLevel"); // "debug"
```
### Validation
```typescript
const config = createConfig({
layers: [defaultsLayer({ port: -1 })],
validate: (data) => {
if ((data.port as number) < 0) throw new Error("Port must be positive");
return data;
},
});
// Throws ConfigError with code "CONFIG_INVALID"
```
### Secrets Masking
```typescript
const config = createConfig({
layers: [defaultsLayer({ database: { url: "postgres://..." } })],
sensitive: ["database.url"],
});
config.toJSON(); // { database: { url: "postgres://..." } }
config.toMasked(); // { database: { url: "****" } }
```
### Direct ConfigStore Usage
```typescript
import { createConfigStore } from "@suaveplan/config";
const store = createConfigStore({
data: { port: 3000, database: { url: "postgres://..." } },
sensitive: ["database.url"],
});
store.get("database.url"); // "postgres://..."
store.has("debug"); // false
store.toMasked(); // masks sensitive paths
```
### CLI Parsing
```typescript
import { cliLayer, parseArgs } from "@suaveplan/config";
parseArgs(
["--port=8080", "--debug", "--host", "example.com"],
{ "--port": "port", "--debug": "debug", "--host": "host" }
);
// → { "--port": "8080", "--debug": "true", "--host": "example.com" }
```
## Constraints
- **Zero `fs` dependency** — The library never reads files. The consumer must read/parse JSON/YAML/TOML and pass the parsed object to `fileLayer({ content: parsed })`.
- **Arrays are replaced, not concatenated** during deep merge.
- **`undefined` values in override are skipped** — they do not erase base values.
- **`null` values in override do replace** base values.
- **Env var coercion is automatic but aggressive** — `"0"` coerces to `false`, not `0`. If you need numeric `0`, handle it manually or use a validator.
- **Dot-path getters return `undefined` for missing paths** or when traversing through non-objects / `null`.
- **`hasByPath` returns `true` for `null`, `false`, `0`, and `""`** — it only returns `false` when the value is strictly `undefined`.
- **Sensitive masking only masks paths that exist.** Nonexistent paths are silently ignored.
- **`CONFIG_INVALID` is thrown but not registered** in the local metadata map. The registered codes (`CONFIG_MISSING`, `CONFIG_INVALID_TYPE`, etc.) are used via `ConfigError` static factories.
## Checklist
1. Never pass file paths to `config` — read and parse externally, then use `fileLayer({ content: parsed })`.
2. Use `envLayer` mapping to bridge env var names to config dot-paths (e.g., `DATABASE_URL: "database.url"`).
3. Remember `"0"` coerces to `false` in `envLayer` and `cliLayer` — use a validator if numeric `0` is required.
4. Use `sensitive` paths in `createConfig` or `createConfigStore` to enable `.toMasked()` for logs.
5. Use `environment` + `environments` for per-stage overrides (development, production, etc.).
6. Provide a `validate` function for schema validation — throw on invalid data.

View File

@@ -0,0 +1,56 @@
---
name: sp-contacts
description: Universal contact and group management with hierarchical groups, audience resolution, contact merger, and CSV/JSON import-export.
---
| Class / Function | Responsibility |
| --- | --- |
| `ContactManager` | CRUD for channel-agnostic contacts with Zod validation and OTel counters |
| `GroupManager` | Hierarchical n-level group management via adjacency + materialized path |
| `MembershipManager` | Many-to-many contact-group memberships with ancestor count propagation |
| `AudienceResolver` | Flattens group sets into deduplicated, filtered contact lists via descendant walk |
| `ContactMerger` | Consolidates duplicate contacts into a primary record, merging channels and metadata |
| `ContactImporter` / `ContactExporter` | Ingests from CSV/JSON and serializes to CSV/JSON with RFC 4180 compliance |
| `InMemoryContactStore` / `InMemoryGroupStore` / `InMemoryMembershipStore` | Pluggable reference storage with `Clock` + `IdGenerator` injection |
| `buildGroupTree`, `flattenGroupTree`, `collectAllGroupIds` | Pure tree utilities for traversing `GroupTreeNode<TMeta>` hierarchies |
| `CONTACTS_CODES` / `ContactsError` | Stable `CONTACTS.*` error code registry with typed error hierarchy |
**File Structure:** `packages/commerce/contacts/contacts/src/{core/{contact-manager,contact-merger,group-manager,membership-manager,audience-resolver},stores/{contact-store,group-store,membership-store},import-export/{contact-importer,contact-exporter},tree,error-codes,otel,schemas,types}/`
**Sibling packages:**
- `@suaveplan/audience-rules` — rule-based audience segmentation that consumes `ContactData`
- `@suaveplan/macos-contacts` — macOS Contacts.app bridge for importing platform contacts
- `@suaveplan/error` — `defineCodes` and `SuaveplanError` base
- `@suaveplan/opentelemetry` — OTel counters and histograms
- `@suaveplan/types` — `Clock` + `IdGenerator` injectables
**Patterns**
Quick Start:
```ts
import { ContactManager, GroupManager, MembershipManager, AudienceResolver, InMemoryContactStore, InMemoryGroupStore, InMemoryMembershipStore } from "@suaveplan/contacts";
const contacts = new ContactManager({ store: new InMemoryContactStore() });
const groups = new GroupManager({ store: new InMemoryGroupStore() });
const memberships = new MembershipManager({ contactStore, groupStore, membershipStore: new InMemoryMembershipStore() });
const vip = await groups.create({ tenantId: "acme", name: "VIP" });
const alice = await contacts.create({ tenantId: "acme", displayName: "Alice", channels: [{ type: "email", value: "alice@example.com", primary: true, verified: true }] });
await memberships.addContactToGroup(vip.id, alice.id);
const audience = await new AudienceResolver({ contactStore, groupStore, membershipStore }).resolve({ groupIds: [vip.id] });
```
- **Subpath imports:** Deep imports like `@suaveplan/contacts/core/contact-manager/contact-manager` are available via `package.json` exports for tree-shaking.
- **Storage backends:** Implement `ContactStore`, `GroupStore`, and `MembershipStore` interfaces and pass them into managers; `InMemory*` stores are the reference.
- **Import/Export pipelines:** `ContactImporter.ingest(records, fieldMapping)` parses arrays; `ContactExporter.export(contacts, { format: "csv" })` emits RFC 4180 CSV.
- **Error handling:** Every public boundary throws `ContactsError` with a `CONTACTS.*` code. Codes: `INVALID_INPUT`, `CONTACT_NOT_FOUND`, `GROUP_NOT_FOUND`, `MEMBERSHIP_NOT_FOUND`, `DUPLICATE_CONTACT`, `DUPLICATE_GROUP`, `CIRCULAR_REFERENCE`, `IMPORT_FAILED`, `EXPORT_FAILED`.
**Constraints**
- Multi-tenant via `tenantId` on every entity and query.
- All temporal and id-generating paths accept injectable `Clock` and `IdGenerator`; no `Date.now()` / `Math.random()` in production code.
- Every public mutating method validates inputs with Zod and emits at least one OTel instrument.
- Six OTel counters and three histograms are wired through `@suaveplan/opentelemetry`.
**Checklist**
- [ ] Read `src/error-codes/error-codes.md` for the complete `CONTACTS_CODES` registry
- [ ] Read `src/stores/*/README.md` for store interface contracts
- [ ] Read `src/core/*/README.md` for manager class behavior and test expectations
- [ ] Verify subpath exports in `package.json` match the source tree

View File

@@ -0,0 +1,61 @@
---
name: sp-control-surface-schema
description: Canonical schema for DAW-style control surfaces — 18 ControlKind variants, 6 Binding variants, 4 GroupLayout variants, Zod validators, migrations, DFS traversal, cross-checks.
---
# sp-control-surface-schema
| Symbol | Kind | Responsibility |
|---|---|---|
| `ControlSurfaceSchema` | Schema | Root Zod schema for a complete control surface definition |
| `ControlNodeSchema` | Schema | Discriminated union: `Group` or `Control` nodes |
| `ControlKindSchema` | Schema | 18 control kind variants (fader, knob, button, pad, encoder, etc.) |
| `BindingSchema` | Schema | 6 binding variants (MIDI in/out, plugin param, transport, DMX, compound) |
| `GroupLayoutSchema` | Schema | 4 layout variants (absolute, grid, stack, free3d) |
| `validateSurface` | Function | Full validation with duplicate-id and binding-reference cross-checks |
| `upgradeSurface` | Function | Forward-migrates a surface through registered `Migration` steps |
| `MigrationRegistry` | Class | Registry for schema version migrations |
| `walkSurface` | Function | DFS traversal with a `SurfaceVisitor` callback |
| `assertUniqueIds` / `assertBindingReferences` | Function | Identity and reference integrity checks |
| `CONTROL_SURFACE_SCHEMA_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/media/audio/control-surface-schema/src/{schemas,types,validate,upgrade,walk,identity,constants,error-codes}/`
**Sibling packages:**
- `@suaveplan/audio-controllers` — MIDI controller plugins (binding target)
- `@suaveplan/react-control-surface` — React renderer for control surfaces
**Patterns:**
*Quick Start:*
```ts
import { ControlSurfaceSchema, validateSurface } from "@suaveplan/control-surface-schema";
const surface = ControlSurfaceSchema.parse(rawJson);
const result = validateSurface(surface); // { ok: true } or { ok: false, errors }
```
*Migration:*
```ts
import { upgradeSurface, MigrationRegistry } from "@suaveplan/control-surface-schema/upgrade";
const upgraded = upgradeSurface(surface, { targetVersion: 2 });
```
*DFS traversal:*
```ts
import { walkSurface } from "@suaveplan/control-surface-schema/walk";
walkSurface(surface, (node, depth) => console.log(node.id, depth));
```
**Constraints:**
- Zero runtime deps beyond `@suaveplan/error`, `@suaveplan/types`, `tslib`, `zod`
- Subpath exports: `/schemas/control-surface-schema`, `/validate/validate`, `/upgrade/upgrade`, `/walk/walk`, `/identity/identity`, `/constants`, `/error-codes`
- `CURRENT_SCHEMA_VERSION` tracks the latest schema version
- `ControlKind` values exported as `CONTROL_KIND_VALUES` array
- All schemas support both `.parse()` and `.safeParse()`
**Checklist:**
- [ ] `ControlSurfaceSchema` validates a full surface definition
- [ ] `validateSurface` catches duplicate IDs and dangling binding references
- [ ] `upgradeSurface` applies migrations in order from source to target version
- [ ] `walkSurface` visits every node in DFS order
- [ ] All 18 control kinds, 6 bindings, and 4 layouts are schema-validated

View File

@@ -0,0 +1,120 @@
---
name: sp-controller
description: Universal input controller foundation with 37-field ControllerInput, BaseController abstract base, device adapters, merger, and typed event emitter.
---
# Controller for Suaveplan
Universal input controller foundation. Ships the unified `ControllerInput` shape (37 normalised fields in `[0, 255]`), `BaseController` abstract base with injectable `Clock`/`IdGenerator`, `pollRate` gating and per-input threshold/deadzone normalisation, the typed `InputEventEmitter`, device adapters (`KeyboardController`, `GamepadController`), and `ControllerMerger`.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `BaseController` | Abstract base owning 37-field `ControllerInput` state, per-input gating, typed `InputEventEmitter`, and `pollRate`-gated `update()` cadence. |
| `KeyboardController` | Browser-tier adapter normalising DOM `keydown`/`keyup` into `ControllerInput`. |
| `GamepadController` | Browser-tier adapter polling the Web Gamepad API and normalising button + axis state. |
| `ControllerMerger` | Fuses an ordered list of `BaseController` children into a single virtual controller. |
| `InputEventEmitter` | Typed event dispatcher for `input`, `state-change`, and `combo` events with disposable listeners. |
| `CONTROLLER_CODES` + `ControllerError` | Stable error registry via `defineCodes("CONTROLLER", [...])`. |
## File Structure
```
gaming/games/controller/
src/base/ # BaseController abstract base
src/keyboard/ # KeyboardController
src/gamepad/ # GamepadController
src/merger/ # ControllerMerger
src/events/ # InputEventEmitter
src/telemetry/ # OTel counters and histograms
src/types/ # ControllerInput, keys, vectors
src/errors/ # CONTROLLER_CODES, ControllerError
```
Sibling packages:
- `@suaveplan/events` — typed event emitter patterns.
- `@suaveplan/error` — provides `SuaveplanError` base and `defineCodes` registry.
- `@suaveplan/opentelemetry` — OTel instrumentation helpers.
## Patterns
### Quick Start
```typescript
import { BaseController } from "@suaveplan/controller";
class FakeController extends BaseController {
protected override updateState(): void {
// pump device-specific signals into setState()
}
}
const c = new FakeController({ device: "fake", pollRate: 30 });
c.onInput("up", (input, value) => console.log(input, value));
c.setState({ up: 200 });
console.log(c.getValue("up")); // 200
c.destroy();
```
### Keyboard Adapter
```typescript
import { KeyboardController } from "@suaveplan/controller";
const kb = new KeyboardController({
eventTarget: window,
pollRate: 60,
});
kb.onInput("up", (input, value) => {
if (value > 0) console.log("jump!");
});
kb.update();
```
### Gamepad Adapter
```typescript
import { GamepadController } from "@suaveplan/controller";
const gp = new GamepadController({
gamepadSource: () => navigator.getGamepads()[0],
pollRate: 60,
});
gp.onInput("lx", (input, value) => console.log("stick x:", value));
gp.update();
```
### Merging Multiple Controllers
```typescript
import { ControllerMerger, KeyboardController, GamepadController } from "@suaveplan/controller";
const merger = new ControllerMerger([
new KeyboardController({ eventTarget: window }),
new GamepadController({ gamepadSource: () => navigator.getGamepads()[0] }),
]);
merger.onInput("up", (input, value) => console.log("merged up:", value));
merger.update();
```
## Constraints
- `BaseController` is abstract — must be extended by a device adapter or `ControllerMerger`.
- `pollRate` gating coalesces repeated `update()` calls within `1000 / pollRate` ms.
- Per-input values are clamped to `[0, 255]`, rounded to integers, with deadzone and threshold normalisation.
- `input` and `state-change` events fire only when the post-normalisation value differs from the previous value.
- Strict mode (`strict: true`) makes post-`destroy()` `setState` throw `ControllerError(DESTROYED_INSTANCE)`.
- Injectable `Clock` and `IdGenerator` for deterministic testing.
## Checklist
1. Extend `BaseController` or use a built-in adapter (`KeyboardController`, `GamepadController`) for device input.
2. Call `update()` in your game loop or animation frame; the pollRate gate prevents redundant work.
3. Use `ControllerMerger` when you want multiple physical devices to control the same logical player.
4. Set `strict: true` in production to catch use-after-destroy bugs early.
5. Inject a `FakeClock` in tests to control timing without touching globals.

View File

@@ -0,0 +1,152 @@
---
name: sp-crdt
description: Composable CRDT primitives (counter, set, register, array) with vector-clock causality and a sync manager for distributed state convergence.
---
# CRDTs for Suaveplan
Conflict-free Replicated Data Types for distributed systems. Provides eight composable CRDT primitives—grow-only and positive-negative counters, observe-remove and two-phase sets, last-write-wins and multi-value registers, and a replicated growable array—plus a vector-clock for causality tracking and a sync manager that coordinates delta exchange across heterogeneous replicas.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `GCounter` | Grow-only counter; merge takes the element-wise maximum of per-node counters |
| `PNCounter` | Bidirectional counter composed of two internal G-Counters (increments minus decrements) |
| `ORSet<T>` | Observe-Remove Set; each add generates a unique tag so concurrent adds survive removes |
| `TwoPSet<T>` | Two-Phase Set; elements can be added and removed, but removed elements can never be re-added |
| `LWWRegister<T>` | Last-Write-Wins Register; highest timestamp wins, ties broken by lexicographic nodeId |
| `MVRegister<T>` | Multi-Value Register; preserves all concurrent writes using vector-clock dominance |
| `RGA<T>` | Replicated Growable Array; ordered list with causal insert/delete and deterministic (seq, nodeId) tie-breaking |
| `VectorClock` | Per-node monotonic counters with `compare` returning before/after/equal/concurrent |
| `CrdtSyncManager` | Registry of CRDTs; `tick()` advances local clock, `exportDeltas()` / `sync()` exchange state |
| `CrdtError` | Typed error class using `CRDT_CODES` (MERGE_CONFLICT, INVALID_STATE, DESERIALIZE_FAILED, OPERATION_INVALID) |
## File Structure
```
foundation/flow/crdt/
src/crdts/g-counter/ # Grow-only counter
src/crdts/pn-counter/ # Positive-negative counter
src/crdts/or-set/ # Observe-remove set
src/crdts/two-p-set/ # Two-phase set
src/crdts/lww-register/ # Last-write-wins register
src/crdts/mv-register/ # Multi-value register
src/crdts/rga/ # Replicated growable array
src/core/vector-clock/ # Causality tracking
src/core/sync-manager/ # Delta-exchange coordinator
src/errors/ # CrdtError and CRDT_CODES
src/types/ # Shared CRDT interfaces and state shapes
src/constants/ # DEFAULT_NODE_ID, DEFAULT_CRDT_ID_PREFIX
```
Sibling packages:
- `@suaveplan/fsm` — typed finite state machines with guards, actions, and history tracking
- `@suaveplan/queue` — enterprise-grade queue management with workers, retry, and dead-letter
- `@suaveplan/workflow` — workflow/pipeline orchestration with saga compensation and retry
## Patterns
### Quick Start
```typescript
import { GCounter, CrdtSyncManager } from "@suaveplan/crdt";
const counter = new GCounter("my-counter");
counter.increment("node-1", 3);
counter.increment("node-2", 5);
console.log(counter.total()); // 8
const mgr = new CrdtSyncManager({ nodeId: "node-1" });
mgr.register(counter);
mgr.tick();
const deltas = mgr.exportDeltas(); // send over network
```
### Bidirectional Sync Between Replicas
```typescript
import { GCounter, CrdtSyncManager } from "@suaveplan/crdt";
const a = new CrdtSyncManager({ nodeId: "a" });
const b = new CrdtSyncManager({ nodeId: "b" });
const counterA = new GCounter("shared");
const counterB = new GCounter("shared");
a.register(counterA);
b.register(counterB);
counterA.increment("a", 3);
counterB.increment("b", 4);
a.tick();
b.tick();
const { local, remote } = a.syncWith(b);
console.log(counterA.total()); // 7
console.log(counterB.total()); // 7
```
### Concurrent Add/Remove with OR-Set
```typescript
import { ORSet } from "@suaveplan/crdt";
const a = new ORSet<string>();
a.add("x", "n1");
const b = a.clone();
a.remove("x");
b.add("x", "n2");
a.merge(b.state());
console.log(a.has("x")); // true — concurrent add survives remove
```
### Multi-Value Register Conflict Detection
```typescript
import { MVRegister } from "@suaveplan/crdt";
const a = new MVRegister<number>("n1");
a.set(1);
const b = new MVRegister<number>("n2");
b.set(2);
a.merge(b.state());
console.log(a.conflict()); // true
console.log(a.get()); // [1, 2]
a.set(3); // sequential overwrite resolves conflict
console.log(a.conflict()); // false
console.log(a.get()); // [3]
```
### RGA Ordered Insertions
```typescript
import { RGA } from "@suaveplan/crdt";
const r = new RGA<string>("node-a");
const idA = r.insert("a");
const idB = r.insert("b", idA);
console.log(r.value()); // ["a", "b"]
r.remove(idA);
console.log(r.value()); // ["b"]
console.log(r.indexOf(idB)); // 0
```
## Constraints
- All CRDT constructors generate a random UUID id via `crypto.randomUUID()` when no id is provided; LWWRegister and MVRegister additionally require a `nodeId` argument.
- `GCounter.increment` and `PNCounter.increment`/`decrement` throw `CrdtError(CRDT_CODES.OPERATION_INVALID)` for non-positive deltas.
- `TwoPSet.add` throws `CrdtError(CRDT_CODES.OPERATION_INVALID)` if the element was previously removed.
- `fromJSON` on every CRDT throws `CrdtError(CRDT_CODES.DESERIALIZE_FAILED)` for invalid JSON or a mismatched `type` field.
- `RGA.insert` returns a globally unique id of the form `<seq>@<nodeId>`; concurrent inserts at the same predecessor are ordered by higher `seq` first, then lexicographically larger `nodeId`.
- `CrdtSyncManager.sync` skips deltas for unknown CRDT ids and returns them in the `skipped` count.
- Every CRDT implements the `CRDT<TValue, TState>` interface: `id`, `value()`, `merge(remoteState)`, `state()`, `toJSON()`, `fromJSON(json)`, `clone()`.
## Checklist
1. Verify the required CRDT is available in the exports map (root, `crdts/<name>`, `core/vector-clock`, `core/sync-manager`, `errors/crdt-error`).
2. Ensure `nodeId` is passed to `LWWRegister`, `MVRegister`, and `RGA` constructors; omitting it causes runtime issues or incorrect tie-breaking.
3. After local mutations, call `CrdtSyncManager.tick()` before `exportDeltas()` so the vector clock advances.
4. When merging remote state, use the typed `state()` output of the source CRDT; do not hand-craft state objects unless you preserve the exact `type` discriminator.
5. Check for `CrdtError` with `CRDT_CODES` constants; never assert against raw string literals.

View File

@@ -0,0 +1,50 @@
---
name: sp-create-suaveplan-plugin
description: Interactive scaffold for new Suaveplan API plugins — generates plugin.json, definePlugin body, tests, vitest configs, migrations, and README.
---
# sp-create-suaveplan-plugin
| Symbol | Kind | Responsibility |
|---|---|---|
| `scaffold` | Function | Interactive CLI that prompts for isolation tier, capabilities, DB engine, then generates the full plugin skeleton |
| `generate` | Function | Programmatic variant of scaffold; returns an in-memory list of `GeneratedFile` records |
| `PromptAnswersSchema` / `validateAnswers` | Function | Zod schema + validator for scaffold inputs (`IsolationTier`, `CapabilitySelection`, `DatabaseEngine`) |
| `answersForFixture` | Function | Pre-canned prompt answers for test fixtures |
| `renderTree` | Function | Renders the scaffolded file tree as a visual tree string |
| `SCAFFOLD_CODES` | Constant | Error-code registry via `@suaveplan/error` `defineCodes` |
**File Structure:** `packages/api-stack/api/create-suaveplan-plugin/src/{bin,generator,prompt,templates}/`
**Sibling packages:**
- `@suaveplan/wasm-compiler-javy` — WASM compiler for compiled plugins
- `@suaveplan/testing-integration-postgres` — Postgres fixture for integration tests
- `@suaveplan/testing-integration-surrealdb` — SurrealDB fixture for integration tests
**Patterns:**
*Quick Start (CLI):*
```bash
npx create-suaveplan-plugin
# interactive prompts → generates complete plugin skeleton
```
*Programmatic generation:*
```ts
import { generate, validateAnswers } from "create-suaveplan-plugin/generator";
const answers = validateAnswers({ isolationTier: "sandboxed", capabilities: ["rest"], databaseEngine: "sqlite" });
const files = generate(answers);
```
**Constraints:**
- CLI entrypoint: `create-suaveplan-plugin` bin
- Subpath exports: `/error-codes`, `/prompt`, `/generator`
- Generates `plugin.json`, `definePlugin` body, host test, integration test, vitest configs, migrations, README
- Integration tests wired to matching `testing-integration-{postgres,sqlite,surrealdb}` fixture
- No `@suaveplan/*` runtime dependencies other than `@suaveplan/error`
**Checklist:**
- [ ] `scaffold` generates a testable plugin skeleton from interactive prompts
- [ ] `generate` returns in-memory `GeneratedFile` records without writing to disk
- [ ] `validateAnswers` rejects invalid isolation tiers or capability combinations
- [ ] Output includes `plugin.json`, host test, and integration test

View File

@@ -0,0 +1,192 @@
---
name: sp-cron
description: Universal cron expression parser, next-fire calculator with timezone + DST awareness, and lightweight in-process scheduler.
---
# Cron for Suaveplan
Universal cron expression parser with timezone and DST-aware next-run calculation, plus an opt-in lightweight in-process scheduler. Zero external cron dependencies — written natively against `@suaveplan/error`, `@suaveplan/types`, and `Intl.DateTimeFormat`.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `parseCron` | Parse 5/6/7-field expressions into a frozen `CronExpression` with `nextRun`, `nextRuns`, `previousRun`, and `matches` methods. |
| `isValidCron` | Non-throwing parse check; catches every thrown value and returns `boolean`. |
| `parseField` | Tokenize a single cron field into a sorted, unique, frozen array of values; rejects unsupported Quartz tokens (`L`, `W`, `#`, `?`). |
| `expandMacro` / `macroTable` | Expand `@yearly`, `@monthly`, `@daily`, `@hourly`, etc. to canonical 5-field strings. |
| `computeNextRun` / `computePreviousRun` | Forward/backward search loops using wall-clock climbing with an 8-year horizon. |
| `toWallClock` / `fromWallClock` | Bidirectional epoch ↔ wall-clock conversion via cached `Intl.DateTimeFormat`; two-pass offset correction. |
| `detectDstTransition` | Classify DST gaps/overlaps between two adjacent instants as `spring-forward` or `fall-back`. |
| `CronScheduler` | Lightweight in-process driver using one `setTimeout` per job; no queue, retry, or concurrency control. |
| `CronError` / `CRON_CODES` | Typed error class via `defineCodes("CRON", [...])`; runtime guard rejects unregistered codes. |
| `recordParse` / `recordNextRun` / `recordDstTransition` | OpenTelemetry histogram + counter instrumentation for parse duration, next-run duration, and DST crossings. |
## File Structure
```
foundation/timing/cron/
src/parse/parse-cron.ts # parseCron, isValidCron, CronExpression interface
src/fields/cron-fields.ts # parseField, rangeFor — single-field tokenizer
src/macros/macros.ts # expandMacro, macroTable
src/next-run/next-run.ts # computeNextRun, computePreviousRun
src/next-run/next-run-fields.ts # Low-level field-walking helpers (climbFields, advanceUntilDayMatches)
src/timezone/timezone.ts # toWallClock, fromWallClock, detectDstTransition, assertTimezone
src/scheduler/cron-scheduler.ts # CronScheduler, createCronScheduler
src/errors/cron-error.ts # CronError, CRON_CODES, registerCronErrorCodes
src/otel/cron-otel.ts # recordParse, recordNextRun, recordDstTransition
src/index.ts # Barrel re-export
```
Sibling packages:
- `@suaveplan/datetime` — Calendar math, business-day arithmetic, and RRULE recurrence; use for scheduling UIs and invoice date logic.
- `@suaveplan/timing` — Stopwatch, throttle/debounce, task scheduler with concurrency, vector clocks; use for elapsed-time measurement and rate control.
## Patterns
### Quick Start
```typescript
import { parseCron, isValidCron } from "@suaveplan/cron";
const expr = parseCron("*/15 * * * *");
console.log(expr.fields.minutes); // [0, 15, 30, 45]
const next = expr.nextRun(Date.UTC(2026, 4, 11, 10, 7, 0), "America/New_York");
console.log(new Date(next as number).toISOString());
console.log(isValidCron("0 9 * * MON-FRI")); // true
console.log(isValidCron("garbage")); // false
```
### Opt-In Seconds and Quartz Year Fields
```typescript
import { parseCron } from "@suaveplan/cron";
const withSeconds = parseCron("0 */5 * * * *", { seconds: true });
console.log(withSeconds.fields.seconds); // [0]
const withYear = parseCron("0 0 9 * * * 2026-2028", {
seconds: true,
years: true,
});
console.log(withYear.fields.years); // [2026, 2027, 2028]
```
### Scheduler with Timezone
```typescript
import { CronScheduler } from "@suaveplan/cron";
const scheduler = new CronScheduler({
onError: (err, id) => console.error(`job ${id} failed`, err),
});
scheduler.add("0 9 * * MON-FRI", async () => sendDailyDigest(), {
tz: "America/New_York",
});
scheduler.add("@hourly", () => collectMetrics());
scheduler.start();
process.on("SIGTERM", async () => {
await scheduler.stop();
process.exit(0);
});
```
### Deterministic Testing with Fake Timers
```typescript
import { CronScheduler } from "@suaveplan/cron";
import type { SchedulerTimers } from "@suaveplan/cron";
const now = { value: Date.UTC(2026, 4, 11, 9, 59, 0) };
const pending = new Map<number, { cb: () => void; fireAt: number }>();
let nextId = 1;
const timers: SchedulerTimers = {
setTimer: (cb, ms) => {
const id = nextId++;
pending.set(id, { cb, fireAt: now.value + ms });
return id;
},
clearTimer: (id) => pending.delete(id),
};
const scheduler = new CronScheduler({ clock: () => now.value, timers });
scheduler.add("0 * * * *", () => console.log("fired"));
scheduler.start();
// Fast-forward 1 hour.
now.value += 60 * 60 * 1000;
for (const [id, { cb, fireAt }] of pending) {
if (fireAt <= now.value) { pending.delete(id); cb(); }
}
await scheduler.stop();
```
### Wall-Clock and DST Utilities
```typescript
import { toWallClock, fromWallClock, detectDstTransition, assertTimezone } from "@suaveplan/cron";
// Decompose epoch to wall-clock components in any IANA zone
const wall = toWallClock(Date.UTC(2026, 0, 1, 5, 0, 0), "America/New_York");
console.log(wall); // { year: 2026, month: 1, day: 1, hour: 0, minute: 0, second: 0, weekday: 4 }
// Convert wall-clock back to epoch (two-pass offset correction)
const epoch = fromWallClock(
{ year: 2026, month: 1, day: 1, hour: 0, minute: 0, second: 0 },
"America/New_York"
);
// Classify DST transition between adjacent instants
const before = Date.UTC(2026, 2, 8, 6, 0, 0); // 01:00 EST
const after = Date.UTC(2026, 2, 8, 8, 0, 0); // 04:00 EDT
const kind = detectDstTransition(before, after, "America/New_York");
console.log(kind); // "spring-forward"
// Eager timezone validation at config boundary
assertTimezone("Europe/London"); // ok
assertTimezone("Mars/Olympus_Mons"); // throws CronError(CRON.TIMEZONE_INVALID)
```
### Typed Error Handling
```typescript
import { CRON_CODES, CronError } from "@suaveplan/cron";
try {
parseCron("0 0 31 2 *").nextRun(Date.UTC(2026, 0, 1), "UTC");
} catch (err) {
if (err instanceof CronError && err.code === CRON_CODES.NEXT_RUN_NOT_FOUND) {
console.log("Expression has no valid fire times within the horizon");
}
}
```
## Constraints
- **Search horizon is 8 years** — `nextRun` and `previousRun` throw `CRON.NEXT_RUN_NOT_FOUND` when no match exists within 8 years of the anchor instant.
- **`nextRun` is strictly greater than `after`**; `previousRun` is strictly less than `before` — equality never matches.
- **Vixie OR semantics for day-of-month + day-of-week** — When both are restricted, either match is sufficient; when one is `*`, the other must match.
- **`strictDoW: true` rejects both dom and dow being non-wildcards** — Enforced at parse time, not runtime.
- **Unsupported Quartz tokens throw `CRON.UNSUPPORTED_SYNTAX`** — `L`, `W`, `#`, `?` are explicitly rejected.
- **Day-of-week accepts 0 and 7 for Sunday** — Both normalize to 0 internally; deduplicated in output.
- **`@reboot` macro is explicitly unsupported** — Throws `CRON.UNSUPPORTED_SYNTAX`.
- **CronScheduler has no queue, retry, or concurrency control** — It is a single-timer-per-job driver; use a workflow-tier scheduler for queue/retry/concurrency.
- **CronScheduler `stop()` awaits in-flight handlers** but does not abort them — clears pending timers and resolves after all active promises settle.
- **All errors use `CronError` with `defineCodes("CRON", ...)`** — Never construct raw `Error` or hand-type `"CRON.XXX"` strings.
## Checklist
1. Use `parseCron` for expression parsing; use `isValidCron` at API boundaries for non-throwing validation.
2. Enable `{ seconds: true }` or `{ years: true }` explicitly when parsing 6-field or 7-field expressions.
3. Use `strictDoW: true` when your application requires exactly one of day-of-month or day-of-week to be a wildcard.
4. Inject `{ clock, timers }` into `CronScheduler` for deterministic unit tests; do not use `vi.useFakeTimers()`.
5. Use `assertTimezone(tz)` at configuration load time to fail fast on invalid IANA names.
6. Assert against `CRON_CODES.XXX` constants in tests — never hard-code `"CRON.XXX"` string literals.
7. Use `CronScheduler` only for lightweight in-process scheduling; defer to a workflow-tier scheduler for retry, concurrency, or durable queues.

View File

@@ -0,0 +1,112 @@
---
name: sp-crypto-bin
description: Password-hashing CLI binary (Argon2id / BLAKE2b hash + verify) with testable core and env-injected I/O.
---
# Password-Hashing CLI for Suaveplan
`@suaveplan/crypto-bin` ships the `suaveplan-hash-password` binary for Argon2id (default) and BLAKE2b password hashing and verification. It lives in a dedicated package so the universal `@suaveplan/crypto` façade does not pull CLI runtime dependencies into browser or server consumers that only need primitives.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `main(argv, env)` | Testable CLI core: parses args, dispatches to hash or verify, returns numeric exit code (never throws) |
| `parseArgs(argv)` | Hand-rolled argv parser (~40 lines); validates flags and values, throws `HashPasswordCliError` on bad input |
| `detectAlgo(storedHash)` | Sniffs `argon2id` vs `blake2b` from the stored hash's `$prefix$` (PHC/MCF format) |
| `HashPasswordCliEnv` | Output adapter interface with `write(s)` and `writeError(s)` so tests can capture output without subprocess spawn |
| `HashPasswordCliError` | `SuaveplanError` subclass wrapping three stable codes via `defineCodes("CRYPTO", [...])` |
| `src/bin/hash-password.ts` | Thin Node binary shim: wires `process.stdout/stderr` into a `HashPasswordCliEnv` adapter, then calls `main()` |
## File Structure
```
security-stack/crypto/crypto-bin/
src/bin/hash-password.ts # Binary entry shim (process.* I/O + auto-exec guard)
src/hash-password/
hash-password.ts # Testable core: main, parseArgs, detectAlgo, error classes
hash-password.test.ts # Unit tests covering hash mode, verify mode, arg validation
index.ts # Module re-exports
src/index.ts # Package public exports
```
Sibling packages:
- `@suaveplan/crypto` — universal crypto façade; this bin is kept separate so it doesn't pull CLI deps into universal consumers
- `@suaveplan/crypto-hash` — provides `hashPasswordArgon2id`, `verifyPasswordArgon2id`, `hashPasswordBlake2b`, `verifyPasswordBlake2b` primitives
- `@suaveplan/crypto-cli` — general crypto CLI utilities (distinct from this single-purpose binary)
## Patterns
### Quick Start — Programmatic Invocation
```typescript
import { main } from "@suaveplan/crypto-bin";
const out: string[] = [];
const err: string[] = [];
const code = main(
["--password", "pw", "--algo", "blake2b", "--cost-factor", "10"],
{
write: (s) => out.push(s),
writeError: (s) => err.push(s),
},
);
if (code === 0) {
storeHash(out.join("").trim());
}
```
### Verify Mode with Auto-Detected Algorithm
```typescript
import { main } from "@suaveplan/crypto-bin";
const { env, out, err } = (() => {
const o: string[] = [];
const e: string[] = [];
return {
env: { write: (s: string) => o.push(s), writeError: (s: string) => e.push(s) },
out: o,
err: e,
};
})();
const code = main(["--password", "pw", "--verify", "$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>"], env);
// code === 0 → match
// code === 1 → wrong password
// code === 3 → unknown algorithm prefix
// code === 4 → corrupt stored hash
```
### Pure Argv Parsing (Throws on Bad Input)
```typescript
import { parseArgs, HashPasswordCliError, HASH_PASSWORD_CLI_CODES } from "@suaveplan/crypto-bin";
try {
const args = parseArgs(["--password", "pw", "--algo", "blake2b", "--cost-factor", "10"]);
// args.password → "pw"
// args.algo → "blake2b"
// args.costFactor → 10
} catch (err) {
const e = err as HashPasswordCliError;
// e.code === HASH_PASSWORD_CLI_CODES.HASH_PASSWORD_CLI_BAD_ARGS
// e.code === HASH_PASSWORD_CLI_CODES.HASH_PASSWORD_CLI_UNKNOWN_ALGO
}
```
## Constraints
- `main()` never throws — every error path is captured and surfaced via `env.writeError(...)` plus a non-zero return value
- Hand-rolled argv parser (no `@suaveplan/cli` dependency) so the binary stays small; only supports `--password`, `--algo`, `--verify`, and `--cost-factor`
- `--cost-factor` is accepted only for `blake2b` and must be an integer in the range 1–31
- The binary shim uses an auto-exec guard (`process.argv[1]?.endsWith("hash-password.js")`) so importing the module does not trigger an unintended exit
- `detectAlgo` throws `HashPasswordCliError` with code `CRYPTO.HASH_PASSWORD_CLI_UNKNOWN_ALGO` for unrecognised prefixes (e.g. `$bcrypt$`)
## Checklist
1. Ensure `HashPasswordCliError` codes use the `defineCodes("CRYPTO", [...])` registry — never hand-write raw `"CRYPTO.HASH_PASSWORD_CLI_..."` string literals
2. When adding a new algorithm, update `detectAlgo` prefix sniffing, `parseArgs` `--algo` validation, and `main` dispatch logic
3. Preserve the exit-code contract in `main`: 0 = success/match, 1 = verify mismatch, 2 = bad args, 3 = unknown algo, 4 = verify failed (corrupt hash)
4. Keep the binary shim free of logic — all behaviour belongs in `main()` so it remains testable without subprocess spawn

View File

@@ -0,0 +1,145 @@
---
name: sp-crypto-browser
description: WebCrypto-backed engine implementations for @suaveplan/crypto — AES-256-GCM, Ed25519, ECDSA, X25519, ECDH, RSA, AES-KW, and non-extractable key support via crypto.subtle.
---
# WebCrypto Browser Engines for Suaveplan
WebCrypto-backed engine implementations for `@suaveplan/crypto` and `@suaveplan/crypto-core`. Provides zero-dependency `crypto.subtle` based AES-256-GCM, Ed25519, ECDSA-P256/P384, X25519, ECDH-P256/P384, RSA, AES Key Wrap, and non-extractable `CryptoKey` generation. All engines implement interfaces from `@suaveplan/crypto-core` so they plug into facade functions without changing call sites.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `webCryptoAeadEngine` | AES-256-GCM encrypt/decrypt with optional AAD via `crypto.subtle` |
| `webCryptoSignEngine` | Default Ed25519 sign/verify/keypair generation |
| `webCryptoEd25519Engine` | Ed25519 signing engine (32-byte raw public key, 32-byte seed secret key) |
| `webCryptoEcdsaP256Engine` | ECDSA-P256/SHA-256 signing engine (65-byte uncompressed public key) |
| `webCryptoAgreeEngine` | Default X25519 shared-secret derivation |
| `webCryptoX25519Engine` | X25519 key-agreement engine (32-byte raw keys) |
| `webCryptoEcdhP256Engine` | ECDH-P256 key-agreement engine (65-byte uncompressed public key) |
| `rsa` | RSA-PSS sign/verify, RSA-OAEP encrypt/decrypt, RSASSA-PKCS1-v1_5 (DER keys) |
| `aesKw` | AES Key Wrap (RFC 3394) wrap/unwrap operations |
| `ecdsaP384` | ECDSA-P384/SHA-384 sign/verify with 97-byte uncompressed public keys |
| `ecdhP384` | ECDH-P384 key agreement producing 48-byte shared secrets |
| `nonExtractableKey` | Generates `CryptoKey` handles with `extractable: false` |
| `isWebCryptoAvailable` | Runtime guard returning `true` when `crypto.subtle` is present |
| `assertWebCryptoAvailable` | Throws `BrowserCryptoError` if `crypto.subtle` is missing |
| `BrowserCryptoError` | Structured error class with stable `BROWSER_CRYPTO.*` codes |
## File Structure
```
security-stack/crypto/crypto-browser/
src/engines/ # WebCrypto engine implementations
webcrypto-aead-engine/ # AES-256-GCM AeadEngine
webcrypto-sign-engine/ # Ed25519 / ECDSA-P256 SignEngine
webcrypto-agree-engine/ # X25519 / ECDH-P256 AgreeEngine
src/rsa/ # RSA-PSS / RSA-OAEP / RSASSA-PKCS1-v1_5
src/aes-kw/ # AES Key Wrap (RFC 3394)
src/ecdsa-p384/ # ECDSA-P384 / SHA-384
src/ecdh-p384/ # ECDH-P384 key agreement
src/non-extractable-key/ # Non-extractable CryptoKey generation
src/availability/ # Runtime WebCrypto availability guards
src/errors/ # BrowserCryptoError and BROWSER_CRYPTO_CODES
src/constants/ # Key-length constants (AES_256_KEY_LENGTH, etc.)
```
Sibling packages:
- `@suaveplan/crypto-core` — defines engine interfaces (AeadEngine, SignEngine, AgreeEngine) and provides noble-backed default implementations
- `@suaveplan/crypto-sodium` — libsodium-backed engine implementations via WASM
- `@suaveplan/crypto` — opinionated facade/keychain layer that consumes engine implementations
## Patterns
### Quick Start
```typescript
import { webCryptoAeadEngine, isWebCryptoAvailable } from "@suaveplan/crypto-browser";
if (!isWebCryptoAvailable()) throw new Error("WebCrypto not available");
const key = crypto.getRandomValues(new Uint8Array(32));
const nonce = crypto.getRandomValues(new Uint8Array(12));
const ct = await (webCryptoAeadEngine.encrypt(key, new TextEncoder().encode("hello"), nonce) as unknown as Promise<Uint8Array>);
```
### Engine Plug-in with Facade
```typescript
import { encrypt, decrypt } from "@suaveplan/crypto/aead";
import { webCryptoAeadEngine } from "@suaveplan/crypto-browser/engines/webcrypto-aead-engine/webcrypto-aead-engine";
const ciphertext = await encrypt(key, plaintext, nonce, { engine: webCryptoAeadEngine });
```
### Ed25519 Signing
```typescript
import { webCryptoEd25519Engine } from "@suaveplan/crypto-browser/engines/webcrypto-sign-engine/webcrypto-sign-engine";
const kp = await (webCryptoEd25519Engine.generateKeypair() as unknown as Promise<{ publicKey: Uint8Array; secretKey: Uint8Array }>);
const message = new TextEncoder().encode("test message");
const signature = await (webCryptoEd25519Engine.sign(kp.secretKey, message) as unknown as Promise<Uint8Array>);
const valid = await (webCryptoEd25519Engine.verify(kp.publicKey, signature, message) as unknown as Promise<boolean>);
```
### X25519 Key Agreement
```typescript
import { webCryptoX25519Engine } from "@suaveplan/crypto-browser/engines/webcrypto-agree-engine/webcrypto-agree-engine";
const alice = await (webCryptoX25519Engine.generateKeypair() as unknown as Promise<{ publicKey: Uint8Array; secretKey: Uint8Array }>);
const bob = await (webCryptoX25519Engine.generateKeypair() as unknown as Promise<{ publicKey: Uint8Array; secretKey: Uint8Array }>);
const shared = await (webCryptoX25519Engine.agree(alice.secretKey, bob.publicKey) as unknown as Promise<Uint8Array>);
```
### RSA-OAEP Encryption
```typescript
import { rsa } from "@suaveplan/crypto-browser/rsa/rsa";
const { publicKey, privateKey } = await rsa.generateKeypair("RSA-OAEP");
const plaintext = new TextEncoder().encode("rsa-oaep secret");
const ciphertext = await rsa.encrypt(publicKey, plaintext);
const decrypted = await rsa.decrypt(privateKey, ciphertext);
```
### AES Key Wrap
```typescript
import { aesKw } from "@suaveplan/crypto-browser/aes-kw/aes-kw";
const wrappingKey = crypto.getRandomValues(new Uint8Array(32));
const keyToWrap = crypto.getRandomValues(new Uint8Array(16));
const wrapped = await aesKw.wrapKey(wrappingKey, keyToWrap);
const unwrapped = await aesKw.unwrapKey(wrappingKey, wrapped);
```
### Non-extractable Key Generation
```typescript
import { nonExtractableKey } from "@suaveplan/crypto-browser/non-extractable-key/non-extractable-key";
const key = await nonExtractableKey({ name: "AES-GCM", length: 256 }, ["encrypt", "decrypt"]);
// key.extractable === false
// crypto.subtle.exportKey("raw", key) will throw
```
## Constraints
- Requires `crypto.subtle` at runtime — every method guards with `assertWebCryptoAvailable()` and throws `BrowserCryptoError` with code `BROWSER_CRYPTO.NOT_AVAILABLE` if absent
- Engine methods return `Promise<T>` cast to `T` at the type level; always `await` the result even though interfaces declare synchronous returns
- Ed25519/X25519 public keys are 32 raw bytes; secret keys are 32-byte seeds (with internal SPKI/PKCS8 wrapping handled transparently)
- P-256 public keys are 65-byte uncompressed points (`0x04 || X || Y`); private keys are PKCS8 DER bytes
- P-384 public keys are 97-byte uncompressed points; private keys are PKCS8 DER bytes
- RSA public keys are SPKI DER bytes and private keys are PKCS8 DER bytes
- AES-KW wrapping key must be 16, 24, or 32 bytes; the key to wrap must be a multiple of 8 bytes
- All errors surface as `BrowserCryptoError` with one of three codes: `BROWSER_CRYPTO.NOT_AVAILABLE`, `BROWSER_CRYPTO.KEY.GENERATION_FAILED`, or `BROWSER_CRYPTO.OPERATION.FAILED`
## Checklist
1. Verify `crypto.subtle` is available before calling any engine method (`isWebCryptoAvailable()`)
2. Always `await` engine method results even though types declare synchronous returns
3. Use subpath exports (e.g. `@suaveplan/crypto-browser/engines/webcrypto-aead-engine`) for tree-shaking
4. Catch `BrowserCryptoError` and inspect `err.code` for `BROWSER_CRYPTO.NOT_AVAILABLE`, `KEY.GENERATION_FAILED`, or `OPERATION.FAILED`

View File

@@ -0,0 +1,138 @@
---
name: sp-crypto-cipher
description: Low-level AEAD cipher primitives (AES-256-GCM, ChaCha20-Poly1305, XChaCha20-Poly1305) with streaming AEAD and per-primitive subpath exports.
---
# Crypto Cipher for Suaveplan
Low-level AEAD cipher primitives for the `@suaveplan/crypto` stack. Provides three standalone algorithms — AES-256-GCM, ChaCha20-Poly1305, and XChaCha20-Poly1305 — each as a tree-shakeable subpath export, plus chunked streaming AEAD via AES-GCM and XChaCha20 engines with authenticated framing. Nonces are always caller-supplied; the package validates lengths but never generates randomness internally.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `aes-256-gcm` (module) | `encrypt(key, nonce, plaintext, aad?)` / `decrypt(key, nonce, ciphertext, aad?)` with 32-byte key, 12-byte nonce, 16-byte tag |
| `chacha20-poly1305` (module) | `encrypt(key, nonce, plaintext, aad?)` / `decrypt(key, nonce, ciphertext, aad?)` with 32-byte key, 12-byte nonce, 16-byte tag |
| `xchacha20-poly1305` (module) | `encrypt(key, nonce, plaintext, aad?)` / `decrypt(key, nonce, ciphertext, aad?)` with 32-byte key, 24-byte nonce, 16-byte tag |
| `aesGcmStreamingEngine` | `StreamingAeadEngine` implementation that derives 12-byte per-chunk nonces from an 8-byte base nonce + 32-bit counter (high bit carries final-flag) |
| `xchacha20StreamingEngine` | `StreamingAeadEngine` implementation that derives 24-byte per-chunk nonces from a 20-byte base nonce + 32-bit counter |
| `encryptStreamAead` | Async-generator streaming encryption yielding a wire header followed by framed AEAD chunks; binds total chunk count via a final-flag terminator |
| `decryptStreamAead` | Async-generator streaming decryption that buffers fragments, validates the header, and rejects truncation or tag mismatches |
| `CryptoCipherError` | Typed error extending `SuaveplanError`, surfaced for every failure with a stable `CRYPTO.CIPHER.*` code |
| `CRYPTO_CIPHER_CODES` | `defineCodes("CRYPTO.CIPHER", [...])` registry for key-length, nonce-length, decrypt-failure, and streaming errors |
## File Structure
```
security-stack/crypto/crypto-cipher/
src/errors/ # CRYPTO_CIPHER_CODES and CryptoCipherError
src/primitives/aes-256-gcm/ # encrypt/decrypt, noble/native impls, build-time selector
src/primitives/chacha20-poly1305/ # encrypt/decrypt, noble/native impls, build-time selector
src/primitives/xchacha20-poly1305/ # encrypt/decrypt, noble/native impls, build-time selector
src/streaming/streaming-aead/ # encryptStreamAead / decryptStreamAead, wire format, types
src/streaming/streaming-aead-aes-gcm/ # aesGcmStreamingEngine
src/streaming/streaming-aead-xchacha20/ # xchacha20StreamingEngine
```
Sibling packages:
- `@suaveplan/crypto-utils` — shared encoding/byte helpers and `PrimitiveNotImplemented` sentinel used by native stubs
- `@suaveplan/crypto` — high-level façade that combines nonce generation, engine selection, and consumes the streaming AEAD interface
- `@suaveplan/crypto-core` — universal cryptographic primitives (AEAD, signing, key agreement) with pluggable engines
- `@suaveplan/crypto-hash` — SHA-2, HMAC, HKDF, PBKDF2, Argon2id hashing primitives
- `@suaveplan/crypto-browser` — WebCrypto-backed engine implementations for AES-256-GCM, Ed25519, ECDSA-P256, X25519
- `@suaveplan/crypto-sodium` — libsodium-backed engine implementations for AEAD and signing
## Patterns
### Quick Start — Primitive Encrypt/Decrypt
```typescript
import { encrypt, decrypt, KEY_LENGTH, NONCE_LENGTH } from "@suaveplan/crypto-cipher/aes-256-gcm";
const key = new Uint8Array(KEY_LENGTH).fill(0x01);
const nonce = new Uint8Array(NONCE_LENGTH).fill(0x02);
const plaintext = new TextEncoder().encode("hello world");
const ct = encrypt(key, nonce, plaintext);
// ciphertext has tag appended
const pt = decrypt(key, nonce, ct);
```
### Streaming AEAD Round-Trip
```typescript
import { encryptStreamAead, decryptStreamAead } from "@suaveplan/crypto-cipher";
import { aesGcmStreamingEngine } from "@suaveplan/crypto-cipher/streaming/streaming-aead-aes-gcm";
async function* gen(chunks: Uint8Array[]): AsyncGenerator<Uint8Array> {
for (const c of chunks) yield c;
}
const key = new Uint8Array(aesGcmStreamingEngine.keyLength).fill(0x07);
const baseNonce = new Uint8Array(aesGcmStreamingEngine.nonceBaseLength).fill(0x09);
const chunks = [plaintext.slice(0, 10), plaintext.slice(10)];
const wireChunks: Uint8Array[] = [];
for await (const w of encryptStreamAead(key, gen(chunks), {
engine: aesGcmStreamingEngine,
baseNonce,
})) {
wireChunks.push(w);
}
const decrypted = await collect(decryptStreamAead(key, gen(wireChunks), { engine: aesGcmStreamingEngine }));
```
### AAD Authentication
```typescript
import { encrypt, decrypt } from "@suaveplan/crypto-cipher/aes-256-gcm";
const key = new Uint8Array(32).fill(0x01);
const nonce = new Uint8Array(12).fill(0x02);
const aad = new TextEncoder().encode("meta");
const pt = new TextEncoder().encode("payload");
const ct = encrypt(key, nonce, pt, aad);
decrypt(key, nonce, ct, aad); // succeeds
decrypt(key, nonce, ct, new TextEncoder().encode("wrong")); // throws CryptoCipherError
```
### Custom Streaming Engine
```typescript
import { xchacha20StreamingEngine } from "@suaveplan/crypto-cipher/streaming/streaming-aead-xchacha20";
import type { StreamingAeadEngine } from "@suaveplan/crypto-cipher";
const spy: StreamingAeadEngine = {
...xchacha20StreamingEngine,
id: "test-spy",
wireId: 0x7f,
seal: async (input) => {
console.log("seal", input.counter, input.finalFlag);
return xchacha20StreamingEngine.seal(input);
},
open: async (input) => xchacha20StreamingEngine.open(input),
};
for await (const w of encryptStreamAead(key, source, { engine: spy, baseNonce })) {
wire.push(w);
}
```
## Constraints
- Nonces are caller-supplied; this package never generates randomness internally and has no `randomBytes` callsite
- All Phase 0 implementations route to `@noble/ciphers`; native stubs throw `PrimitiveNotImplemented` from `@suaveplan/crypto-utils`
- Implementation flavour (`'noble'` vs `'native'`) is selected at build time via `crypto.config.ts`; selector files are generated by `scripts/gen-crypto-selects.ts`
- Error codes must be declared with `defineCodes("CRYPTO.CIPHER", [...])`; the resulting code string is `CRYPTO.CIPHER.REASON` (e.g. `CRYPTO.CIPHER.DECRYPT_FAILED`) — never hand-write a raw string literal in source or tests
- `sideEffects: false` with per-primitive subpath exports; import from `@suaveplan/crypto-cipher/aes-256-gcm` etc. for tree-shaking
- Streaming wire format: magic(4) `SPAE` || engineId(1) || version(1) || baseNonceLen(2) || baseNonce || [4-byte len BE][1-byte finalFlag][ciphertext||tag]...
- Streaming decryption rejects truncation (missing final-flag frame), mid-frame truncation, data after final frame, corrupted magic, engine mismatch, and bad version — all surfaced as `CryptoCipherError`
## Checklist
1. Verify every primitive has both a `.noble` and `.native` implementation file, even if native is a stub
2. Ensure `CryptoCipherError` is thrown for all decrypt failures and length violations, never a raw `@noble/ciphers` error
3. Confirm error assertions in tests use the exported code constant (`CRYPTO_CIPHER_CODES.DECRYPT_FAILED`), never a hand-typed string literal
4. Run `bunx turbo lint typecheck test:coverage --filter=@suaveplan/crypto-cipher` before declaring changes complete

View File

@@ -0,0 +1,153 @@
---
name: sp-crypto-cli
description: Suaveplan keychain CLI — root + server identity lifecycle (init, roll, revoke, inspect, verify, export-public, restore)
---
# Keychain CLI for Suaveplan
Pure functional API plus a thin `keychain` binary for managing hierarchical identity lifecycles. Creates root and server identities, rolls or revokes server keys, inspects public bundles, verifies trust anchors, and restores roots from BIP39 mnemonics.
Built on `@suaveplan/crypto` primitives; the CLI layer is a typed dispatch wrapper around pure functions so every operation is unit-testable without TTY mocking.
## Architecture
| Component | Responsibility |
|-----------|---------------|
| `rootInit` | Creates a `RootIdentity`, optionally from a BIP39 mnemonic, and seals the vault with a password-AEAD blob |
| `serverInit` | Issues a `ServerIdentity` signed by a parent root or server identity |
| `serverRoll` | Rotates a server's keypair, increments version, emits a CRL entry; deduplicates via an optional idempotency ledger |
| `serverRevokeCmd` | Revokes a server identity, emits a CRL entry; deduplicates via an optional idempotency set |
| `inspect` | Parses a public bundle and returns `subjectUserId`, `subjectDepth`, `issuedAt`, and `slotIds` |
| `verify` | Verifies a bundle's authority signature against a pinned root trust anchor (single-link fast path) |
| `exportPublic` | Returns the raw `publicBundle` bytes from any identity |
| `restore` | Re-derives a root identity from a mnemonic and re-seals the vault |
| `main(argv)` | Binary entry point: minimal argv parser that rejects `--password`, prints help, and dispatches to the functional API |
| `openSealedBundle` | Decrypts a `SealedPrivateKeyBlob` with a password for round-trip verification |
## File Structure
```
security-stack/crypto/crypto-cli/
src/commands/ # Pure functional API for the 8 subcommands
src/bin/ # `keychain` binary entry point (argv parser + dispatch)
tests/setup/ # Test polyfills
```
Sibling packages:
- `@suaveplan/crypto` — universal cryptographic primitives (AEAD, Ed25519/ECDSA-P256, X25519, keychain) that this CLI orchestrates
- `@suaveplan/crypto-bin` — standalone password-hashing CLI (Argon2id / blake2b)
- `@suaveplan/crypto-vault` — vaulted password-manager ecosystem with 2SKD, SRP-6a, and hierarchical key trees
## Patterns
### Quick Start
```typescript
import { defaultByteSource } from "@suaveplan/crypto";
import {
rootInit,
serverInit,
serverRoll,
serverRevokeCmd,
inspect,
verify,
exportPublic,
restore,
openSealedBundle,
} from "@suaveplan/crypto-cli";
import type { Clock } from "@suaveplan/types";
const FIXED_CLOCK: Clock = () => 1_700_000_000_000;
const ctx = { clock: FIXED_CLOCK, rng: defaultByteSource };
const root = await rootInit(ctx, {
userId: 1,
password: "correct horse battery staple",
});
const server = await serverInit(ctx, {
parent: root.root,
userId: 100,
});
const rolled = await serverRoll(ctx, {
parent: root.root,
prior: server.server,
});
const revoked = serverRevokeCmd(ctx, {
server: rolled.server,
reason: "compromised",
});
```
### Idempotency-Safe Roll and Revoke
```typescript
const rollLedger = new Map<string, ServerIdentity>();
const a = await serverRoll(ctx, {
parent: root.root,
prior: v1.server,
idempotencyKey: "k1",
ledger: rollLedger,
});
const b = await serverRoll(ctx, {
parent: root.root,
prior: v1.server,
idempotencyKey: "k1",
ledger: rollLedger,
});
// b.server === a.server (same reference, no re-issuance)
const revokeLedger = new Set<string>();
const r1 = serverRevokeCmd(ctx, {
server: v1.server,
reason: "x",
idempotencyKey: "k1",
ledger: revokeLedger,
});
const r2 = serverRevokeCmd(ctx, {
server: v1.server,
reason: "x",
idempotencyKey: "k1",
ledger: revokeLedger,
});
// r1.crl.userId === r2.crl.userId (deduplicated)
```
### Verify a Bundle Against a Trust Anchor
```typescript
const root = await rootInit(ctx, { userId: 1, password: "p" });
const out = verify({
bundle: root.publicBundle,
trustAnchor: root.root,
});
expect(out.ok).toBe(true);
```
### Restore from Mnemonic
```typescript
const out = await restore(ctx, {
mnemonic: `${"abandon ".repeat(11)}about`,
userId: 1,
password: "p",
version: 1,
});
expect(out.root.userId).toBe(1);
```
## Constraints
- `--password` is explicitly rejected in the binary; passwords must be supplied via TTY prompt or `KEYCHAIN_PASSWORD` env var
- All async commands require a `CommonContext` with injectable `clock` and `rng` — no `Date.now()` or `crypto.getRandomValues` inside the library
- `verify` is a single-link fast path against a pinned root anchor; full chain traversal lives in `@suaveplan/crypto`
- The package has no subpath exports — everything is imported from `@suaveplan/crypto-cli`
## Checklist
1. Confirm `CommonContext` (`clock` + `rng`) is wired before calling any async command
2. Reject `--password` in any CLI host wrapper; use TTY prompt or `KEYCHAIN_PASSWORD` env var instead
3. Pass an `idempotencyKey` + `ledger`/`Set` to `serverRoll` and `serverRevokeCmd` when calling from automated jobs
4. Verify the package has no subpath exports — import everything from `@suaveplan/crypto-cli`

Some files were not shown because too many files have changed in this diff Show More