first commit
This commit is contained in:
5
plugins/library-navigator/.claude-plugin/plugin.json
Normal file
5
plugins/library-navigator/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "library-navigator",
|
||||
"version": "1.0.0",
|
||||
"description": "Natural-language library discovery across the 500+ @suaveplan/* packages"
|
||||
}
|
||||
52
plugins/library-navigator/skills/sp-ab-testing/SKILL.md
Normal file
52
plugins/library-navigator/skills/sp-ab-testing/SKILL.md
Normal 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
|
||||
@@ -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
|
||||
53
plugins/library-navigator/skills/sp-audience-rules/SKILL.md
Normal file
53
plugins/library-navigator/skills/sp-audience-rules/SKILL.md
Normal 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
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
78
plugins/library-navigator/skills/sp-audio-cli/SKILL.md
Normal file
78
plugins/library-navigator/skills/sp-audio-cli/SKILL.md
Normal 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`
|
||||
@@ -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`
|
||||
@@ -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
|
||||
80
plugins/library-navigator/skills/sp-audio-codecs/SKILL.md
Normal file
80
plugins/library-navigator/skills/sp-audio-codecs/SKILL.md
Normal 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`
|
||||
@@ -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
|
||||
67
plugins/library-navigator/skills/sp-audio-core/SKILL.md
Normal file
67
plugins/library-navigator/skills/sp-audio-core/SKILL.md
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
54
plugins/library-navigator/skills/sp-audio-ir-core/SKILL.md
Normal file
54
plugins/library-navigator/skills/sp-audio-ir-core/SKILL.md
Normal 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
|
||||
52
plugins/library-navigator/skills/sp-audio-ir-loader/SKILL.md
Normal file
52
plugins/library-navigator/skills/sp-audio-ir-loader/SKILL.md
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
54
plugins/library-navigator/skills/sp-audio-metadata/SKILL.md
Normal file
54
plugins/library-navigator/skills/sp-audio-metadata/SKILL.md
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
60
plugins/library-navigator/skills/sp-audio-timeline/SKILL.md
Normal file
60
plugins/library-navigator/skills/sp-audio-timeline/SKILL.md
Normal 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
|
||||
66
plugins/library-navigator/skills/sp-audio-transport/SKILL.md
Normal file
66
plugins/library-navigator/skills/sp-audio-transport/SKILL.md
Normal 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
|
||||
92
plugins/library-navigator/skills/sp-audit-logging/SKILL.md
Normal file
92
plugins/library-navigator/skills/sp-audit-logging/SKILL.md
Normal 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
|
||||
103
plugins/library-navigator/skills/sp-auth/SKILL.md
Normal file
103
plugins/library-navigator/skills/sp-auth/SKILL.md
Normal 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
|
||||
144
plugins/library-navigator/skills/sp-barcode/SKILL.md
Normal file
144
plugins/library-navigator/skills/sp-barcode/SKILL.md
Normal 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)`
|
||||
@@ -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
|
||||
@@ -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`.
|
||||
188
plugins/library-navigator/skills/sp-bitpacker-crypto/SKILL.md
Normal file
188
plugins/library-navigator/skills/sp-bitpacker-crypto/SKILL.md
Normal 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
|
||||
154
plugins/library-navigator/skills/sp-bitpacker-delta/SKILL.md
Normal file
154
plugins/library-navigator/skills/sp-bitpacker-delta/SKILL.md
Normal 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"`
|
||||
221
plugins/library-navigator/skills/sp-bitpacker-game/SKILL.md
Normal file
221
plugins/library-navigator/skills/sp-bitpacker-game/SKILL.md
Normal 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`
|
||||
179
plugins/library-navigator/skills/sp-bitpacker-network/SKILL.md
Normal file
179
plugins/library-navigator/skills/sp-bitpacker-network/SKILL.md
Normal 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`
|
||||
180
plugins/library-navigator/skills/sp-bitpacker-schema/SKILL.md
Normal file
180
plugins/library-navigator/skills/sp-bitpacker-schema/SKILL.md
Normal 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.
|
||||
@@ -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(...)`.
|
||||
197
plugins/library-navigator/skills/sp-bitpacker-streaming/SKILL.md
Normal file
197
plugins/library-navigator/skills/sp-bitpacker-streaming/SKILL.md
Normal 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`
|
||||
@@ -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.
|
||||
151
plugins/library-navigator/skills/sp-bitpacker/SKILL.md
Normal file
151
plugins/library-navigator/skills/sp-bitpacker/SKILL.md
Normal 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.
|
||||
223
plugins/library-navigator/skills/sp-booking/SKILL.md
Normal file
223
plugins/library-navigator/skills/sp-booking/SKILL.md
Normal 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`.
|
||||
108
plugins/library-navigator/skills/sp-broadcast-channel/SKILL.md
Normal file
108
plugins/library-navigator/skills/sp-broadcast-channel/SKILL.md
Normal 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
|
||||
113
plugins/library-navigator/skills/sp-bytes/SKILL.md
Normal file
113
plugins/library-navigator/skills/sp-bytes/SKILL.md
Normal 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
|
||||
106
plugins/library-navigator/skills/sp-cache-redis/SKILL.md
Normal file
106
plugins/library-navigator/skills/sp-cache-redis/SKILL.md
Normal 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.
|
||||
133
plugins/library-navigator/skills/sp-cache-storage/SKILL.md
Normal file
133
plugins/library-navigator/skills/sp-cache-storage/SKILL.md
Normal 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.
|
||||
162
plugins/library-navigator/skills/sp-cache/SKILL.md
Normal file
162
plugins/library-navigator/skills/sp-cache/SKILL.md
Normal 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.
|
||||
174
plugins/library-navigator/skills/sp-caddy-imgproxy/SKILL.md
Normal file
174
plugins/library-navigator/skills/sp-caddy-imgproxy/SKILL.md
Normal 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`
|
||||
139
plugins/library-navigator/skills/sp-campaigns/SKILL.md
Normal file
139
plugins/library-navigator/skills/sp-campaigns/SKILL.md
Normal 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.
|
||||
164
plugins/library-navigator/skills/sp-cbor/SKILL.md
Normal file
164
plugins/library-navigator/skills/sp-cbor/SKILL.md
Normal 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`.
|
||||
56
plugins/library-navigator/skills/sp-cli/SKILL.md
Normal file
56
plugins/library-navigator/skills/sp-cli/SKILL.md
Normal 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
|
||||
132
plugins/library-navigator/skills/sp-clipboard/SKILL.md
Normal file
132
plugins/library-navigator/skills/sp-clipboard/SKILL.md
Normal 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
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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))`
|
||||
@@ -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 })`
|
||||
@@ -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`
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)`
|
||||
166
plugins/library-navigator/skills/sp-colours/SKILL.md
Normal file
166
plugins/library-navigator/skills/sp-colours/SKILL.md
Normal 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)`
|
||||
158
plugins/library-navigator/skills/sp-compress-image/SKILL.md
Normal file
158
plugins/library-navigator/skills/sp-compress-image/SKILL.md
Normal 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`
|
||||
126
plugins/library-navigator/skills/sp-compress-svg/SKILL.md
Normal file
126
plugins/library-navigator/skills/sp-compress-svg/SKILL.md
Normal 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.
|
||||
176
plugins/library-navigator/skills/sp-config/SKILL.md
Normal file
176
plugins/library-navigator/skills/sp-config/SKILL.md
Normal 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.
|
||||
56
plugins/library-navigator/skills/sp-contacts/SKILL.md
Normal file
56
plugins/library-navigator/skills/sp-contacts/SKILL.md
Normal 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
|
||||
@@ -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
|
||||
120
plugins/library-navigator/skills/sp-controller/SKILL.md
Normal file
120
plugins/library-navigator/skills/sp-controller/SKILL.md
Normal 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.
|
||||
152
plugins/library-navigator/skills/sp-crdt/SKILL.md
Normal file
152
plugins/library-navigator/skills/sp-crdt/SKILL.md
Normal 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.
|
||||
@@ -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
|
||||
192
plugins/library-navigator/skills/sp-cron/SKILL.md
Normal file
192
plugins/library-navigator/skills/sp-cron/SKILL.md
Normal 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.
|
||||
112
plugins/library-navigator/skills/sp-crypto-bin/SKILL.md
Normal file
112
plugins/library-navigator/skills/sp-crypto-bin/SKILL.md
Normal 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
|
||||
145
plugins/library-navigator/skills/sp-crypto-browser/SKILL.md
Normal file
145
plugins/library-navigator/skills/sp-crypto-browser/SKILL.md
Normal 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`
|
||||
138
plugins/library-navigator/skills/sp-crypto-cipher/SKILL.md
Normal file
138
plugins/library-navigator/skills/sp-crypto-cipher/SKILL.md
Normal 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
|
||||
153
plugins/library-navigator/skills/sp-crypto-cli/SKILL.md
Normal file
153
plugins/library-navigator/skills/sp-crypto-cli/SKILL.md
Normal 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
Reference in New Issue
Block a user