Initial Commit
This commit is contained in:
79
.audit/status-audit-2026-05-25.md
Normal file
79
.audit/status-audit-2026-05-25.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# STATUS Audit — 2026-05-25
|
||||
|
||||
## Summary
|
||||
|
||||
- Reviewed: 35 rows marked `–` in STATUS.md (scope: non-api, non-react, non-r3f, non-games, non-python)
|
||||
- **Actually shipped (flip `–` → `✅`): 7**
|
||||
- `audio-engine-server`, `contacts`, `macos-contacts`, `audience-rules`, `streams-chunk-queue`, `streams-metadata`, `workflow`
|
||||
- **Genuinely pending (no genesis src on disk): 28**
|
||||
- `dmx`, `docs`, `pdf`, `email-templates`, `form-builder`, `caddy-imgproxy`, `emblem-solver`, `ports`, `imessage-adapter`, `sms`, `sms-providers`, `analytics-browser`, `pwa`, `electrobun`, `linter`, `overview`, `sandbox`, `test-vite`, `wip`, `clipboard`, `mcp`, `media`, `spatial-hash`, `storage-browser`, `storybook-test-utils`, `video`, `video-thumbnails`, `web-workers`
|
||||
- **Partial / drift: 0** — every found row passes all 4 hard gates (package.json + non-stub index.ts + tests + substantive README); no half-built folders detected in scope.
|
||||
|
||||
## Detail — flips needed (`–` → `✅`)
|
||||
|
||||
| Package | Folder | OpenSpec change-id | Src state |
|
||||
|---|---|---|---|
|
||||
| `@suaveplan/audio-engine-server` | `/home/paul/web/genesis/packages/media/audio/audio-engine-server` | `openspec/archive/2026-05-23-enterprise-audio-engine-server` | 7 feature dirs (adapter, error-codes, fx, internal, sink, source) + 8 `*.test.ts`; `src/index.ts` 60 lines of real named exports (`BusOrchestrator`, `ServerTimelineAdapter`, schemas, types); README 56 lines; v0.1.0. |
|
||||
| `@suaveplan/contacts` | `/home/paul/web/genesis/packages/commerce/contacts/contacts` | `openspec/archive/2026-05-20-migrate-contacts` | 8 feature dirs (core, error-codes, import-export, otel, schemas, stores, tree, types) + 15 `*.test.ts` + spec-conformance file; `src/index.ts` 121 lines real barrel; README 86 lines; v0.1.0. |
|
||||
| `@suaveplan/macos-contacts` | `/home/paul/web/genesis/packages/commerce/contacts/macos-contacts` | `openspec/archive/2026-05-20-migrate-macos-contacts` | 8 feature dirs (applescript, bridge, error-codes, executor, otel, parser, schemas, types) + 8 `*.test.ts` + spec-conformance; `src/index.ts` 55 lines real barrel; README 41 lines; v0.1.0. |
|
||||
| `@suaveplan/audience-rules` | `/home/paul/web/genesis/packages/commerce/contacts/audience-rules` | `openspec/archive/2026-05-23-enterprise-audience-rules` | 7 feature dirs (predicates, schemas, segment-evaluator, segment-refresher, store, types, error-codes) + 6 `*.test.ts` + spec-conformance; `src/index.ts` 67 lines real barrel with full docblock; README 93 lines; v0.1.0. |
|
||||
| `@suaveplan/streams-chunk-queue` | `/home/paul/web/genesis/packages/foundation/flow/streams-chunk-queue` | `openspec/archive/2026-05-23-enterprise-streams-chunk-queue` | 11 feature dirs (binary-protocol, decryptor, errors, events, hooks, queue, rate-limiter, storage-adapter, telemetry, types, validator) + 13 `*.test.ts`; `src/index.ts` 82 lines real barrel; README 70 lines; v0.1.0. |
|
||||
| `@suaveplan/streams-metadata` | `/home/paul/web/genesis/packages/foundation/flow/streams-metadata` | `openspec/archive/2026-05-23-enterprise-streams-metadata` | 6 feature dirs (errors, events, exif, metadata, mime, types) + 4 `*.test.ts`; `src/index.ts` 23 lines but with substantive named exports incl. `STREAMS_METADATA_CODES`, error classes, factories; README 70 lines; v0.1.0. (Smallest of the shipped set — verify next consumer agrees with surface, but it satisfies all 4 gates.) |
|
||||
| `@suaveplan/workflow` | `/home/paul/web/genesis/packages/foundation/flow/workflow` | `openspec/archive/2026-05-24-enterprise-workflow` | 9 feature dirs (compensation, constants, core, errors, middleware, otel, steps, types, utils) + 14 `*.test.ts`; `src/index.ts` 118 lines real barrel; README 127 lines; v0.1.0. |
|
||||
|
||||
## Detail — partials
|
||||
|
||||
None. Every directory matching the scoped names that exists on disk fully passes gates (1)–(4). The remaining 28 rows have **zero** package directory under `packages/<cat>/<plugin>/<pkg>/` matching the package name (verified via `find packages/ -type d -name <pkg>`).
|
||||
|
||||
## Detail — true leaves (no `@suaveplan/*` runtime deps, ready to migrate)
|
||||
|
||||
Ranked smallest/cleanest leaf first. LOC = `wc -l` over all `*.ts` under main-branch `src/`. "ext deps" = non-`tslib` runtime deps in `package.json`. All listed packages have **zero** `@suaveplan/*` runtime dependencies in main — they can be migrated without any prior-package gating.
|
||||
|
||||
### Tier 1 — trivial leaves (≤ 1k LOC, 0–1 external dep)
|
||||
|
||||
1. **`wip`** — main src 18 LOC, ext deps: 0 (tslib only). Spec `enterprise/wip.md` 116 lines. The smallest possible leaf; effectively a placeholder package per spec — verify scope before swarming.
|
||||
2. **`test-vite`** — main src 20 LOC, ext deps: `vite`, `vitest` (catalog). Spec `enterprise/test-vite.md` 101 lines. Tiny build-helper; pure tooling glue.
|
||||
3. **`caddy-imgproxy`** — main src 49 LOC, ext deps: 0. Spec `enterprise/caddy-imgproxy.md` 118 lines. Server-tier image-proxy Caddy config emitter; very small.
|
||||
4. **`overview`** — main src not in `src/` (skill loader package), 1 `@suaveplan/*` dep is install-time only per package.json structure (`deps: {}`). Spec `enterprise/overview.md` 116 lines. Already named in CLAUDE.md foundation; safe leaf.
|
||||
5. **`storybook-test-utils`** — main src 392 LOC, ext deps: `@storybook/testing-library`, `@testing-library/react`, `react`. Browser-tier. Spec `enterprise/storybook-test-utils.md` 101 lines. Pure adapter shims.
|
||||
6. **`spatial-hash`** — main src 663 LOC, ext deps: 0. Spec `enterprise/spatial-hash.md` 116 lines. Pure-algo gaming-adjacent broad-phase; trivial leaf despite categorisation note (CLAUDE.md says non-games scope here, but this is a generic data structure, not gameplay logic).
|
||||
7. **`linter`** — main src 856 LOC, ext deps: `@biomejs/biome`, `glob`, `remark*`, `unified`, `unist-util-visit`. Spec 118 lines. Server-tier markdown/biome wrapper; many ext deps but all leaf-stable.
|
||||
|
||||
### Tier 2 — small leaves (~1–2k LOC, 0 `@suaveplan/*` runtime deps)
|
||||
|
||||
8. **`clipboard`** — main src 900 LOC, ext deps: 0. Browser-tier. Spec 104 lines.
|
||||
9. **`mcp`** — main src 1024 LOC, ext deps: `zod`. Spec 119 lines. MCP server/client foundation; promising fresh-build candidate.
|
||||
10. **`sandbox`** — main src 3432 LOC, ext deps: 0. Server-tier. Spec 118 lines. Larger but zero @suaveplan deps.
|
||||
11. **`pdf`** — main src 9436 LOC, ext deps: 0. Server-tier. Spec 118 lines. Big but a clean leaf (likely consumes only pdfkit/puppeteer-style deps inlined as devDeps in main); inspect before swarming.
|
||||
12. **`pwa`** — main src 2336 LOC, ext deps: 0. Browser-tier. Spec 115 lines.
|
||||
13. **`emblem-solver`** — main src 2003 LOC, ext deps: 0. Spec 116 lines.
|
||||
|
||||
### Tier 3 — leaves with one `@suaveplan/*` runtime dep (gate one already-shipped pkg)
|
||||
|
||||
14. **`analytics-browser`** — 1037 LOC, depends on `@suaveplan/events` (shipped ✅).
|
||||
15. **`web-workers`** — 961 LOC, depends on `@suaveplan/events` (shipped ✅).
|
||||
16. **`storage-browser`** — 1724 LOC, depends on `@suaveplan/storage` (shipped ✅).
|
||||
17. **`imessage-adapter`** — 718 LOC, depends on `@suaveplan/events` (shipped ✅).
|
||||
18. **`video-thumbnails`** — 1067 LOC, depends on `@suaveplan/error` (shipped ✅).
|
||||
19. **`sms-providers`** — 1755 LOC, depends on `@suaveplan/error` (shipped ✅).
|
||||
|
||||
### Tier 4 — multi-dep but all deps already shipped
|
||||
|
||||
20. **`media`** — 400 LOC; depends on `@suaveplan/mime` (shipped). Smallest of the multi-dep group. **Naming note:** the genesis category dir is also called `media/`, so the package will live at `packages/media/<plugin>/media/` — confirm plugin assignment in `scripts/folder-reorg/assignment.yml` before migration.
|
||||
21. **`email-templates`** — 1953 LOC; React-tier (peer deps on `react`/`react-dom`). No @suaveplan runtime deps.
|
||||
22. **`form-builder`** — 1355 LOC; `@suaveplan/error` + `@suaveplan/events` (both shipped).
|
||||
23. **`ports`** — 2921 LOC; `@suaveplan/geo` (verify shipped; CLAUDE.md lists `geo` as shipped).
|
||||
24. **`sms`** — 1389 LOC; `@suaveplan/error` + `@suaveplan/events` (both shipped).
|
||||
25. **`docs`** — 1136 LOC; zero `@suaveplan/*` runtime deps (only `glob`/`remark*`/`unified`/`gray-matter`). Surprisingly a true leaf despite being listed mid-tier — could move to Tier 2 if confirmed.
|
||||
26. **`video`** — 1517 LOC; `@suaveplan/error` + `@suaveplan/events` (both shipped).
|
||||
27. **`dmx`** — 14012 LOC; `@suaveplan/events` + `@suaveplan/timing` (both shipped). **Largest pending** in scope.
|
||||
28. **`electrobun`** — 2169 LOC; `@suaveplan/cli` + `@suaveplan/config` + `@suaveplan/templates` (all shipped) + external `iconz`.
|
||||
|
||||
### Noteworthy constraints
|
||||
|
||||
- **`docs` is misclassified in main as having deps** — the actual `dependencies` block is purely external (markdown toolchain). It's a true leaf.
|
||||
- **`mcp`** at 1024 LOC + only `zod` is the highest-value/lowest-risk net-new build candidate; consider promoting in priority.
|
||||
- **`overview`** is already listed under genesis foundation in CLAUDE.md as `overview` (machine-readable monorepo manifest). The `–` row in STATUS.md may be stale — **double-check disk** before swarming; if a sibling `overview` package exists outside the audited categories it may already be `✅` and just missing from this row's status. (My `find` against `packages/` for `overview` returned MISSING, so as of audit time the row is correct, but the spec sketches a richer surface than the CLAUDE.md description — reconcile before migration.)
|
||||
- **`pdf` at 9.4k LOC and `dmx` at 14k LOC** are large enough that a single agent migration is risky — plan as multi-chunk OpenSpec changes (similar to recent api-plugin-campaigns chunks A/B/C).
|
||||
- **`spatial-hash`** is technically gaming-category in CLAUDE.md taxonomy; user's scope filter excludes "games" but a generic spatial hash is a foundation data-structure — confirm whether it belongs in scope before migrating.
|
||||
- **`storage-browser`** in main is superseded — the `2026-05-12-enterprise-browser-storage-browser-superseded-by-split` archive plus shipped `storage-browser-{indexeddb,localstorage,opfs}` rows in STATUS.md (lines 654/659 confirmed) means this row should likely be **retired**, not migrated. STATUS.md row at line 740 is stale; the package was split.
|
||||
155
.claude/commands/opsx/apply.md
Normal file
155
.claude/commands/opsx/apply.md
Normal file
@@ -0,0 +1,155 @@
|
||||
---
|
||||
name: "OPSX: Apply"
|
||||
description: Implement tasks from an OpenSpec change (Experimental)
|
||||
category: Workflow
|
||||
tags: [workflow, artifacts, experimental]
|
||||
---
|
||||
|
||||
Implement tasks from an OpenSpec change.
|
||||
|
||||
**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Select the change**
|
||||
|
||||
If a name is provided, use it. Otherwise:
|
||||
- Infer from conversation context if the user mentioned a change
|
||||
- Auto-select if only one active change exists
|
||||
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
||||
|
||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||
|
||||
3. **Get apply instructions**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns:
|
||||
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema)
|
||||
- Progress (total, complete, remaining)
|
||||
- Task list with status
|
||||
- Dynamic instruction based on current state
|
||||
|
||||
**Handle states:**
|
||||
- If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue`
|
||||
- If `state: "all_done"`: congratulate, suggest archive
|
||||
- Otherwise: proceed to implementation
|
||||
|
||||
4. **Read context files**
|
||||
|
||||
Read every file path listed under `contextFiles` from the apply instructions output.
|
||||
The files depend on the schema being used:
|
||||
- **spec-driven**: proposal, specs, design, tasks
|
||||
- Other schemas: follow the contextFiles from CLI output
|
||||
|
||||
5. **Show current progress**
|
||||
|
||||
Display:
|
||||
- Schema being used
|
||||
- Progress: "N/M tasks complete"
|
||||
- Remaining tasks overview
|
||||
- Dynamic instruction from CLI
|
||||
|
||||
6. **Implement tasks (loop until done or blocked)**
|
||||
|
||||
For each pending task:
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
||||
- Continue to next task
|
||||
|
||||
**Pause if:**
|
||||
- Task is unclear → ask for clarification
|
||||
- Implementation reveals a design issue → suggest updating artifacts
|
||||
- Error or blocker encountered → report and wait for guidance
|
||||
- User interrupts
|
||||
|
||||
7. **On completion or pause, show status**
|
||||
|
||||
Display:
|
||||
- Tasks completed this session
|
||||
- Overall progress: "N/M tasks complete"
|
||||
- If all done: suggest archive
|
||||
- If paused: explain why and wait for guidance
|
||||
|
||||
**Output During Implementation**
|
||||
|
||||
```text
|
||||
## Implementing: <change-name> (schema: <schema-name>)
|
||||
|
||||
Working on task 3/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 4/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
```
|
||||
|
||||
**Output On Completion**
|
||||
|
||||
```text
|
||||
## Implementation Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 7/7 tasks complete ✓
|
||||
|
||||
### Completed This Session
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
...
|
||||
|
||||
All tasks complete! You can archive this change with `/opsx:archive`.
|
||||
```
|
||||
|
||||
**Output On Pause (Issue Encountered)**
|
||||
|
||||
```text
|
||||
## Implementation Paused
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 4/7 tasks complete
|
||||
|
||||
### Issue Encountered
|
||||
<description of the issue>
|
||||
|
||||
**Options:**
|
||||
1. <option 1>
|
||||
2. <option 2>
|
||||
3. Other approach
|
||||
|
||||
What would you like to do?
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
|
||||
- Keep going through tasks until done or blocked
|
||||
- Always read context files before starting (from the apply instructions output)
|
||||
- If task is ambiguous, pause and ask before implementing
|
||||
- If implementation reveals issues, pause and suggest artifact updates
|
||||
- Keep code changes minimal and scoped to each task
|
||||
- Update task checkbox immediately after completing each task
|
||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||
- Use contextFiles from CLI output, don't assume specific file names
|
||||
|
||||
**Fluid Workflow Integration**
|
||||
|
||||
This skill supports the "actions on a change" model:
|
||||
|
||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
||||
159
.claude/commands/opsx/archive.md
Normal file
159
.claude/commands/opsx/archive.md
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
name: "OPSX: Archive"
|
||||
description: Archive a completed change in the experimental workflow
|
||||
category: Workflow
|
||||
tags: [workflow, archive, experimental]
|
||||
---
|
||||
|
||||
Archive a completed change in the experimental workflow.
|
||||
|
||||
**Input**: Optionally specify a change name after `/opsx:archive` (e.g., `/opsx:archive add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show only active changes (not already archived).
|
||||
Include the schema used for each change if available.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check artifact completion status**
|
||||
|
||||
Run `openspec status --change "<name>" --json` to check artifact completion.
|
||||
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used
|
||||
- `artifacts`: List of artifacts with their status (`done` or other)
|
||||
|
||||
**If any artifacts are not `done`:**
|
||||
- Display warning listing incomplete artifacts
|
||||
- Prompt user for confirmation to continue
|
||||
- Proceed if user confirms
|
||||
|
||||
3. **Check task completion status**
|
||||
|
||||
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
||||
|
||||
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
||||
|
||||
**If incomplete tasks found:**
|
||||
- Display warning showing count of incomplete tasks
|
||||
- Prompt user for confirmation to continue
|
||||
- Proceed if user confirms
|
||||
|
||||
**If no tasks file exists:** Proceed without task-related warning.
|
||||
|
||||
4. **Assess delta spec sync state**
|
||||
|
||||
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
||||
|
||||
**If delta specs exist:**
|
||||
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
||||
- Determine what changes would be applied (adds, modifications, removals, renames)
|
||||
- Show a combined summary before prompting
|
||||
|
||||
**Prompt options:**
|
||||
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
||||
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
||||
|
||||
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
||||
|
||||
5. **Perform the archive**
|
||||
|
||||
Create the archive directory if it doesn't exist:
|
||||
|
||||
```bash
|
||||
mkdir -p openspec/changes/archive
|
||||
```
|
||||
|
||||
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
||||
|
||||
**Check if target already exists:**
|
||||
- If yes: Fail with error, suggest renaming existing archive or using different date
|
||||
- If no: Move the change directory to archive
|
||||
|
||||
```bash
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
```
|
||||
|
||||
6. **Display summary**
|
||||
|
||||
Show archive completion summary including:
|
||||
- Change name
|
||||
- Schema that was used
|
||||
- Archive location
|
||||
- Spec sync status (synced / sync skipped / no delta specs)
|
||||
- Note about any warnings (incomplete artifacts/tasks)
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```text
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** ✓ Synced to main specs
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Output On Success (No Delta Specs)**
|
||||
|
||||
```text
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** No delta specs
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Output On Success With Warnings**
|
||||
|
||||
```text
|
||||
## Archive Complete (with warnings)
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** Sync skipped (user chose to skip)
|
||||
|
||||
**Warnings:**
|
||||
- Archived with 2 incomplete artifacts
|
||||
- Archived with 3 incomplete tasks
|
||||
- Delta spec sync was skipped (user chose to skip)
|
||||
|
||||
Review the archive if this was not intentional.
|
||||
```
|
||||
|
||||
**Output On Error (Archive Exists)**
|
||||
|
||||
```text
|
||||
## Archive Failed
|
||||
|
||||
**Change:** <change-name>
|
||||
**Target:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
|
||||
Target archive directory already exists.
|
||||
|
||||
**Options:**
|
||||
1. Rename the existing archive
|
||||
2. Delete the existing archive if it's a duplicate
|
||||
3. Wait until a different date to archive
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
|
||||
- Always prompt for change selection if not provided
|
||||
- Use artifact graph (openspec status --json) for completion checking
|
||||
- Don't block archive on warnings - just inform and confirm
|
||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||
- Show clear summary of what happened
|
||||
- If sync is requested, use the Skill tool to invoke `openspec-sync-specs` (agent-driven)
|
||||
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
||||
181
.claude/commands/opsx/explore.md
Normal file
181
.claude/commands/opsx/explore.md
Normal file
@@ -0,0 +1,181 @@
|
||||
---
|
||||
name: "OPSX: Explore"
|
||||
description: "Enter explore mode - think through ideas, investigate problems, clarify requirements"
|
||||
category: Workflow
|
||||
tags: [workflow, explore, experimental, thinking]
|
||||
---
|
||||
|
||||
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
||||
|
||||
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
||||
|
||||
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
||||
|
||||
**Input**: The argument after `/opsx:explore` is whatever the user wants to think about. Could be:
|
||||
|
||||
- A vague idea: "real-time collaboration"
|
||||
- A specific problem: "the auth system is getting unwieldy"
|
||||
- A change name: "add-dark-mode" (to explore in context of that change)
|
||||
- A comparison: "postgres vs sqlite for this"
|
||||
- Nothing (just enter explore mode)
|
||||
|
||||
---
|
||||
|
||||
## The Stance
|
||||
|
||||
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
||||
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
||||
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
||||
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
||||
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
||||
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
||||
|
||||
---
|
||||
|
||||
## What You Might Do
|
||||
|
||||
Depending on what the user brings, you might:
|
||||
|
||||
**Explore the problem space**
|
||||
|
||||
- Ask clarifying questions that emerge from what they said
|
||||
- Challenge assumptions
|
||||
- Reframe the problem
|
||||
- Find analogies
|
||||
|
||||
**Investigate the codebase**
|
||||
|
||||
- Map existing architecture relevant to the discussion
|
||||
- Find integration points
|
||||
- Identify patterns already in use
|
||||
- Surface hidden complexity
|
||||
|
||||
**Compare options**
|
||||
|
||||
- Brainstorm multiple approaches
|
||||
- Build comparison tables
|
||||
- Sketch tradeoffs
|
||||
- Recommend a path (if asked)
|
||||
|
||||
**Visualize**
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Use ASCII diagrams liberally │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ State │────────▶│ State │ │
|
||||
│ │ A │ │ B │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
│ │
|
||||
│ System diagrams, state machines, │
|
||||
│ data flows, architecture sketches, │
|
||||
│ dependency graphs, comparison tables │
|
||||
│ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Surface risks and unknowns**
|
||||
|
||||
- Identify what could go wrong
|
||||
- Find gaps in understanding
|
||||
- Suggest spikes or investigations
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec Awareness
|
||||
|
||||
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
||||
|
||||
### Check for context
|
||||
|
||||
At the start, quickly check what exists:
|
||||
|
||||
```bash
|
||||
openspec list --json
|
||||
```
|
||||
|
||||
This tells you:
|
||||
|
||||
- If there are active changes
|
||||
- Their names, schemas, and status
|
||||
- What the user might be working on
|
||||
|
||||
If the user mentioned a specific change name, read its artifacts for context.
|
||||
|
||||
### When no change exists
|
||||
|
||||
Think freely. When insights crystallize, you might offer:
|
||||
|
||||
- "This feels solid enough to start a change. Want me to create a proposal?"
|
||||
- Or keep exploring - no pressure to formalize
|
||||
|
||||
### When a change exists
|
||||
|
||||
If the user mentions a change or you detect one is relevant:
|
||||
|
||||
1. **Read existing artifacts for context**
|
||||
- `openspec/changes/<name>/proposal.md`
|
||||
- `openspec/changes/<name>/design.md`
|
||||
- `openspec/changes/<name>/tasks.md`
|
||||
- etc.
|
||||
|
||||
2. **Reference them naturally in conversation**
|
||||
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
||||
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
||||
|
||||
3. **Offer to capture when decisions are made**
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|----------------------------|--------------------------------|
|
||||
| New requirement discovered | `specs/<capability>/spec.md` |
|
||||
| Requirement changed | `specs/<capability>/spec.md` |
|
||||
| Design decision made | `design.md` |
|
||||
| Scope changed | `proposal.md` |
|
||||
| New work identified | `tasks.md` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
- "This is a new requirement. Add it to specs?"
|
||||
- "This changes scope. Update the proposal?"
|
||||
|
||||
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
||||
|
||||
---
|
||||
|
||||
## What You Don't Have To Do
|
||||
|
||||
- Follow a script
|
||||
- Ask the same questions every time
|
||||
- Produce a specific artifact
|
||||
- Reach a conclusion
|
||||
- Stay on topic if a tangent is valuable
|
||||
- Be brief (this is thinking time)
|
||||
|
||||
---
|
||||
|
||||
## Ending Discovery
|
||||
|
||||
There's no required ending. Discovery might:
|
||||
|
||||
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
||||
- **Just provide clarity**: User has what they need, moves on
|
||||
- **Continue later**: "We can pick this up anytime"
|
||||
|
||||
When things crystallize, you might offer a summary - but it's optional. Sometimes the thinking IS the value.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
||||
- **Don't fake understanding** - If something is unclear, dig deeper
|
||||
- **Don't rush** - Discovery is thinking time, not task time
|
||||
- **Don't force structure** - Let patterns emerge naturally
|
||||
- **Don't auto-capture** - Offer to save insights, don't just do it
|
||||
- **Do visualize** - A good diagram is worth many paragraphs
|
||||
- **Do explore the codebase** - Ground discussions in reality
|
||||
- **Do question assumptions** - Including the user's and your own
|
||||
116
.claude/commands/opsx/propose.md
Normal file
116
.claude/commands/opsx/propose.md
Normal file
@@ -0,0 +1,116 @@
|
||||
---
|
||||
name: "OPSX: Propose"
|
||||
description: Propose a new change - create it and generate all artifacts in one step
|
||||
category: Workflow
|
||||
tags: [workflow, artifacts, experimental]
|
||||
---
|
||||
|
||||
Propose a new change - create the change and generate all artifacts in one step.
|
||||
|
||||
I'll create a change with artifacts:
|
||||
|
||||
- proposal.md (what & why)
|
||||
- design.md (how)
|
||||
- tasks.md (implementation steps)
|
||||
|
||||
When ready to implement, run /opsx:apply
|
||||
|
||||
---
|
||||
|
||||
**Input**: The argument after `/opsx:propose` is the change name (kebab-case), OR a description of what the user wants to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
|
||||
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
|
||||
Parse the JSON to get:
|
||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||
- `artifacts`: list of all artifacts with their status and dependencies
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||
- `template`: The structure to use for your output file
|
||||
- `instruction`: Schema-specific guidance for this artifact type
|
||||
- `outputPath`: Where to write the artifact
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` to start implementing."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
**Guardrails**
|
||||
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
||||
"name": "suaveplan-plugins",
|
||||
"version": "1.0.0",
|
||||
"description": "Suaveplan project-local plugins tailored for React 19 + TypeScript + Tailwind CSS + shadcn/ui monorepo",
|
||||
"owner": {
|
||||
"name": "SuavePlan"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "suaveplan-frontend-design",
|
||||
"description": "Suaveplan-specific frontend design skill for React + Tailwind + shadcn/ui. Enforces project conventions and available @suaveplan/* libraries.",
|
||||
"version": "1.0.0",
|
||||
"source": "./plugins/suaveplan-frontend-design",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "suaveplan-webapp-testing",
|
||||
"description": "Playwright E2E testing toolkit for Suaveplan browser packages. TypeScript .spec.ts tests, Storybook visual testing, shadcn/ui component testing.",
|
||||
"version": "1.0.0",
|
||||
"source": "./plugins/suaveplan-webapp-testing",
|
||||
"category": "testing"
|
||||
},
|
||||
{
|
||||
"name": "suaveplan-web-artifacts",
|
||||
"description": "Build production-grade React artifacts and demos using Suaveplan's component library. React 19 + Vite + Tailwind + shadcn/ui.",
|
||||
"version": "1.0.0",
|
||||
"source": "./plugins/suaveplan-web-artifacts",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "suaveplan-mcp-builder",
|
||||
"description": "Create MCP servers that expose Suaveplan plugin functionality. Maps to definePlugin() architecture with Hono routes and SurrealDB.",
|
||||
"version": "1.0.0",
|
||||
"source": "./plugins/suaveplan-mcp-builder",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "suaveplan-doc-coauthoring",
|
||||
"description": "Structured workflow for co-authoring Suaveplan package documentation. Enforces .md template, simple+advanced examples, and 90%+ coverage.",
|
||||
"version": "1.0.0",
|
||||
"source": "./plugins/suaveplan-doc-coauthoring",
|
||||
"category": "documentation"
|
||||
},
|
||||
{
|
||||
"name": "suaveplan-skill-creator",
|
||||
"description": "Guide for creating project-local skills in the suaveplan-plugins marketplace. Covers plugin structure, SKILL.md authoring, and registration.",
|
||||
"version": "1.0.0",
|
||||
"source": "./plugins/suaveplan-skill-creator",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "suaveplan-theme-factory",
|
||||
"description": "Tailwind CSS + shadcn/ui theming toolkit with 10 curated themes. HSL color system, dark mode, CSS variables for consistent UI styling.",
|
||||
"version": "1.0.0",
|
||||
"source": "./plugins/suaveplan-theme-factory",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "suaveplan-threejs",
|
||||
"description": "React Three Fiber (R3F) development skills for Suaveplan's 3D packages. Covers animation, geometry, interaction, lighting, loaders, materials, post-processing, shaders, textures, and fundamentals.",
|
||||
"version": "1.0.0",
|
||||
"source": "./plugins/suaveplan-threejs",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "suaveplan-webgpu",
|
||||
"description": "WebGPU rendering with Three.js TSL in React Three Fiber. GPU compute shaders, node materials, post-processing, and WGSL integration for the Suaveplan R3F ecosystem.",
|
||||
"version": "1.0.0",
|
||||
"source": "./plugins/suaveplan-webgpu",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "suaveplan-package-structure",
|
||||
"description": "Library package structure patterns, code organization rules, documentation requirements, and Storybook conventions for the Suaveplan monorepo.",
|
||||
"version": "1.0.0",
|
||||
"source": "./plugins/suaveplan-package-structure",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "suaveplan-testing-workflow",
|
||||
"description": "Testing commands, CI/CD integration, advanced turborepo filtering, E2E/Storybook testing, development workflow, and troubleshooting.",
|
||||
"version": "1.0.0",
|
||||
"source": "./plugins/suaveplan-testing-workflow",
|
||||
"category": "testing"
|
||||
},
|
||||
{
|
||||
"name": "suaveplan-dependency-management",
|
||||
"description": "Bun monorepo dependency management with version overrides, catalogs, workspace protocol, tree-shaking, and React version conflict resolution.",
|
||||
"version": "1.0.0",
|
||||
"source": "./plugins/suaveplan-dependency-management",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "suaveplan-bun-runtime",
|
||||
"description": "Bun v1.3.11 runtime APIs, CLI commands, bundler, test runner, database clients, and performance patterns for the Suaveplan monorepo.",
|
||||
"version": "1.0.0",
|
||||
"source": "./plugins/suaveplan-bun-runtime",
|
||||
"category": "development"
|
||||
},
|
||||
{
|
||||
"name": "suaveplan-bitpacker",
|
||||
"description": "Design and create BitPacker binary serialization schemas for compact data encoding in the Suaveplan monorepo using @suaveplan/bitpacker.",
|
||||
"version": "1.0.0",
|
||||
"source": "./plugins/suaveplan-bitpacker",
|
||||
"category": "development"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-bitpacker",
|
||||
"version": "1.0.0",
|
||||
"description": "BitPacker binary serialization schema design for Suaveplan"
|
||||
}
|
||||
@@ -0,0 +1,714 @@
|
||||
---
|
||||
name: bitpacker-schemas
|
||||
description: Design and create BitPacker binary serialization schemas for compact data encoding in the Suaveplan monorepo using @suaveplan/bitpacker
|
||||
---
|
||||
|
||||
# BitPacker Schemas for Suaveplan
|
||||
|
||||
Define compact binary serialization schemas using `@suaveplan/bitpacker`. Schemas describe data shapes declaratively -- the library handles bit-level packing, unpacking, validation, delta compression, and checksums. Use this skill when designing network protocols, game state sync, compact storage formats, or any scenario requiring minimal payload size.
|
||||
|
||||
## Architecture
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Schema Definition | Plain TypeScript objects with `as const` |
|
||||
| Serialization | `BitPacker` class from `@suaveplan/bitpacker` |
|
||||
| Delta Compression | Field-level, presence-based, or none |
|
||||
| Integrity | CRC32, Adler32, XXHash32 checksums |
|
||||
| Validation | Built-in schema matching and error reporting |
|
||||
|
||||
## Schema Type Reference
|
||||
|
||||
Every value in a schema object tells BitPacker how to encode that field. The type is inferred from the value syntax.
|
||||
|
||||
### Primitives
|
||||
|
||||
| Syntax | Type | Bits | Example | Notes |
|
||||
|--------|------|------|---------|-------|
|
||||
| `true` or `false` | Boolean | 1 | `{ active: true }` | Single bit |
|
||||
| `null` | Null | 0 | `{ empty: null }` | Marker only |
|
||||
| `N` (number) | Unsigned integer | ceil(log2(N+1)) | `{ age: 255 }` | Range 0..N |
|
||||
| `"uuid"` | UUID | 128 | `{ id: "uuid" }` | Standard UUID string |
|
||||
|
||||
### Strings
|
||||
|
||||
| Syntax | Type | Example | Notes |
|
||||
|--------|------|---------|-------|
|
||||
| `"N"` | Fixed-length string | `{ code: "5" }` | Exactly N chars, padded |
|
||||
| `"-N"` | Variable-length string | `{ name: "-20" }` | Up to N chars, length-prefixed |
|
||||
|
||||
Variable-length strings (`"-N"`) are almost always preferred -- fixed-length wastes bits on short values.
|
||||
|
||||
### Numbers with Decimals (Fixed-Point)
|
||||
|
||||
| Syntax | Type | Example | Encoded as |
|
||||
|--------|------|---------|------------|
|
||||
| `"W.D"` | Unsigned fixed-point | `{ price: "999.2" }` | Integer with D decimal digits, range 0..W |
|
||||
| `"-W.D"` | Signed fixed-point | `{ x: "-100.3" }` | Signed integer, range -W..W, D decimals |
|
||||
|
||||
Fixed-point is more compact than floats when range and precision are known. `"100.2"` stores values like `42.85` using far fewer bits than `f32`.
|
||||
|
||||
### IEEE Floats
|
||||
|
||||
| Syntax | Bits | Example |
|
||||
|--------|------|---------|
|
||||
| `"f16"` | 16 | `{ ratio: "f16" }` |
|
||||
| `"f32"` | 32 | `{ temperature: "f32" }` |
|
||||
| `"f64"` | 64 | `{ precise: "f64" }` |
|
||||
|
||||
Use only when range or precision is unbounded. Prefer fixed-point when possible.
|
||||
|
||||
### BigInt
|
||||
|
||||
| Syntax | Bits | Example |
|
||||
|--------|------|---------|
|
||||
| `"bigint-u64"` | 64 | `{ snowflake: "bigint-u64" }` |
|
||||
| `"bigint-i64"` | 64 | `{ offset: "bigint-i64" }` |
|
||||
|
||||
### TypedArrays
|
||||
|
||||
Format: `[-)][length][type]` where type is `u8`, `u16`, `u32`, `i8`, `i16`, `i32`, `f32`, `f64`.
|
||||
|
||||
| Syntax | Type | Example | Notes |
|
||||
|--------|------|---------|-------|
|
||||
| `"Nu8"` | Fixed Uint8Array | `{ rgb: "3u8" }` | Exactly N elements |
|
||||
| `"-Nu8"` | Variable Uint8Array | `{ buf: "-100u8" }` | Up to N elements |
|
||||
| `"Nf32"` | Fixed Float32Array | `{ coords: "3f32" }` | Exactly N elements |
|
||||
| `"-Nf32"` | Variable Float32Array | `{ verts: "-100f32" }` | Up to N elements |
|
||||
|
||||
### Enums
|
||||
|
||||
| Syntax | Type | Bits | Example | Notes |
|
||||
|--------|------|------|---------|-------|
|
||||
| `"enum:a\|b\|c"` | String literal union | `ceil(log2(N))` | `{ team: "enum:red\|green\|blue" }` | Values split on `\|`, order-sensitive |
|
||||
|
||||
`EnumPacker` selects the index, not the string. Do not reorder values across protocol versions without migration support.
|
||||
|
||||
### Date / Time
|
||||
|
||||
| Syntax | Type | Bits | Notes |
|
||||
|--------|------|------|-------|
|
||||
| `"timestamp"` | ms since epoch | 42 | ~139 years from epoch |
|
||||
| `"timestamp-ms"` | ms since epoch | 53 | full JS safe integer |
|
||||
| `"timestamp-s"` | seconds since epoch | 32 | standard Unix timestamp |
|
||||
| `"timespan"` | duration in ms | 42 | long durations |
|
||||
| `"timespan-ms"` | duration in ms | 32 | up to ~49.7 days |
|
||||
| `"date-only"` | calendar date | 21 | 12-bit year + 4-bit month + 5-bit day |
|
||||
|
||||
Unpack returns a `number` (ms since epoch) for all tokens; `date-only` also returns a ms timestamp (midnight local time). You may pack a `Date` instance or a `number`.
|
||||
|
||||
### Variable-Length Integers
|
||||
|
||||
| Syntax | Payload | Notes |
|
||||
|--------|---------|-------|
|
||||
| `"varint"` | `number` unsigned | 1–5 bytes; tight for small values |
|
||||
| `"varint-signed"` | `number` signed | ZigZag-encoded |
|
||||
| `"varint-u32"` | `number` unsigned, range-checked to 2³²−1 | |
|
||||
| `"varint-u64"` | `bigint` unsigned | 1–10 bytes |
|
||||
|
||||
### Bitsets
|
||||
|
||||
| Syntax | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `"bitset-N"` | `boolean[]` length N | fixed, no length prefix |
|
||||
| `"bitset--M"` | `boolean[]`, max M | variable; `ceil(log2(M+1))`-bit length prefix |
|
||||
|
||||
One bit per element. `"bitset-16"` is 16 bits on the wire.
|
||||
|
||||
### Maps and Sets
|
||||
|
||||
| Syntax | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| `"map<K,V>"` | `Map<K, V>` | `maxEntries` defaults to 65535 |
|
||||
| `"map<K,V,N>"` | `Map<K, V>` | explicit `maxEntries = N` |
|
||||
| `"set<V,N>"` | `Set<V>` | `N` sizes the length prefix (required) |
|
||||
|
||||
`K` and `V` follow the same disambiguation as other tokens:
|
||||
|
||||
- bare digits (`255`) → integer-range schema (number)
|
||||
- leading `-` or non-numeric (`-20`, `uuid`) → string schema
|
||||
|
||||
So `"map<-20,255,1024>"` = map from variable-length string ≤20 chars to integer 0..255, up to 1024 entries.
|
||||
|
||||
### Custom Types
|
||||
|
||||
Register a handler and reference it by name:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
BitPacker,
|
||||
CustomTypeRegistry,
|
||||
DateTypeHandler,
|
||||
} from '@suaveplan/bitpacker';
|
||||
|
||||
CustomTypeRegistry.getInstance().register(new DateTypeHandler());
|
||||
|
||||
const schema = { createdAt: { _customType: 'date' } } as const;
|
||||
const packer = new BitPacker(schema);
|
||||
```
|
||||
|
||||
The strategy length-prefixes the handler's `Uint8Array` output with 32 bits so handlers with variable frames stay self-delimiting.
|
||||
|
||||
### BigInt Arrays
|
||||
|
||||
| Syntax | Example |
|
||||
|--------|---------|
|
||||
| `"bigint-u64[]"` | `{ ids: "bigint-u64[]" }` |
|
||||
| `"bigint-i64[]"` | `{ offsets: "bigint-i64[]" }` |
|
||||
|
||||
### Binary Blobs
|
||||
|
||||
| Syntax | Type | Example | Notes |
|
||||
|--------|------|---------|-------|
|
||||
| `"~"` | Adaptive blob | `{ data: "~" }` | Uint8Array with 2-bit width-tagged length prefix |
|
||||
|
||||
Uses a 2-bit tag to self-describe the length prefix size (2-5 bytes). The packer picks the smallest tag that fits:
|
||||
|
||||
| Tag | Length bytes | Max size |
|
||||
|-----|-------------|----------|
|
||||
| 0 | 2 | 64 KiB |
|
||||
| 1 | 3 | 16 MiB |
|
||||
| 2 | 4 | 4 GiB |
|
||||
| 3 | 5 | 1 TiB |
|
||||
|
||||
Use for binary payloads with unpredictable sizes (file contents, images, buffers). No max declaration needed in the schema.
|
||||
|
||||
### Nested Objects
|
||||
|
||||
Nest schemas arbitrarily deep using plain objects:
|
||||
|
||||
```typescript
|
||||
const schema = {
|
||||
player: {
|
||||
name: "-20",
|
||||
position: {
|
||||
x: "-1000.2",
|
||||
y: "-1000.2",
|
||||
z: "-1000.2"
|
||||
}
|
||||
}
|
||||
} as const;
|
||||
```
|
||||
|
||||
### Arrays
|
||||
|
||||
Three equivalent notations for arrays. Choose based on readability.
|
||||
|
||||
**Notation 1: String shorthand** -- `-maxLength[elementSchema]`
|
||||
|
||||
```typescript
|
||||
{ scores: "-10[255]" } // Up to 10 integers 0-255
|
||||
{ names: "-5[-20]" } // Up to 5 variable strings
|
||||
{ flags: "-8[true]" } // Up to 8 booleans
|
||||
{ matrix: "-4[-3[255]]" } // Nested: 4 arrays of 3 ints
|
||||
{ items: '-3[{"id":"uuid","name":"-20"}]' } // Up to 3 objects
|
||||
```
|
||||
|
||||
**Notation 2: `_bp` config** -- preferred in TypeScript for type safety
|
||||
|
||||
```typescript
|
||||
{
|
||||
scores: {
|
||||
_bp: {
|
||||
schema: {
|
||||
type: "array",
|
||||
elementType: "integer", // "integer" | "float" | "string" | "boolean" | "schema"
|
||||
elementMax: 255, // For integer/float: max value
|
||||
maxLength: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Notation 3: Direct config object**
|
||||
|
||||
```typescript
|
||||
{
|
||||
scores: {
|
||||
type: "array",
|
||||
elementType: "integer",
|
||||
elementMax: 255,
|
||||
maxLength: 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Optional Fields
|
||||
|
||||
Three forms, each valid pure JSON:
|
||||
|
||||
```typescript
|
||||
// 1. Primitive "?" suffix — one presence bit, then the base token.
|
||||
{ nickname: "-20?", id: "uuid?", lastSeen: "timestamp-ms?" }
|
||||
|
||||
// 2. Object-level _optional marker, with optional `_default` on absence.
|
||||
{
|
||||
profile: {
|
||||
_optional: true,
|
||||
_schema: { bio: "-200", avatar: "uuid" },
|
||||
_default: null
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Legacy `_bp` wrapper (still supported).
|
||||
{ nickname: { _bp: { schema: { type: "-20" }, value: "" } } }
|
||||
```
|
||||
|
||||
### Discriminated Unions (`_oneOf`)
|
||||
|
||||
```typescript
|
||||
{
|
||||
_oneOf: {
|
||||
tag: "kind",
|
||||
variants: {
|
||||
move: { x: 255, y: 255 },
|
||||
chat: { text: "-128" },
|
||||
spawn: { id: "uuid" }
|
||||
}
|
||||
}
|
||||
}
|
||||
// accepts { kind: "move", x, y } | { kind: "chat", text } | { kind: "spawn", id }
|
||||
```
|
||||
|
||||
Packs `ceil(log2(N))` bits of variant index + the matching variant's
|
||||
payload. The tag field is reconstructed on unpack.
|
||||
|
||||
### Tuples (Heterogeneous Arrays)
|
||||
|
||||
```typescript
|
||||
// Array literal with different per-index schemas.
|
||||
["uuid", 255, "-20"] // [string-uuid, u8, var-string]
|
||||
```
|
||||
|
||||
Each index is packed against its own schema; tuple length is fixed by
|
||||
the schema itself.
|
||||
|
||||
### Shared Sub-Schemas (`_defs` / `$ref`)
|
||||
|
||||
```typescript
|
||||
{
|
||||
_defs: {
|
||||
position: { x: 255, y: 255, z: 255 }
|
||||
},
|
||||
_schema: {
|
||||
from: { $ref: "position" },
|
||||
to: { $ref: "position" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`$ref` is resolved eagerly — the referenced sub-schema is inlined before
|
||||
packing. Cycles throw.
|
||||
|
||||
### Schema Version (`_version`)
|
||||
|
||||
```typescript
|
||||
{ _version: 3, _schema: { id: "uuid", name: "-40" } }
|
||||
```
|
||||
|
||||
Packs a 16-bit version tag in front of the payload. On unpack, a
|
||||
mismatch throws so callers can dispatch to a `VersionedSchema`
|
||||
migration.
|
||||
|
||||
### Conditional Fields (`_if` / `_then` / `_else`)
|
||||
|
||||
```typescript
|
||||
{
|
||||
weapon: 15,
|
||||
ammo: {
|
||||
_if: { field: "weapon", op: "!=", value: 0 },
|
||||
_then: 255,
|
||||
_else: null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The predicate is a pure JSON data structure — no callbacks. Supported
|
||||
ops: `==`, `!=`, `<`, `<=`, `>`, `>=`, `in` (value must be an array),
|
||||
`has` (field must be an array). The referenced field must appear
|
||||
earlier in its containing object so the predicate can evaluate at
|
||||
unpack time.
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Basic Pack/Unpack
|
||||
|
||||
```typescript
|
||||
import { BitPacker } from '@suaveplan/bitpacker';
|
||||
|
||||
const schema = {
|
||||
id: "uuid",
|
||||
name: "-30",
|
||||
score: 1000,
|
||||
active: true
|
||||
} as const;
|
||||
|
||||
const packer = new BitPacker(schema);
|
||||
const packed: Uint8Array = packer.pack({
|
||||
id: "123e4567-e89b-12d3-a456-426614174000",
|
||||
name: "Alice",
|
||||
score: 847,
|
||||
active: true
|
||||
});
|
||||
const unpacked = packer.unpack(packed);
|
||||
```
|
||||
|
||||
### Delta Compression
|
||||
|
||||
Send only changed fields after the first full state. Three modes:
|
||||
|
||||
```typescript
|
||||
// No delta (default) -- full state every time
|
||||
new BitPacker(schema);
|
||||
|
||||
// Presence-based -- bit flag per field, skips unchanged
|
||||
new BitPacker(schema, { delta: true });
|
||||
|
||||
// Field-level -- XOR diffs for numbers, granular tracking
|
||||
new BitPacker(schema, {
|
||||
delta: {
|
||||
mode: 'field-level',
|
||||
xorNumbers: true,
|
||||
maxFieldCount: 64,
|
||||
stateKey: 'player-42'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Delta usage:
|
||||
|
||||
```typescript
|
||||
const packer = new BitPacker(schema, { delta: { mode: 'field-level' } });
|
||||
|
||||
const state1 = { x: 100, y: 200, health: 100, name: "Alice" };
|
||||
packer.pack(state1); // Full state (baseline)
|
||||
|
||||
const state2 = { x: 150, y: 200, health: 100, name: "Alice" };
|
||||
const delta = packer.pack(state2); // Only x changed -- much smaller
|
||||
|
||||
const restored = packer.unpack(delta); // Full object restored
|
||||
```
|
||||
|
||||
### Checksums
|
||||
|
||||
```typescript
|
||||
const packer = new BitPacker(schema, {
|
||||
checksum: {
|
||||
algorithm: 'crc32', // 'crc32' | 'adler32' | 'xxhash32' | 'none'
|
||||
verifyOnUnpack: true,
|
||||
throwOnError: true
|
||||
}
|
||||
});
|
||||
// 4-byte checksum auto-appended on pack, auto-verified on unpack
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
```typescript
|
||||
const packer = new BitPacker(schema);
|
||||
|
||||
// Quick check
|
||||
if (packer.validate(data)) {
|
||||
packer.pack(data);
|
||||
}
|
||||
|
||||
// Detailed errors
|
||||
const errors = packer.getValidationErrors(data);
|
||||
// [{ path: "score", code: "OUT_OF_RANGE", message: "..." }]
|
||||
|
||||
// Schema matching -- find structural differences
|
||||
const result = packer.match(schema, data);
|
||||
// { additions: [...], subtractions: [...], mismatches: [...] }
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
```typescript
|
||||
new BitPacker(schema, {
|
||||
floatsAsStrings: false, // true: unpack fixed-point as "2.85" strings
|
||||
validateOnPack: true, // Validate data before packing
|
||||
validateOnUnpack: true, // Validate data after unpacking
|
||||
strictValidation: true, // Warnings become errors
|
||||
safetyLimits: {
|
||||
maxBufferSize: 1_048_576, // 1MB max packed size
|
||||
maxRecursionDepth: 100, // Nesting depth limit
|
||||
maxStringLength: 10_000, // Per-string char limit
|
||||
maxArrayLength: 10_000 // Per-array element limit
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
```typescript
|
||||
packer.on('beforePack', (data) => { /* inspect */ });
|
||||
packer.on('afterPack', (packed) => { /* log size */ });
|
||||
packer.on('packError', (error) => { /* handle */ });
|
||||
packer.on('beforeUnpack', (packed) => { /* inspect */ });
|
||||
packer.on('afterUnpack', (data) => { /* log */ });
|
||||
packer.on('unpackError', (error) => { /* handle */ });
|
||||
```
|
||||
|
||||
## Schema Design Guidelines
|
||||
|
||||
### 1. Choose the tightest type
|
||||
|
||||
Pick the smallest encoding that covers your value range:
|
||||
|
||||
| Value Range | Best Type | Bits |
|
||||
|-------------|-----------|------|
|
||||
| true/false | `true` | 1 |
|
||||
| 0-1 | `1` | 1 |
|
||||
| 0-255 | `255` | 8 |
|
||||
| 0-65535 | `65535` | 16 |
|
||||
| -100.00 to 100.00 | `"-100.2"` | ~15 |
|
||||
| Any 32-bit float | `"f32"` | 32 |
|
||||
| UUID | `"uuid"` | 128 |
|
||||
|
||||
### 2. Prefer fixed-point over floats
|
||||
|
||||
`"100.2"` uses ~14 bits. `"f32"` always uses 32 bits. If you know the range and precision, fixed-point wins.
|
||||
|
||||
### 3. Use variable-length strings
|
||||
|
||||
`"-20"` stores a 3-char name in ~27 bits (length prefix + 3 chars). `"20"` always uses ~140 bits (20 chars padded).
|
||||
|
||||
### 4. Use delta for real-time state
|
||||
|
||||
Game state, sensor data, or any frequently-updated structure benefits from field-level delta. Only changed fields are transmitted.
|
||||
|
||||
### 5. Size arrays conservatively
|
||||
|
||||
`"-10[255]"` allocates bits for the length prefix based on 10. If your array rarely exceeds 4 items, use `"-4[255]"` instead.
|
||||
|
||||
### 6. Use `as const` on schema objects
|
||||
|
||||
TypeScript const assertions enable full type inference on pack/unpack:
|
||||
|
||||
```typescript
|
||||
const schema = { name: "-20", score: 255 } as const;
|
||||
const packer = new BitPacker(schema);
|
||||
// packer.pack() expects { name: string; score: number }
|
||||
// packer.unpack() returns { name: string; score: number }
|
||||
```
|
||||
|
||||
## Complete Example: Game State Schema
|
||||
|
||||
```typescript
|
||||
import { BitPacker } from '@suaveplan/bitpacker';
|
||||
|
||||
const playerSchema = {
|
||||
id: "uuid",
|
||||
name: "-24",
|
||||
position: {
|
||||
x: "-5000.2",
|
||||
y: "-500.2",
|
||||
z: "-5000.2"
|
||||
},
|
||||
rotation: {
|
||||
yaw: "-180.1",
|
||||
pitch: "-90.1"
|
||||
},
|
||||
velocity: {
|
||||
x: "-50.1",
|
||||
y: "-50.1",
|
||||
z: "-50.1"
|
||||
},
|
||||
health: 100,
|
||||
armor: 100,
|
||||
weapon: 15,
|
||||
ammo: 999,
|
||||
crouching: true,
|
||||
sprinting: true,
|
||||
team: 3,
|
||||
inventory: "-8[255]",
|
||||
statusEffects: "-4[15]"
|
||||
} as const;
|
||||
|
||||
const packer = new BitPacker(playerSchema, {
|
||||
delta: { mode: 'field-level', xorNumbers: true, stateKey: 'player' },
|
||||
checksum: { algorithm: 'crc32', verifyOnUnpack: true }
|
||||
});
|
||||
```
|
||||
|
||||
### Registering a Custom Strategy
|
||||
|
||||
`BitPacker.registerStrategy(strategy)` slots a new `PackingStrategy` into the chain immediately before the catch-all `InvalidSchemaStrategy`. Use this for protocol-specific notations you don't want to fold into the core library:
|
||||
|
||||
```typescript
|
||||
import type { PackingStrategy } from '@suaveplan/bitpacker';
|
||||
|
||||
class RoundedPercentStrategy implements PackingStrategy {
|
||||
canHandle(schema) { return schema === 'pct'; }
|
||||
pack(data, _schema, ctx) { ctx.bitOps.packBits(Math.round(Number(data)), 7); }
|
||||
unpack(_schema, ctx) { return ctx.bitOps.unpackBits(7); }
|
||||
}
|
||||
|
||||
const packer = new BitPacker({ progress: 'pct' });
|
||||
packer.registerStrategy(new RoundedPercentStrategy());
|
||||
```
|
||||
|
||||
## Standalone Packers
|
||||
|
||||
These packers are exported from `@suaveplan/bitpacker` and are ALSO routed through the declarative schema notation (enum / datetime / varint / bit-array / map / set). You can still use them directly on `BitOperations` when you need to hand-compose a bit stream outside of `new BitPacker(schema)`.
|
||||
|
||||
### VarInt (Variable-Length Integers)
|
||||
|
||||
Protobuf-style encoding: small values use fewer bytes (1 byte for 0-127). Each byte uses 7 data bits + 1 continuation bit. This IS the dynamic-size encoding mechanism in BitPacker.
|
||||
|
||||
```typescript
|
||||
import { VarIntPacker, BitOperations } from '@suaveplan/bitpacker';
|
||||
|
||||
const bitOps = new BitOperations();
|
||||
const packer = new VarIntPacker(bitOps);
|
||||
|
||||
// Schema types: 'varint' | 'varint-signed' | 'varint-u32' | 'varint-u64'
|
||||
packer.pack(300, 'varint'); // 2 bytes (vs fixed 4 bytes)
|
||||
packer.pack(-42, 'varint-signed'); // ZigZag: small negatives stay compact
|
||||
packer.packBigInt(9007199254740993n, 'varint-u64'); // 64-bit BigInt
|
||||
|
||||
// Standalone (no BitOperations needed)
|
||||
const { bytes, byteCount } = VarIntPacker.encode(300); // 2 bytes
|
||||
const { value } = VarIntPacker.decode(bytes); // 300
|
||||
const needed = VarIntPacker.bytesNeeded(300); // 2
|
||||
```
|
||||
|
||||
Bytes needed per value range:
|
||||
|
||||
| Range | Bytes |
|
||||
|-------|-------|
|
||||
| 0-127 | 1 |
|
||||
| 128-16383 | 2 |
|
||||
| 16384-2097151 | 3 |
|
||||
| 2097152-268435455 | 4 |
|
||||
| 268435456+ | 5 |
|
||||
|
||||
### Enum
|
||||
|
||||
Automatic minimum-bit encoding. `ceil(log2(N))` bits for N values.
|
||||
|
||||
```typescript
|
||||
import { EnumPacker, BitOperations } from '@suaveplan/bitpacker';
|
||||
|
||||
const bitOps = new BitOperations();
|
||||
const packer = new EnumPacker(bitOps);
|
||||
|
||||
const schema = { type: 'enum' as const, values: ['red', 'green', 'blue'] };
|
||||
packer.pack('green', schema); // 2 bits (ceil(log2(3)))
|
||||
const value = packer.unpack(schema); // 'green'
|
||||
```
|
||||
|
||||
Config options: `includeValidityBit` (nullable enums), `allowUnknown` (forward-compatible).
|
||||
|
||||
### DateTime
|
||||
|
||||
Compact timestamp/timespan encoding with multiple precision levels.
|
||||
|
||||
```typescript
|
||||
import { DateTimePacker, BitOperations } from '@suaveplan/bitpacker';
|
||||
|
||||
const bitOps = new BitOperations();
|
||||
const packer = new DateTimePacker(bitOps, { epoch: 0 });
|
||||
|
||||
packer.pack(Date.now(), 'timestamp'); // 42-bit ms (~139 years)
|
||||
packer.pack(Date.now(), 'timestamp-ms'); // 53-bit ms (full JS safe int)
|
||||
packer.pack(Date.now(), 'timestamp-s'); // 32-bit seconds (Unix)
|
||||
packer.pack(60000, 'timespan'); // 42-bit ms duration
|
||||
packer.pack(60000, 'timespan-ms'); // 32-bit ms duration (~49.7 days)
|
||||
packer.pack(new Date(), 'date-only'); // 21-bit year/month/day
|
||||
```
|
||||
|
||||
### Map and Set
|
||||
|
||||
```typescript
|
||||
import { MapPacker, SetPacker, BitOperations } from '@suaveplan/bitpacker';
|
||||
|
||||
// Map<string, number>
|
||||
const mapPacker = new MapPacker(bitOps, { sortKeys: true });
|
||||
const mapSchema = { type: 'map', keySchema: '-20', valueSchema: 255, maxEntries: 10 };
|
||||
mapPacker.pack(new Map([['hp', 100], ['mp', 50]]), mapSchema);
|
||||
|
||||
// Set<number>
|
||||
const setPacker = new SetPacker(bitOps, { sortValues: true });
|
||||
const setSchema = { type: 'set', valueSchema: 255, maxElements: 10 };
|
||||
setPacker.pack(new Set([1, 2, 3]), setSchema);
|
||||
```
|
||||
|
||||
### BitArray
|
||||
|
||||
1 bit per boolean (8:1 compression vs byte-per-boolean). Fixed or variable length.
|
||||
|
||||
```typescript
|
||||
import { BitArrayPacker, BitOperations } from '@suaveplan/bitpacker';
|
||||
|
||||
const bitOps = new BitOperations();
|
||||
const packer = new BitArrayPacker(bitOps);
|
||||
|
||||
// Fixed-length: exactly 8 booleans = 8 bits
|
||||
packer.pack([true, false, true, true, false, false, true, false],
|
||||
{ type: 'bit-array', length: 8, variable: false });
|
||||
|
||||
// Variable-length: up to 32 booleans, length-prefixed
|
||||
packer.pack([true, false, true],
|
||||
{ type: 'bit-array', length: 32, variable: true });
|
||||
```
|
||||
|
||||
## Declarative vs. Imperative
|
||||
|
||||
The library has two API surfaces:
|
||||
|
||||
1. **Declarative schema** (`new BitPacker(schema)`) — the schema notation described above. Handles pack/unpack/validate/delta/checksum/custom types automatically. Use this for almost everything.
|
||||
2. **Standalone packers** (`VarIntPacker`, `EnumPacker`, `DateTimePacker`, `MapPacker`, `SetPacker`, `BitArrayPacker`) — operate directly on `BitOperations` for hand-composed bit streams outside of the schema engine.
|
||||
|
||||
The standalone packers are now also reachable as schema tokens (enum / datetime / varint / bit-array / map / set) and via `_customType` + `CustomTypeRegistry`. Drop to imperative only when you explicitly need to share a single `BitOperations` buffer across multiple packers.
|
||||
|
||||
## Packing Strategy Precedence
|
||||
|
||||
`BitPacker` tests strategies in this order and uses the first whose `canHandle` returns true:
|
||||
|
||||
1. Null (`null`)
|
||||
2. Optional (`_bp` wrapper)
|
||||
3. Array config (object with `type: "array"`)
|
||||
4. Array notation (`"-N[...]"` string)
|
||||
5. Boolean (`true`/`false`)
|
||||
6. BigInt array (`"bigint-u64[]"`)
|
||||
7. BigInt (`"bigint-u64"`)
|
||||
8. Blob (`"~"`)
|
||||
9. Enum token (`"enum:..."`)
|
||||
10. DateTime tokens (`"timestamp"`, `"timestamp-ms"`, `"timestamp-s"`, `"timespan"`, `"timespan-ms"`, `"date-only"`)
|
||||
11. VarInt tokens (`"varint"`, `"varint-signed"`, `"varint-u32"`, `"varint-u64"`)
|
||||
12. Bitset tokens (`"bitset-N"`, `"bitset--M"`)
|
||||
13. Map tokens (`"map<K,V>"`, `"map<K,V,N>"`)
|
||||
14. Set tokens (`"set<V,N>"`)
|
||||
15. String (remaining string values including UUID, float notation, typed arrays)
|
||||
16. Number (numeric values)
|
||||
17. Array literal (`[]`)
|
||||
18. Custom type marker (`{ _customType: "..." }`)
|
||||
19. Object (nested schema)
|
||||
20. User-registered strategies via `registerStrategy`
|
||||
21. Invalid schema (catch-all error)
|
||||
|
||||
## Constraints
|
||||
|
||||
- Always use `as const` on schema objects for type inference
|
||||
- Integer max values must be non-negative (range is 0..N for unsigned)
|
||||
- Signed types use `-` prefix: `"-100.2"` for signed fixed-point, `"-Nu8"` for variable typed arrays
|
||||
- Array string notation uses JSON for object elements: `'-3[{"id":255}]'`
|
||||
- Delta compression requires consistent schema between sender and receiver
|
||||
- Safety limits default to 1MB buffer, 100 depth, 10k string/array length
|
||||
- Schema objects are immutable after BitPacker construction
|
||||
|
||||
## Checklist
|
||||
|
||||
When designing a new BitPacker schema:
|
||||
|
||||
1. List every field and its value range
|
||||
2. Choose the tightest type for each field (fixed-point over float, variable over fixed string)
|
||||
3. Use `as const` assertion on the schema object
|
||||
4. Decide on delta mode based on update frequency (real-time: field-level, occasional: none)
|
||||
5. Add checksum if data crosses trust boundaries (network, storage)
|
||||
6. Set safety limits appropriate to your use case
|
||||
7. Validate with `getValidationErrors()` during development
|
||||
8. Test pack/unpack round-trip with edge case values (0, max, empty strings, full arrays)
|
||||
9. Measure packed size against alternatives (JSON, MessagePack) to confirm savings
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-bun-runtime",
|
||||
"version": "1.0.0",
|
||||
"description": "Bun v1.3.11 runtime APIs, CLI commands, bundler, test runner, database clients, and performance patterns for the Suaveplan monorepo."
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-dependency-management",
|
||||
"version": "1.0.0",
|
||||
"description": "Bun monorepo dependency management, version overrides, catalogs, workspace protocol, and tree-shaking"
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
---
|
||||
name: dependency-management
|
||||
description: Bun monorepo dependency management with version overrides, catalogs, workspace protocol, tree-shaking configuration, and React version conflict resolution
|
||||
---
|
||||
|
||||
# Dependency Management
|
||||
|
||||
Use this skill when adding dependencies, resolving version conflicts, configuring tree-shaking, or troubleshooting build issues related to package versions in the Suaveplan monorepo.
|
||||
|
||||
## Bun Monorepo Dependency Management
|
||||
|
||||
### Critical Rules
|
||||
|
||||
- **Always use exact versions** in `overrides` and `catalog` (not ranges like `^19.0.0`)
|
||||
- Use Bun's `overrides` field (NOT `resolutions` -- that's npm/yarn)
|
||||
- Bun only supports **top-level overrides** (no nested overrides)
|
||||
|
||||
### Example Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"overrides": {
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
},
|
||||
"workspaces": {
|
||||
"catalog": {
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Nuclear Clean (Required After Override Changes)
|
||||
|
||||
After changing `overrides` or `catalog`, do a complete nuclear clean:
|
||||
|
||||
```bash
|
||||
rm -rf node_modules bun.lockb
|
||||
find . -type d -name "node_modules" -exec rm -rf {} +
|
||||
bun install --force
|
||||
```
|
||||
|
||||
### Verify Single Version
|
||||
|
||||
```bash
|
||||
find . -path "*/node_modules/react/package.json" -exec grep "version" {} \; | sort -u
|
||||
# Should show only ONE version
|
||||
```
|
||||
|
||||
## Check Framework Dependencies First
|
||||
|
||||
Before using any library:
|
||||
|
||||
1. Review `package.json` for existing dependencies
|
||||
2. Follow existing import patterns in the package
|
||||
3. Use workspace protocol (`workspace:*`) for internal deps
|
||||
4. Use Bun catalogs for centralized version management
|
||||
|
||||
## Workspace Protocol
|
||||
|
||||
Internal package dependencies use `workspace:*`:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@suaveplan/core": "workspace:*",
|
||||
"@suaveplan/types": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Bun Workspaces
|
||||
|
||||
Defined in root `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspaces": ["packages/*", "apps/*"]
|
||||
}
|
||||
```
|
||||
|
||||
## Bun Catalogs
|
||||
|
||||
Centralized dependency versions in root `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"catalogs": {
|
||||
"react": "^18.2.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vitest": "^1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use in packages:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"react": "catalog:react",
|
||||
"typescript": "catalog:build"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tree-Shaking Requirement
|
||||
|
||||
**All library packages MUST declare side effects.**
|
||||
|
||||
### Pure Libraries
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@suaveplan/package-name",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Packages with CSS/Styles
|
||||
|
||||
```json
|
||||
{
|
||||
"sideEffects": ["**/*.css", "**/*.scss"]
|
||||
}
|
||||
```
|
||||
|
||||
### Verification Checklist
|
||||
|
||||
Verify no actual side effects in entry points:
|
||||
|
||||
- No top-level function calls
|
||||
- No global object mutations
|
||||
- No immediate IIFEs
|
||||
- Only `export` statements allowed
|
||||
|
||||
Required for optimal bundle sizes (30-50% reduction).
|
||||
|
||||
## Troubleshooting: React Version Conflicts
|
||||
|
||||
### Symptoms
|
||||
|
||||
- `Error: Minified React error #31`
|
||||
- `TypeError: Cannot read properties of null (reading 'useContext')`
|
||||
- Build fails during static page generation
|
||||
|
||||
### Root Cause
|
||||
|
||||
Multiple React versions in monorepo.
|
||||
|
||||
### Solution
|
||||
|
||||
```bash
|
||||
# 1. Add exact version overrides to root package.json
|
||||
# {
|
||||
# "overrides": {
|
||||
# "react": "19.0.0",
|
||||
# "react-dom": "19.0.0",
|
||||
# "@types/react": "19.0.0",
|
||||
# "@types/react-dom": "19.0.0"
|
||||
# }
|
||||
# }
|
||||
|
||||
# 2. Update catalog to match (exact versions, no ^)
|
||||
# {
|
||||
# "workspaces": {
|
||||
# "catalog": {
|
||||
# "react": "19.0.0",
|
||||
# "react-dom": "19.0.0"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
# 3. Nuclear clean install (REQUIRED!)
|
||||
rm -rf node_modules bun.lockb
|
||||
find . -type d -name "node_modules" -exec rm -rf {} +
|
||||
bun install --force
|
||||
|
||||
# 4. Verify single React version
|
||||
find . -path "*/node_modules/react/package.json" -exec grep version {} \; | sort -u
|
||||
# Should show only ONE version
|
||||
```
|
||||
|
||||
### Key Points
|
||||
|
||||
- Use Bun `overrides` (NOT `resolutions`)
|
||||
- Use **exact versions** (no `^` or `~`)
|
||||
- Complete clean install is REQUIRED
|
||||
- Verify workspace catalog matches overrides
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-doc-coauthoring",
|
||||
"version": "1.0.0",
|
||||
"description": "Structured workflow for co-authoring Suaveplan package documentation"
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
---
|
||||
name: doc-coauthoring
|
||||
description: Co-author module documentation for the Suaveplan monorepo following the module-docs template with simple + advanced examples, 200-word minimum, and 90%+ documentation coverage
|
||||
---
|
||||
|
||||
# Documentation Co-Authoring for Suaveplan
|
||||
|
||||
Write and maintain co-located `.md` documentation files for every implementation module in the Suaveplan monorepo. Documentation is a first-class artifact -- every `.ts` or `.tsx` implementation file must have a corresponding `.md` file in the same directory. This skill follows a 3-stage workflow: context gathering, iterative refinement, and reader testing.
|
||||
|
||||
## Documentation Architecture
|
||||
|
||||
### File Mapping
|
||||
|
||||
```text
|
||||
MyClass.ts -> MyClass.md (required)
|
||||
my-hook.ts -> my-hook.md (required)
|
||||
component.tsx -> component.md (required)
|
||||
types.ts -> types.md (required -- for shared type files)
|
||||
utils.ts -> utils.md (required)
|
||||
index.ts -> (not required -- barrel exports only)
|
||||
```
|
||||
|
||||
### Exclusions (No `.md` Required)
|
||||
|
||||
- `.test.ts` / `.test.tsx` -- test files
|
||||
- `.spec.ts` -- Playwright E2E files
|
||||
- `.types.ts` -- types files within a component directory (shared type files in `types/` DO need docs)
|
||||
- `.stories.tsx` -- Storybook stories
|
||||
- `constants/index.ts` -- simple constant exports
|
||||
|
||||
### Directory Hierarchy
|
||||
|
||||
Documentation follows a three-level hierarchy with each level linking down:
|
||||
|
||||
```text
|
||||
packages/<category>/<package>/
|
||||
README.md # Level 1: Package overview
|
||||
src/
|
||||
<category>/
|
||||
README.md # Level 2: Category index
|
||||
<feature>/
|
||||
<feature>.md # Level 3: Module documentation
|
||||
<feature>.ts
|
||||
```
|
||||
|
||||
**Level 1 (Package README)**: Lists all categories with descriptions, quick start, installation.
|
||||
**Level 2 (Category README)**: Lists all feature modules with one-line descriptions and links.
|
||||
**Level 3 (Module `.md`)**: Full documentation per the template below.
|
||||
|
||||
## Template
|
||||
|
||||
All module documentation follows `.claude/templates/module-docs.md`. The minimum requirements are:
|
||||
|
||||
### Required Sections
|
||||
|
||||
1. **Purpose** -- What the module does and why it exists (minimum 2 sentences)
|
||||
2. **Features** -- Bullet list of capabilities
|
||||
3. **Usage: Basic Example** -- Simple import and usage (the "happy path")
|
||||
4. **Usage: Advanced Example** -- Composition, configuration, edge cases
|
||||
5. **API Reference** -- Every public export documented with parameters and return types
|
||||
6. **Implementation Notes** -- Design decisions, performance considerations, known limitations
|
||||
|
||||
### Content Requirements
|
||||
|
||||
- **Minimum 200 words** of content (excluding code blocks)
|
||||
- **Both simple AND advanced examples** are mandatory in every doc
|
||||
- **Code examples must compile** -- use real types and imports from the package
|
||||
- **No marketing language** -- state capabilities honestly
|
||||
- **No placeholder text** -- every section must contain real content
|
||||
- **Update when code changes** -- documentation must stay in sync
|
||||
|
||||
## 3-Stage Workflow
|
||||
|
||||
### Stage 1: Context Gathering
|
||||
|
||||
Before writing documentation, gather full context:
|
||||
|
||||
1. **Read the implementation file** -- understand every public export
|
||||
2. **Read the types file** -- understand all interfaces and type definitions
|
||||
3. **Read the test file** -- understand expected behavior and edge cases
|
||||
4. **Read related modules** -- understand how this module fits in the package
|
||||
5. **Read the category README** -- understand the broader context
|
||||
|
||||
```bash
|
||||
# Example: documenting a crypto module
|
||||
# Read the implementation
|
||||
cat packages/universal/crypto/src/symmetric/encryption/encryption.ts
|
||||
# Read the types
|
||||
cat packages/universal/crypto/src/symmetric/encryption/encryption.types.ts
|
||||
# Read the tests (understand behavior)
|
||||
cat packages/universal/crypto/src/symmetric/encryption/encryption.test.ts
|
||||
# Read the category README
|
||||
cat packages/universal/crypto/src/symmetric/README.md
|
||||
```
|
||||
|
||||
### Stage 2: Iterative Refinement
|
||||
|
||||
Write the documentation in passes:
|
||||
|
||||
**Pass 1 -- Structure**: Create all section headings and fill in Purpose + Features.
|
||||
|
||||
**Pass 2 -- Examples**: Write the Basic Example (simplest correct usage) and Advanced Example (realistic production usage showing composition, error handling, and configuration).
|
||||
|
||||
**Pass 3 -- API Reference**: Document every public export:
|
||||
|
||||
- Functions: parameters with types, return type, throws, example
|
||||
- Classes: constructor, methods, properties
|
||||
- Types/Interfaces: every field with description
|
||||
- Constants: value, purpose, usage context
|
||||
|
||||
**Pass 4 -- Notes**: Add Implementation Notes covering:
|
||||
|
||||
- Why this approach was chosen over alternatives
|
||||
- Performance characteristics (time/space complexity)
|
||||
- Known limitations and workarounds
|
||||
- Platform compatibility notes
|
||||
|
||||
### Stage 3: Reader Testing
|
||||
|
||||
Validate the documentation by asking:
|
||||
|
||||
1. **Can a new developer use this module after reading only the Basic Example?**
|
||||
2. **Can a senior developer configure advanced use cases from the Advanced Example?**
|
||||
3. **Are all public APIs documented so a developer never needs to read source code?**
|
||||
4. **Do the code examples actually compile with the correct imports?**
|
||||
5. **Is the word count at least 200 words (excluding code blocks)?**
|
||||
|
||||
## Example: Complete Module Documentation
|
||||
|
||||
```markdown
|
||||
# Encryption
|
||||
|
||||
## Purpose
|
||||
|
||||
Provides symmetric encryption and decryption using AES-256-GCM with authenticated
|
||||
encryption. Wraps the Web Crypto API to provide a consistent interface across Node.js
|
||||
and browser environments with automatic IV generation and authentication tag handling.
|
||||
|
||||
## Features
|
||||
|
||||
- AES-256-GCM authenticated encryption
|
||||
- Automatic random IV generation per encryption call
|
||||
- AEAD (Authenticated Encryption with Associated Data) support
|
||||
- Streaming encryption for large payloads
|
||||
- Cross-platform (Node.js, browsers, edge runtimes)
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Example
|
||||
|
||||
\`\`\`typescript
|
||||
import { encrypt, decrypt, generateKey } from '@suaveplan/crypto/symmetric/encryption/encryption';
|
||||
|
||||
const key = await generateKey();
|
||||
const ciphertext = await encrypt(key, 'Hello, world!');
|
||||
const plaintext = await decrypt(key, ciphertext);
|
||||
// plaintext === 'Hello, world!'
|
||||
\`\`\`
|
||||
|
||||
### Advanced Example
|
||||
|
||||
\`\`\`typescript
|
||||
import {
|
||||
encrypt,
|
||||
decrypt,
|
||||
generateKey,
|
||||
type EncryptionOptions,
|
||||
} from '@suaveplan/crypto/symmetric/encryption/encryption';
|
||||
|
||||
const key = await generateKey();
|
||||
|
||||
// Encrypt with associated data for AEAD
|
||||
const options: EncryptionOptions = {
|
||||
associatedData: new TextEncoder().encode('metadata-v1'),
|
||||
outputFormat: 'base64',
|
||||
};
|
||||
|
||||
const ciphertext = await encrypt(key, largePayload, options);
|
||||
|
||||
// Decryption must provide the same associated data
|
||||
const plaintext = await decrypt(key, ciphertext, {
|
||||
associatedData: options.associatedData,
|
||||
inputFormat: 'base64',
|
||||
});
|
||||
\`\`\`
|
||||
|
||||
## API Reference
|
||||
|
||||
### generateKey(): Promise<CryptoKey>
|
||||
|
||||
Generates a new AES-256-GCM key using the Web Crypto API.
|
||||
|
||||
**Returns:** Promise resolving to a CryptoKey suitable for encrypt/decrypt.
|
||||
|
||||
### encrypt(key: CryptoKey, data: string | Uint8Array, options?: EncryptionOptions): Promise<Uint8Array>
|
||||
|
||||
Encrypts data using AES-256-GCM.
|
||||
|
||||
**Parameters:**
|
||||
- `key` -- AES-256-GCM CryptoKey from generateKey()
|
||||
- `data` -- Plaintext string or binary data
|
||||
- `options` -- Optional EncryptionOptions
|
||||
|
||||
**Returns:** Ciphertext with prepended IV (12 bytes) and appended auth tag (16 bytes).
|
||||
|
||||
**Throws:** `CryptoError` if key is invalid or encryption fails.
|
||||
|
||||
### decrypt(key: CryptoKey, ciphertext: Uint8Array, options?: DecryptionOptions): Promise<string | Uint8Array>
|
||||
|
||||
Decrypts AES-256-GCM ciphertext.
|
||||
|
||||
**Parameters:**
|
||||
- `key` -- Same CryptoKey used for encryption
|
||||
- `ciphertext` -- Output from encrypt()
|
||||
- `options` -- Optional DecryptionOptions
|
||||
|
||||
**Returns:** Original plaintext.
|
||||
|
||||
**Throws:** `CryptoError` if authentication fails (tampered data) or key mismatch.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Design Decisions
|
||||
|
||||
- **AES-256-GCM chosen over AES-CBC** because GCM provides authentication
|
||||
(integrity + confidentiality) in a single operation, preventing padding oracle attacks.
|
||||
- **IV prepended to ciphertext** so callers do not need to manage IV storage separately.
|
||||
The 12-byte IV is randomly generated per call, making nonce reuse statistically impossible.
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- **Time Complexity**: O(n) where n is plaintext length
|
||||
- **Memory**: Entire plaintext must fit in memory (use streaming API for large files)
|
||||
- **Web Crypto API**: Hardware-accelerated on platforms with AES-NI support
|
||||
|
||||
### Known Limitations
|
||||
|
||||
- Maximum plaintext size of 64 GiB per encryption call (GCM specification limit)
|
||||
- Streaming API requires ReadableStream support (not available in all edge runtimes)
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Check documentation coverage before marking work complete:
|
||||
|
||||
```bash
|
||||
# Check documentation coverage for a specific package
|
||||
bun run docs:check --filter=@suaveplan/<package-name>
|
||||
|
||||
# Target: 90%+ documentation coverage
|
||||
```
|
||||
|
||||
## Updating Existing Documentation
|
||||
|
||||
When implementation code changes:
|
||||
|
||||
1. Read the updated implementation file
|
||||
2. Identify what changed (new exports, changed signatures, removed APIs)
|
||||
3. Update the corresponding `.md` file to reflect changes
|
||||
4. Verify examples still compile with the new API
|
||||
5. Add a Changelog entry if the change is significant
|
||||
|
||||
## Constraints
|
||||
|
||||
- **200-word minimum** per documentation file (excluding code blocks)
|
||||
- **Simple + Advanced examples** mandatory in every doc
|
||||
- **90%+ documentation coverage** per package
|
||||
- **Co-located files** -- `MyClass.ts` and `MyClass.md` must be in the same directory
|
||||
- **No placeholder text** -- every section must have real content
|
||||
- **Code examples must use real imports** -- `@suaveplan/<package>` paths
|
||||
- **Follow `.claude/templates/module-docs.md`** template structure
|
||||
- **Update docs when code changes** -- stale documentation is a defect
|
||||
- **Link between levels** -- README -> Category README -> Module docs
|
||||
- Template exclusions: `.test.ts`, `.spec.ts`, `.types.ts`, `.stories.tsx`
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-frontend-design",
|
||||
"version": "1.0.0",
|
||||
"description": "Suaveplan-specific frontend design skill for React + Tailwind + shadcn/ui"
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces for the Suaveplan monorepo. All output MUST use **React 19 + TypeScript + Tailwind CSS + shadcn/ui** and follow Suaveplan CLAUDE.md conventions.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Required Stack
|
||||
|
||||
All frontend code MUST use:
|
||||
|
||||
- **React 19** with TypeScript generics and strict types
|
||||
- **Tailwind CSS** for styling (via utility classes, no inline styles)
|
||||
- **shadcn/ui** components from `@suaveplan/react-ui` as the base component library
|
||||
- **`cn()` utility** from `@suaveplan/react-ui` for conditional class merging (clsx + tailwind-merge)
|
||||
- **CSS variables** for theming (defined in Tailwind config or `:root`)
|
||||
- **Motion** (framer-motion) for React animations when available; CSS transitions/animations for simpler effects
|
||||
|
||||
### Available Suaveplan Libraries
|
||||
|
||||
Use these internal packages instead of external alternatives:
|
||||
|
||||
| Need | Use | Import |
|
||||
|------|-----|--------|
|
||||
| UI primitives (Button, Dialog, Card, etc.) | `@suaveplan/react-ui` | `import { Button } from '@suaveplan/react-ui'` |
|
||||
| Class merging | `cn()` from `@suaveplan/react-ui` | `import { cn } from '@suaveplan/react-ui'` |
|
||||
| Forms + validation | `@suaveplan/react-forms` | `import { Form } from '@suaveplan/react-forms'` |
|
||||
| Data tables | `@suaveplan/react-data-table` | `import { DataTable } from '@suaveplan/react-data-table'` |
|
||||
| Toast / notifications | `@suaveplan/react-notifications` | `import { toast } from '@suaveplan/react-notifications'` |
|
||||
| Auth components | `@suaveplan/react-auth` | `import { LoginForm } from '@suaveplan/react-auth'` |
|
||||
| Timeline / Gantt / Calendar | `@suaveplan/react-timeline` | `import { Timeline, Gantt, Calendar } from '@suaveplan/react-timeline'` |
|
||||
| Media players | `@suaveplan/react-media` | `import { MediaPlayer } from '@suaveplan/react-media'` |
|
||||
| i18n / translation | `@suaveplan/react-translate` | `import { useTranslation } from '@suaveplan/react-translate'` |
|
||||
| Consent overlays | `@suaveplan/react-consent-overlay` | `import { ConsentOverlay } from '@suaveplan/react-consent-overlay'` |
|
||||
| Drive sharing UI | `@suaveplan/drive-react-sharing` | `import { ShareDialog } from '@suaveplan/drive-react-sharing'` |
|
||||
| Email templates | `@suaveplan/email-templates` | `import { WelcomeEmail } from '@suaveplan/email-templates'` |
|
||||
|
||||
**NEVER** install or import external UI libraries (Material UI, Chakra, Ant Design, Mantine, Radix directly, etc.) when a Suaveplan equivalent exists. Import shadcn/ui primitives from `@suaveplan/react-ui`.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Must use React + Tailwind + shadcn/ui. Must be tree-shakeable. Must follow Suaveplan file structure conventions.
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code that is:
|
||||
|
||||
- Production-grade, functional, and type-safe
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
- Tree-shakeable with proper exports
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
|
||||
- **Typography**: Use Tailwind's font system with custom font families defined in `tailwind.config`. Avoid generic system fonts. Pair a distinctive display font with a refined body font using `font-display` and `font-body` custom utilities. Load via `@fontsource` packages or Google Fonts with `next/font`.
|
||||
- **Color & Theme**: Use Tailwind CSS variables and shadcn/ui theming. Define colors in HSL format for shadcn compatibility. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. Use `dark:` variants for dark mode support.
|
||||
- **Motion**: Use Motion (framer-motion) for component animations — `AnimatePresence`, `motion.div`, staggered children via `variants`. Use Tailwind's `transition-*`, `animate-*` utilities for simpler effects. Focus on high-impact moments: one well-orchestrated page load with staggered reveals creates more delight than scattered micro-interactions.
|
||||
- **Spatial Composition**: Use Tailwind's grid/flex utilities creatively. Unexpected layouts with `grid-cols-[custom]`, asymmetric `gap-*`, overlapping with negative margins or `absolute` positioning. Generous `space-*` or controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere with Tailwind gradients (`bg-gradient-to-*`), backdrop blur (`backdrop-blur-*`), shadows (`shadow-*`), and custom CSS for noise textures, geometric patterns, or grain overlays. Use `before:` and `after:` pseudo-elements via Tailwind for decorative effects.
|
||||
|
||||
### shadcn/ui Integration
|
||||
|
||||
- Build on top of shadcn/ui primitives — extend them, don't replace them
|
||||
- Use `cn()` from `@suaveplan/react-ui` for all conditional class composition
|
||||
- Override shadcn/ui defaults with Tailwind classes, not inline styles
|
||||
- Respect the `data-*` attributes shadcn uses for state styling
|
||||
- Example pattern:
|
||||
|
||||
```tsx
|
||||
import { Button } from '@suaveplan/react-ui';
|
||||
import { cn } from '@suaveplan/react-ui';
|
||||
|
||||
interface ActionButtonProps {
|
||||
variant?: 'primary' | 'danger';
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ActionButton({ variant = 'primary', className, children }: ActionButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
'relative overflow-hidden transition-all duration-300',
|
||||
variant === 'primary' && 'bg-emerald-600 hover:bg-emerald-700 hover:shadow-emerald-500/25 hover:shadow-lg',
|
||||
variant === 'danger' && 'bg-rose-600 hover:bg-rose-700 hover:shadow-rose-500/25 hover:shadow-lg',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
NEVER use:
|
||||
|
||||
- Generic AI-generated aesthetics (overused font families like Inter/Roboto/Arial, cliched purple gradients on white, predictable layouts)
|
||||
- Inline styles — use Tailwind classes exclusively
|
||||
- External UI libraries when a `@suaveplan/*` equivalent exists
|
||||
- `vi.mock()` or `jest.mock()` in tests — use real implementations
|
||||
- Files over 250 lines (code) or 500 lines (tests)
|
||||
- `tsc` directly — use `bun run build` (Vite/tsup)
|
||||
- TODO comments or placeholder implementations
|
||||
|
||||
NEVER converge on common choices (Space Grotesk, etc.) across generations. Vary between light and dark themes, different fonts, different aesthetics.
|
||||
|
||||
## Suaveplan Code Standards
|
||||
|
||||
All generated frontend code MUST follow:
|
||||
|
||||
1. **Max 250 lines per file** — split components into smaller focused modules
|
||||
2. **TypeScript generics** — components should accept generic type parameters where applicable
|
||||
3. **Each component gets its own folder** with: `.tsx`, `.test.tsx`, `.stories.tsx`, `.types.ts` (if needed), `.md`
|
||||
4. **Storybook stories required** — default story, playground with controls, key variant stories
|
||||
5. **Playwright `.spec.ts`** for E2E testing of interactive components
|
||||
6. **Tree-shakeable** — use named exports, no side effects, declare `sideEffects: false`
|
||||
7. **No re-exports** — import directly from source packages
|
||||
8. **`workspace:*`** for internal dependencies
|
||||
9. **CSS classes use `sp-` prefix** for custom component-specific styles (matches existing convention)
|
||||
10. **Accessibility** — ARIA roles, keyboard navigation, focus management, screen reader support
|
||||
|
||||
### File Structure Pattern
|
||||
|
||||
```text
|
||||
components/
|
||||
my-component/
|
||||
my-component.tsx # Implementation (max 250 lines)
|
||||
my-component.test.tsx # Vitest unit tests (max 500 lines)
|
||||
my-component.stories.tsx # Storybook stories
|
||||
my-component.types.ts # Type definitions
|
||||
my-component.md # Documentation with simple + advanced examples
|
||||
```
|
||||
|
||||
### Documentation (.md) Requirements
|
||||
|
||||
Every `.md` file must include:
|
||||
|
||||
- Purpose description (200+ words)
|
||||
- **Simple Example**: basic usage, getting started
|
||||
- **Advanced Example**: generics, composition, edge cases
|
||||
- API reference for all public exports
|
||||
- Design decisions and implementation notes
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
When building a frontend component/page:
|
||||
|
||||
1. Design direction chosen and documented
|
||||
2. Component file structure created (`.tsx`, `.test.tsx`, `.stories.tsx`, `.types.ts`, `.md`)
|
||||
3. TypeScript types/interfaces defined with generics where applicable
|
||||
4. Component implemented using React + Tailwind + shadcn/ui
|
||||
5. `cn()` used for all conditional class composition
|
||||
6. Accessibility attributes applied (ARIA, keyboard, focus)
|
||||
7. Storybook stories written (default, playground, variants)
|
||||
8. Unit tests written with Vitest (95%+ coverage target)
|
||||
9. Playwright E2E spec written for interactive behaviors
|
||||
10. Documentation written with simple + advanced examples
|
||||
11. File sizes verified (code <= 250 lines, tests <= 500 lines)
|
||||
12. Lint clean: `bun run lint --filter=@suaveplan/<package>`
|
||||
13. Types clean: `bun run typecheck --filter=@suaveplan/<package>`
|
||||
14. Tests passing: `bun run test --filter=@suaveplan/<package>`
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Commit fully to a distinctive vision while respecting the Suaveplan stack and conventions.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-mcp-builder",
|
||||
"version": "1.0.0",
|
||||
"description": "Create MCP servers exposing Suaveplan plugin functionality"
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
---
|
||||
name: mcp-builder
|
||||
description: Build MCP (Model Context Protocol) servers that expose Suaveplan plugin functionality using TypeScript, Hono, SurrealDB, and the definePlugin() architecture
|
||||
---
|
||||
|
||||
# MCP Builder for Suaveplan
|
||||
|
||||
Build MCP servers that expose Suaveplan's plugin architecture as tool endpoints for AI agents. This skill bridges the Suaveplan API plugin system with the Model Context Protocol, allowing plugins to serve both HTTP routes and MCP tool interfaces from the same codebase.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Suaveplan's server-side plugin system (`@suaveplan/api`) provides the foundation. An MCP server wraps one or more plugins, exposing their operations as MCP tools, resources, and prompts.
|
||||
|
||||
```text
|
||||
Suaveplan Plugin (definePlugin)
|
||||
|
|
||||
+-- Hono routes (HTTP API)
|
||||
|
|
||||
+-- MCP Server adapter
|
||||
|
|
||||
+-- Tools (operations)
|
||||
+-- Resources (data endpoints)
|
||||
+-- Prompts (templated interactions)
|
||||
```
|
||||
|
||||
### Core Technologies
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Plugin Framework | `definePlugin()` from `@suaveplan/api` |
|
||||
| HTTP Router | Hono |
|
||||
| Database | SurrealDB with `.surql` migration files |
|
||||
| Schema Validation | Zod |
|
||||
| Runtime | Bun |
|
||||
| MCP Protocol | `@modelcontextprotocol/sdk` |
|
||||
|
||||
## Plugin Lifecycle
|
||||
|
||||
Every Suaveplan plugin follows this lifecycle:
|
||||
|
||||
```text
|
||||
onRegister -> onInit -> onReady -> (running) -> onClose
|
||||
```
|
||||
|
||||
- **onRegister**: Plugin is registered in the plugin registry. Validate configuration.
|
||||
- **onInit**: Initialize resources, connect to databases, run migrations.
|
||||
- **onReady**: Mount Hono routes, start serving. The MCP adapter binds here.
|
||||
- **onClose**: Cleanup resources, close connections.
|
||||
|
||||
## File Structure
|
||||
|
||||
An MCP-enabled Suaveplan plugin follows this layout:
|
||||
|
||||
```text
|
||||
packages/server/api-plugin-<name>/
|
||||
src/
|
||||
plugin.ts # Plugin definition with definePlugin()
|
||||
plugin.test.ts # Plugin lifecycle tests
|
||||
plugin.types.ts # Configuration and type definitions
|
||||
plugin.md # Plugin documentation
|
||||
mcp/
|
||||
mcp-adapter.ts # MCP server adapter
|
||||
mcp-adapter.test.ts # MCP adapter tests
|
||||
mcp-adapter.types.ts # MCP-specific types
|
||||
mcp-adapter.md # MCP adapter documentation
|
||||
tools/
|
||||
<tool-name>.ts # Individual tool implementations
|
||||
<tool-name>.test.ts # Tool tests
|
||||
<tool-name>.md # Tool documentation
|
||||
resources/
|
||||
<resource-name>.ts # Resource implementations
|
||||
prompts/
|
||||
<prompt-name>.ts # Prompt templates
|
||||
routes/
|
||||
<route>.ts # Hono route handlers
|
||||
<route>.test.ts # Route tests
|
||||
<route>.md # Route documentation
|
||||
surql/
|
||||
001-initial-schema.surql # SurrealDB migration files
|
||||
002-add-indexes.surql # Sequential migrations
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
```
|
||||
|
||||
## Creating a Plugin with definePlugin()
|
||||
|
||||
### Basic Plugin Definition
|
||||
|
||||
```typescript
|
||||
// src/plugin.ts
|
||||
import { definePlugin } from '@suaveplan/api';
|
||||
import z from 'zod';
|
||||
import type { MyPluginConfig } from './plugin.types';
|
||||
|
||||
const ConfigSchema = z.object({
|
||||
apiKey: z.string().min(1),
|
||||
maxRetries: z.number().int().positive().default(3),
|
||||
enableMcp: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const myPlugin = definePlugin<MyPluginConfig>({
|
||||
name: 'my-plugin',
|
||||
version: '1.0.0',
|
||||
description: 'Description of what this plugin does',
|
||||
dependencies: ['database'], // Required plugins
|
||||
optionalDependencies: ['auth'], // Optional plugins
|
||||
migrationsPath: './surql', // SurrealDB migration directory
|
||||
tags: ['mcp', 'custom'],
|
||||
|
||||
configSchema: {
|
||||
schema: ConfigSchema,
|
||||
defaults: {
|
||||
apiKey: '',
|
||||
maxRetries: 3,
|
||||
enableMcp: true,
|
||||
},
|
||||
},
|
||||
|
||||
async onInit(ctx) {
|
||||
// Run after dependencies are initialized
|
||||
// Access config: ctx.config
|
||||
// Access database: ctx.db
|
||||
// Run migrations automatically from migrationsPath
|
||||
},
|
||||
|
||||
async onReady(ctx) {
|
||||
// Mount Hono routes
|
||||
ctx.app.route('/my-plugin', createRoutes(ctx));
|
||||
|
||||
// Initialize MCP adapter if enabled
|
||||
if (ctx.config.enableMcp) {
|
||||
const mcpServer = createMcpAdapter(ctx);
|
||||
ctx.app.route('/mcp/my-plugin', mcpServer);
|
||||
}
|
||||
},
|
||||
|
||||
async onClose(ctx) {
|
||||
// Cleanup resources
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Types Definition
|
||||
|
||||
```typescript
|
||||
// src/plugin.types.ts
|
||||
import type { z } from 'zod';
|
||||
|
||||
export interface MyPluginConfig {
|
||||
apiKey: string;
|
||||
maxRetries: number;
|
||||
enableMcp: boolean;
|
||||
}
|
||||
|
||||
export interface MyPluginContext {
|
||||
config: MyPluginConfig;
|
||||
db: SurrealDB;
|
||||
app: Hono;
|
||||
}
|
||||
```
|
||||
|
||||
## MCP Adapter Pattern
|
||||
|
||||
### Wrapping a Plugin as an MCP Server
|
||||
|
||||
```typescript
|
||||
// src/mcp/mcp-adapter.ts
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import z from 'zod';
|
||||
import type { MyPluginContext } from '../plugin.types';
|
||||
|
||||
export function createMcpAdapter(ctx: MyPluginContext): McpServer {
|
||||
const server = new McpServer({
|
||||
name: 'suaveplan-my-plugin',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
// Register tools
|
||||
server.tool(
|
||||
'create_item',
|
||||
'Create a new item in the system',
|
||||
{
|
||||
name: z.string().describe('Item name'),
|
||||
category: z.enum(['a', 'b', 'c']).describe('Item category'),
|
||||
metadata: z.record(z.string()).optional().describe('Optional metadata'),
|
||||
},
|
||||
async ({ name, category, metadata }) => {
|
||||
const result = await ctx.db.create('item', { name, category, metadata });
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'query_items',
|
||||
'Search items with filters',
|
||||
{
|
||||
category: z.enum(['a', 'b', 'c']).optional(),
|
||||
limit: z.number().int().positive().default(10),
|
||||
},
|
||||
async ({ category, limit }) => {
|
||||
const query = category
|
||||
? `SELECT * FROM item WHERE category = $category LIMIT $limit`
|
||||
: `SELECT * FROM item LIMIT $limit`;
|
||||
const results = await ctx.db.query(query, { category, limit });
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Register resources
|
||||
server.resource(
|
||||
'item_schema',
|
||||
'item://schema',
|
||||
async (uri) => ({
|
||||
contents: [{
|
||||
uri: uri.href,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify({
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
category: { type: 'string', enum: ['a', 'b', 'c'] },
|
||||
},
|
||||
}),
|
||||
}],
|
||||
})
|
||||
);
|
||||
|
||||
// Register prompts
|
||||
server.prompt(
|
||||
'summarize_items',
|
||||
'Generate a summary of items by category',
|
||||
{ category: z.string().optional() },
|
||||
({ category }) => ({
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: category
|
||||
? `Summarize all items in category "${category}"`
|
||||
: 'Summarize all items across all categories',
|
||||
},
|
||||
}],
|
||||
})
|
||||
);
|
||||
|
||||
return server;
|
||||
}
|
||||
```
|
||||
|
||||
## SurrealDB Migrations
|
||||
|
||||
Migration files live in `surql/` and are executed in alphabetical order during `onInit`:
|
||||
|
||||
```sql
|
||||
-- surql/001-initial-schema.surql
|
||||
DEFINE TABLE item SCHEMAFULL;
|
||||
DEFINE FIELD name ON item TYPE string ASSERT $value != NONE;
|
||||
DEFINE FIELD category ON item TYPE string ASSERT $value INSIDE ['a', 'b', 'c'];
|
||||
DEFINE FIELD metadata ON item FLEXIBLE TYPE option<object>;
|
||||
DEFINE FIELD created_at ON item TYPE datetime DEFAULT time::now();
|
||||
DEFINE FIELD updated_at ON item TYPE datetime DEFAULT time::now();
|
||||
|
||||
DEFINE INDEX idx_item_category ON item FIELDS category;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- surql/002-add-full-text.surql
|
||||
DEFINE ANALYZER item_analyzer TOKENIZERS blank, class FILTERS lowercase, ascii;
|
||||
DEFINE INDEX idx_item_name_search ON item FIELDS name SEARCH ANALYZER item_analyzer BM25;
|
||||
```
|
||||
|
||||
**Critical**: The `package.json` `files` array MUST include `"surql"` for migrations to work when the plugin is published:
|
||||
|
||||
```json
|
||||
{
|
||||
"files": ["dist", "surql"]
|
||||
}
|
||||
```
|
||||
|
||||
## Hono Route Handlers
|
||||
|
||||
```typescript
|
||||
// src/routes/items.ts
|
||||
import { Hono } from 'hono';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import z from 'zod';
|
||||
import type { MyPluginContext } from '../plugin.types';
|
||||
|
||||
const CreateItemSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
category: z.enum(['a', 'b', 'c']),
|
||||
metadata: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
export function createItemRoutes(ctx: MyPluginContext) {
|
||||
const app = new Hono();
|
||||
|
||||
app.post('/', zValidator('json', CreateItemSchema), async (c) => {
|
||||
const body = c.req.valid('json');
|
||||
const result = await ctx.db.create('item', body);
|
||||
return c.json(result, 201);
|
||||
});
|
||||
|
||||
app.get('/', async (c) => {
|
||||
const items = await ctx.db.select('item');
|
||||
return c.json(items);
|
||||
});
|
||||
|
||||
app.get('/:id', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const item = await ctx.db.select(`item:${id}`);
|
||||
return c.json(item);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Registration
|
||||
|
||||
Register the plugin in the plugin registry:
|
||||
|
||||
```typescript
|
||||
// packages/server/api/src/plugin/plugin-registry.ts
|
||||
import { myPlugin } from '@suaveplan/api-plugin-my-name';
|
||||
|
||||
registry.register(myPlugin);
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Plugin Lifecycle Tests
|
||||
|
||||
```typescript
|
||||
// src/plugin.test.ts
|
||||
import { describe, expect, it, beforeAll, afterAll } from 'vitest';
|
||||
import { myPlugin } from './plugin';
|
||||
|
||||
describe('MyPlugin', () => {
|
||||
it('has correct metadata', () => {
|
||||
expect(myPlugin.name).toBe('my-plugin');
|
||||
expect(myPlugin.version).toBe('1.0.0');
|
||||
expect(myPlugin.dependencies).toContain('database');
|
||||
});
|
||||
|
||||
it('validates config schema', () => {
|
||||
const result = myPlugin.configSchema!.schema.safeParse({
|
||||
apiKey: 'test-key',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid config', () => {
|
||||
const result = myPlugin.configSchema!.schema.safeParse({
|
||||
apiKey: '',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### MCP Adapter Tests
|
||||
|
||||
```typescript
|
||||
// src/mcp/mcp-adapter.test.ts
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createMcpAdapter } from './mcp-adapter';
|
||||
|
||||
describe('MCP Adapter', () => {
|
||||
it('registers expected tools', () => {
|
||||
const adapter = createMcpAdapter(mockContext);
|
||||
const tools = adapter.listTools();
|
||||
expect(tools).toContainEqual(
|
||||
expect.objectContaining({ name: 'create_item' })
|
||||
);
|
||||
expect(tools).toContainEqual(
|
||||
expect.objectContaining({ name: 'query_items' })
|
||||
);
|
||||
});
|
||||
|
||||
it('create_item tool validates input', async () => {
|
||||
const adapter = createMcpAdapter(mockContext);
|
||||
const result = await adapter.callTool('create_item', {
|
||||
name: 'Test Item',
|
||||
category: 'a',
|
||||
});
|
||||
expect(result.content[0].text).toContain('Test Item');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Max 250 lines** per implementation file
|
||||
- **Max 500 lines** per test file
|
||||
- **95% minimum test coverage** via `bun run test:coverage`
|
||||
- **No mocks** -- use real SurrealDB instances for integration tests
|
||||
- **Zod schemas** for all validation (already in monorepo, do not add Joi or Yup)
|
||||
- **TypeScript only** -- no JavaScript files
|
||||
- **Biome linting** -- not ESLint
|
||||
- **Documentation** -- every `.ts` implementation file needs a matching `.md` file with 200+ words, simple + advanced examples
|
||||
- **`files` in package.json** must include `"surql"` when migrations exist
|
||||
- **Plugin naming**: `@suaveplan/api-plugin-<name>` for packages, `<name>` for the plugin `name` field
|
||||
- Migration files must be numbered sequentially: `001-`, `002-`, etc.
|
||||
|
||||
## Checklist
|
||||
|
||||
1. Plugin defined with `definePlugin()` and proper lifecycle hooks
|
||||
2. Zod config schema with sensible defaults
|
||||
3. SurrealDB migrations in `surql/` directory
|
||||
4. Hono routes for HTTP API
|
||||
5. MCP adapter wrapping plugin operations as tools
|
||||
6. Tests for plugin lifecycle, routes, and MCP tools
|
||||
7. Documentation for every implementation file
|
||||
8. Registered in plugin registry
|
||||
9. `package.json` includes `"surql"` in files array
|
||||
10. Lint clean: `bun run lint --filter=@suaveplan/api-plugin-<name>`
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-package-structure",
|
||||
"version": "1.0.0",
|
||||
"description": "Library package structure patterns and code organization rules for Suaveplan monorepo"
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
---
|
||||
name: package-structure
|
||||
description: Library package structure patterns, code organization rules, documentation requirements, and Storybook conventions for the Suaveplan monorepo
|
||||
---
|
||||
|
||||
# Package Structure & Code Organization
|
||||
|
||||
Use this skill when creating new packages, adding modules to existing packages, or organizing code within the Suaveplan monorepo. Covers architecture principles, file placement, documentation requirements, and component conventions.
|
||||
|
||||
## Package Architecture Principles
|
||||
|
||||
- **Universal packages** (`packages/universal/*`) must have ZERO browser/React dependencies
|
||||
- **Browser packages** (`packages/browser/*`) can depend on React, DOM APIs, and browser-specific libraries
|
||||
- **Server packages** (`packages/server/*`) can depend on Node.js APIs and server-specific libraries
|
||||
|
||||
Place utilities in the appropriate category:
|
||||
|
||||
- CSS/Tailwind utilities (cn, clsx, tailwind-merge) belong in `@suaveplan/react-ui`
|
||||
- React hooks and components belong in browser or react packages
|
||||
- Platform-agnostic utilities belong in universal packages
|
||||
|
||||
## Think Before Write
|
||||
|
||||
Before creating any file:
|
||||
|
||||
1. Consider WHERE to place it (correct package category)
|
||||
2. Check existing patterns and structure in the target package
|
||||
3. Use `tests/` for test files
|
||||
4. Use `scripts/` for utility scripts
|
||||
5. Use `claudedocs/` for Claude-generated documentation
|
||||
|
||||
## Documentation Requirement
|
||||
|
||||
**Every implementation file MUST have a corresponding .md file** co-located in the same directory with the same name (`MyClass.ts` -> `MyClass.md`).
|
||||
|
||||
### Exclusions (no .md required)
|
||||
|
||||
- `.test.ts` / `.test.tsx` -- test files
|
||||
- `.spec.ts` -- Playwright E2E files
|
||||
- `.types.ts` -- types files within a component directory (shared type files in `types/` DO need docs)
|
||||
- `.stories.tsx` -- Storybook stories
|
||||
- `constants/index.ts` -- simple constant exports
|
||||
|
||||
### Content Requirements
|
||||
|
||||
- **Minimum 200 words** of content (excluding code blocks)
|
||||
- **Both simple AND advanced examples** are mandatory in every doc
|
||||
- **Code examples must compile** -- use real types and imports from the package
|
||||
- **No marketing language** -- state capabilities honestly
|
||||
- **No placeholder text** -- every section must contain real content
|
||||
- **Update when code changes** -- documentation must stay in sync
|
||||
|
||||
### Template
|
||||
|
||||
Use standard template from `.claude/templates/module-docs.md`. Required sections:
|
||||
|
||||
1. **Purpose** -- What the module does and why it exists (minimum 2 sentences)
|
||||
2. **Features** -- Bullet list of capabilities
|
||||
3. **Usage: Basic Example** -- Simple import and usage (the "happy path")
|
||||
4. **Usage: Advanced Example** -- Composition, configuration, edge cases
|
||||
5. **API Reference** -- Every public export documented with parameters and return types
|
||||
6. **Implementation Notes** -- Design decisions, performance considerations, known limitations
|
||||
|
||||
### Example Structure
|
||||
|
||||
````markdown
|
||||
# Module Name
|
||||
|
||||
## Purpose
|
||||
Brief description of what this module does and why it exists.
|
||||
|
||||
## Features
|
||||
- Feature 1
|
||||
- Feature 2
|
||||
|
||||
## Usage
|
||||
|
||||
### Simple Example
|
||||
```typescript
|
||||
import { MyClass } from '@suaveplan/package';
|
||||
const instance = new MyClass();
|
||||
```
|
||||
|
||||
### Advanced Example
|
||||
```typescript
|
||||
import { MyClass } from '@suaveplan/package';
|
||||
const instance = new MyClass<CustomType>({
|
||||
onEvent: (data) => transform(data),
|
||||
strategy: 'optimized',
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### MyClass
|
||||
|
||||
#### constructor(options?: Options)
|
||||
|
||||
...
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Design decisions
|
||||
- Performance considerations
|
||||
````
|
||||
|
||||
### Documentation Hierarchy
|
||||
|
||||
Three-level linking structure:
|
||||
|
||||
```text
|
||||
packages/<category>/<package>/
|
||||
README.md # Level 1: Package overview
|
||||
src/
|
||||
<category>/
|
||||
README.md # Level 2: Category index
|
||||
<feature>/
|
||||
<feature>.md # Level 3: Module documentation
|
||||
```
|
||||
|
||||
- Root `README.md` -> Category `README.md` -> Feature `.md`
|
||||
- Each level links to the next, creating discoverable documentation
|
||||
|
||||
## Library Package Structure Pattern
|
||||
|
||||
For library packages (especially in `packages/universal/*`):
|
||||
|
||||
### Category Organization
|
||||
|
||||
- Group related features into category directories (e.g., `asymmetric/`, `symmetric/`, `core/`)
|
||||
- Each category directory MUST have a `README.md` index linking to all feature modules
|
||||
- Feature modules go in subdirectories within categories
|
||||
|
||||
### Feature Module Pattern (four files per feature)
|
||||
|
||||
```text
|
||||
category/
|
||||
README.md # Category index
|
||||
feature-name/
|
||||
feature-name.ts # Implementation
|
||||
feature-name.test.ts # Tests
|
||||
feature-name.types.ts # Type definitions
|
||||
feature-name.md # Documentation
|
||||
```
|
||||
|
||||
### Special Directories
|
||||
|
||||
- `types/`: Shared types, flat structure (`.ts` + `.md` only, no tests)
|
||||
- `utils/`: Utilities, flat structure (`.ts` + `.test.ts` + `.md`)
|
||||
- `constants/`: Just `index.ts` file (no tests or docs required)
|
||||
|
||||
### Subpath Exports (for tree-shaking)
|
||||
|
||||
Add granular exports in `package.json` for each feature module:
|
||||
|
||||
```json
|
||||
{
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./category/feature/feature": {
|
||||
"types": "./dist/category/feature/feature.d.ts",
|
||||
"import": "./dist/category/feature/feature.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Full Example (from @suaveplan/crypto)
|
||||
|
||||
```text
|
||||
packages/universal/crypto/
|
||||
README.md # Package overview
|
||||
src/
|
||||
asymmetric/
|
||||
README.md # Category index
|
||||
key-exchange/
|
||||
key-exchange.ts
|
||||
key-exchange.test.ts
|
||||
key-exchange.types.ts
|
||||
key-exchange.md
|
||||
public-key-crypto/
|
||||
signing/
|
||||
symmetric/
|
||||
README.md
|
||||
encryption/
|
||||
password/
|
||||
streaming/
|
||||
types/ # Shared types (flat)
|
||||
common.ts
|
||||
common.md
|
||||
keys.ts
|
||||
keys.md
|
||||
utils/ # Utilities (flat)
|
||||
utils.ts
|
||||
utils.test.ts
|
||||
utils.md
|
||||
constants/ # Constants (no tests/docs)
|
||||
index.ts
|
||||
```
|
||||
|
||||
## Storybook Stories Required for Components
|
||||
|
||||
Every React component MUST have a corresponding `.stories.tsx` file with:
|
||||
|
||||
- A default story
|
||||
- A playground story with controls
|
||||
- Key variant stories
|
||||
|
||||
Storybook is the primary visual documentation for UI components. Components without stories are considered incomplete.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- Follow language/framework standards (camelCase for JS/TS)
|
||||
- Files, functions, variables must clearly describe purpose
|
||||
- No mixed naming conventions within same project
|
||||
|
||||
## Clean Workspace
|
||||
|
||||
- Remove temporary files when done
|
||||
- Delete build artifacts and logs
|
||||
- No artifact pollution (debug.sh, test.log, temp/)
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-skill-creator",
|
||||
"version": "1.0.0",
|
||||
"description": "Guide for creating project-local skills in the suaveplan-plugins marketplace"
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
---
|
||||
name: skill-creator
|
||||
description: Create new Claude Code skills for the Suaveplan monorepo plugin marketplace following project conventions, progressive disclosure, and degrees-of-freedom principles
|
||||
---
|
||||
|
||||
# Skill Creator for Suaveplan
|
||||
|
||||
Create new Claude Code skills (SKILL.md files) within the Suaveplan plugin marketplace at `.claude/plugins/suaveplan-plugins/`. Each skill is a focused instruction set that teaches Claude how to perform a specific task within the Suaveplan monorepo, following established patterns for React 19, TypeScript, Tailwind CSS, Biome, Vitest, and the monorepo architecture.
|
||||
|
||||
## Marketplace Structure
|
||||
|
||||
The Suaveplan skill marketplace lives at:
|
||||
|
||||
```text
|
||||
.claude/plugins/suaveplan-plugins/
|
||||
.claude-plugin/ # Marketplace metadata directory
|
||||
plugins/
|
||||
suaveplan-<skill-group>/ # Plugin container
|
||||
.claude-plugin/ # Plugin metadata directory
|
||||
skills/
|
||||
<skill-name>/
|
||||
SKILL.md # The skill definition file
|
||||
```
|
||||
|
||||
### Existing Plugins
|
||||
|
||||
| Plugin Directory | Skill(s) | Purpose |
|
||||
|-----------------|----------|---------|
|
||||
| `suaveplan-webapp-testing` | `webapp-testing` | Playwright + Storybook E2E testing |
|
||||
| `suaveplan-web-artifacts` | `web-artifacts-builder` | React component and page building |
|
||||
| `suaveplan-mcp-builder` | `mcp-builder` | MCP server plugins |
|
||||
| `suaveplan-doc-coauthoring` | `doc-coauthoring` | Module documentation |
|
||||
| `suaveplan-skill-creator` | `skill-creator` | Creating new skills (this skill) |
|
||||
| `suaveplan-theme-factory` | `theme-factory` | Tailwind/shadcn theme generation |
|
||||
| `suaveplan-frontend-design` | `frontend-design` | UI/UX design patterns |
|
||||
| `suaveplan-threejs` | Multiple | Three.js/R3F sub-skills |
|
||||
| `suaveplan-webgpu` | `webgpu-threejs-tsl` | WebGPU shader language |
|
||||
|
||||
## Creating a New Skill
|
||||
|
||||
### Step 1: Plan the Skill
|
||||
|
||||
Before writing, answer these questions:
|
||||
|
||||
1. **What task does this skill accomplish?** (one sentence)
|
||||
2. **Which Suaveplan packages does it touch?** (list them)
|
||||
3. **What existing patterns should it reference?** (file structure, testing, linting)
|
||||
4. **What are the degrees of freedom?** (what can the user customize vs. what is fixed)
|
||||
|
||||
### Step 2: Create the Directory Structure
|
||||
|
||||
```bash
|
||||
# Create the plugin container and skill directory
|
||||
mkdir -p .claude/plugins/suaveplan-plugins/plugins/suaveplan-<group>/skills/<skill-name>
|
||||
mkdir -p .claude/plugins/suaveplan-plugins/plugins/suaveplan-<group>/.claude-plugin
|
||||
```
|
||||
|
||||
### Step 3: Write the SKILL.md
|
||||
|
||||
Every SKILL.md follows this structure:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: <skill-name>
|
||||
description: <One line describing the skill in context of Suaveplan, React 19, TypeScript, Tailwind>
|
||||
---
|
||||
|
||||
# <Skill Title> for Suaveplan
|
||||
|
||||
<Opening paragraph: what this skill does, when to use it, 2-3 sentences max>
|
||||
|
||||
## Architecture
|
||||
<Technology table or diagram showing how this fits into Suaveplan>
|
||||
|
||||
## File Structure
|
||||
<Show the directory layout this skill produces or works with>
|
||||
|
||||
## Patterns
|
||||
<The core patterns and code templates, with TypeScript examples>
|
||||
|
||||
## Constraints
|
||||
<Hard rules: line limits, coverage requirements, tooling choices>
|
||||
|
||||
## Checklist
|
||||
<Numbered list of verification steps before marking work complete>
|
||||
```
|
||||
|
||||
### Step 4: Enable the Plugin
|
||||
|
||||
Add the plugin to `.claude/settings.local.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"suaveplan-<group>@suaveplan-plugins": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Core Principles for Writing Skills
|
||||
|
||||
### 1. Conciseness
|
||||
|
||||
Every sentence must teach something. Remove filler words, motivational language, and redundant explanations.
|
||||
|
||||
**Wrong:**
|
||||
> This powerful and flexible skill enables you to build amazing components that leverage the full power of React 19's cutting-edge features.
|
||||
|
||||
**Right:**
|
||||
> Build React 19 components within the Suaveplan monorepo using TypeScript, Tailwind CSS, and shadcn/ui from `@suaveplan/react-ui`.
|
||||
|
||||
### 2. Progressive Disclosure
|
||||
|
||||
Start with the simplest correct usage, then layer complexity. Structure information so a reader can stop at any point and have enough to begin work.
|
||||
|
||||
```text
|
||||
1. One-sentence purpose
|
||||
2. Technology table (what tools are used)
|
||||
3. File structure (where things go)
|
||||
4. Basic pattern (simplest correct example)
|
||||
5. Advanced pattern (composition, configuration, edge cases)
|
||||
6. Constraints (hard rules)
|
||||
7. Checklist (verification steps)
|
||||
```
|
||||
|
||||
### 3. Degrees of Freedom
|
||||
|
||||
Clearly distinguish between:
|
||||
|
||||
- **Fixed** -- things the developer cannot change (linter, test framework, file naming)
|
||||
- **Configurable** -- things with sensible defaults that can be overridden
|
||||
- **Open** -- things the developer decides entirely
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
Fixed: Biome for linting (not ESLint), Vitest for tests, .spec.ts for E2E
|
||||
Configurable: Component variant styles (via cva), Storybook port numbers
|
||||
Open: Component API design, business logic, state management approach
|
||||
```
|
||||
|
||||
### 4. Suaveplan Context
|
||||
|
||||
Every skill must reference the monorepo conventions:
|
||||
|
||||
- **React 19** with TypeScript strict mode
|
||||
- **Tailwind CSS** with `cn()` from `@suaveplan/react-ui`
|
||||
- **Biome** for linting (never ESLint)
|
||||
- **Vitest** for unit tests, **Playwright** for E2E
|
||||
- **Bun** as package manager, **Turborepo** for task orchestration
|
||||
- **No mocks** -- real implementations in tests
|
||||
- **95% test coverage** minimum
|
||||
- **250-line implementation file limit**, **500-line test file limit**
|
||||
- **Documentation** -- `.md` file co-located with every implementation file, 200+ words, simple + advanced examples
|
||||
- **`data-testid`** with `sp-` prefix for all Suaveplan components
|
||||
- **`workspace:*`** for internal dependencies
|
||||
- **No tsc** -- Vite/tsup handle compilation, TypeScript for type-checking only
|
||||
|
||||
### 5. Actionable Code Examples
|
||||
|
||||
Every pattern in a SKILL.md must include a TypeScript code example that:
|
||||
|
||||
- Uses real import paths (`@suaveplan/react-ui`, not `./utils`)
|
||||
- Compiles with the project's TypeScript configuration
|
||||
- Follows Biome formatting rules
|
||||
- Includes type annotations
|
||||
|
||||
## SKILL.md Frontmatter
|
||||
|
||||
Every SKILL.md must start with YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: <skill-name> # Lowercase, hyphenated, matches directory name
|
||||
description: <description> # Must mention Suaveplan + relevant technologies
|
||||
---
|
||||
```
|
||||
|
||||
The `name` field must match the skill directory name exactly. The `description` should be under 150 characters and mention the primary technologies.
|
||||
|
||||
## Validation Checklist for New Skills
|
||||
|
||||
Before considering a skill complete:
|
||||
|
||||
1. Frontmatter has `name` and `description` fields
|
||||
2. `name` matches the directory name
|
||||
3. `description` mentions Suaveplan and primary technologies
|
||||
4. Opening paragraph explains what and when (not why or motivation)
|
||||
5. Technology table lists all tools and frameworks
|
||||
6. File structure shows actual monorepo paths
|
||||
7. Code examples use real `@suaveplan/*` imports
|
||||
8. Constraints section lists all hard rules (line limits, coverage, linting)
|
||||
9. Checklist section provides verification steps
|
||||
10. No marketing language, no emojis, no filler
|
||||
11. All fixed conventions are stated (Biome, Vitest, Playwright, Bun)
|
||||
12. Degrees of freedom are clear (what is fixed vs. configurable vs. open)
|
||||
|
||||
## Example: Minimal Skill
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: csv-export
|
||||
description: Export Suaveplan data tables to CSV using TypeScript streaming and React 19 download patterns
|
||||
---
|
||||
|
||||
# CSV Export for Suaveplan
|
||||
|
||||
Generate CSV files from `@suaveplan/react-data-table` data using TypeScript streaming. Works client-side with Blob URLs or server-side with Node.js streams.
|
||||
|
||||
## Architecture
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Data Source | `@suaveplan/react-data-table` column definitions |
|
||||
| Encoding | TextEncoder streaming API |
|
||||
| Download | Blob URL + anchor click (browser) |
|
||||
| Testing | Vitest |
|
||||
|
||||
## File Structure
|
||||
|
||||
\`\`\`
|
||||
src/utils/csv-export/
|
||||
csv-export.ts # Implementation
|
||||
csv-export.test.ts # Tests
|
||||
csv-export.types.ts # Types
|
||||
csv-export.md # Documentation
|
||||
\`\`\`
|
||||
|
||||
## Basic Pattern
|
||||
|
||||
\`\`\`typescript
|
||||
import { exportToCsv } from './csv-export';
|
||||
import type { ColumnDef } from '@suaveplan/react-data-table';
|
||||
|
||||
const columns: ColumnDef[] = [
|
||||
{ accessorKey: 'name', header: 'Name' },
|
||||
{ accessorKey: 'email', header: 'Email' },
|
||||
];
|
||||
|
||||
const data = [{ name: 'Alice', email: 'alice@example.com' }];
|
||||
const csv = exportToCsv(columns, data);
|
||||
// "Name,Email\nAlice,alice@example.com"
|
||||
\`\`\`
|
||||
|
||||
## Constraints
|
||||
|
||||
- Max 250 lines per file
|
||||
- 95% test coverage
|
||||
- No external CSV libraries -- use TextEncoder
|
||||
- Handle commas, quotes, and newlines in cell values per RFC 4180
|
||||
|
||||
## Checklist
|
||||
|
||||
1. Implementation handles all RFC 4180 edge cases
|
||||
2. Tests cover: empty data, special characters, large datasets, Unicode
|
||||
3. Documentation has simple + advanced examples
|
||||
4. Biome lint clean
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
- Skills must be concise -- remove every unnecessary word
|
||||
- No Python scripts or external tooling -- manual directory creation only
|
||||
- Every skill must reference Suaveplan monorepo conventions
|
||||
- Code examples must compile with project TypeScript config
|
||||
- No emojis in SKILL.md files
|
||||
- Progressive disclosure: simplest information first, complexity layers later
|
||||
- One skill per directory -- no multi-skill files
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-testing-workflow",
|
||||
"version": "1.0.0",
|
||||
"description": "Testing commands, CI/CD integration, turborepo filtering, and troubleshooting for Suaveplan"
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
---
|
||||
name: testing-workflow
|
||||
description: Testing commands, CI/CD integration, advanced turborepo filtering, E2E/Storybook testing, development workflow, and troubleshooting for the Suaveplan monorepo
|
||||
---
|
||||
|
||||
# Testing Workflow & CI/CD
|
||||
|
||||
Use this skill when running tests, setting up CI/CD, troubleshooting build issues, or following the development workflow in the Suaveplan monorepo.
|
||||
|
||||
## Package Testing
|
||||
|
||||
```bash
|
||||
# Lint a package
|
||||
bun run lint --filter=@suaveplan/<package-name>
|
||||
|
||||
# Type check a package
|
||||
bun run typecheck --filter=@suaveplan/<package-name>
|
||||
|
||||
# Run unit/integration tests
|
||||
bun run test --filter=@suaveplan/<package-name>
|
||||
|
||||
# Run tests with coverage
|
||||
bun run test:coverage --filter=@suaveplan/<package-name>
|
||||
|
||||
# Run E2E tests (Playwright)
|
||||
bun run e2e --filter=@suaveplan/<package-name>
|
||||
```
|
||||
|
||||
## Workspace-Wide Testing
|
||||
|
||||
```bash
|
||||
bunx turbo lint # Lint all packages
|
||||
bunx turbo typecheck # Type check all packages
|
||||
bunx turbo test # Run all tests
|
||||
bunx turbo test:coverage # All tests with coverage
|
||||
bunx turbo e2e # All E2E tests
|
||||
```
|
||||
|
||||
## Advanced Turborepo Filtering
|
||||
|
||||
```bash
|
||||
# Package and its dependencies
|
||||
bunx turbo build --filter=@suaveplan/web...
|
||||
|
||||
# Package and its dependents
|
||||
bunx turbo build --filter=...@suaveplan/core
|
||||
|
||||
# Affected since main branch
|
||||
bunx turbo build --filter=...[main]
|
||||
|
||||
# Affected since specific commit
|
||||
bunx turbo build --filter=...[HEAD^1]
|
||||
|
||||
# Combine filters (AND)
|
||||
bunx turbo test --filter=@suaveplan/ui --filter=...@suaveplan/design-system
|
||||
|
||||
# Multiple packages (OR)
|
||||
bunx turbo test --filter=@suaveplan/{ui,forms,auth-ui}
|
||||
|
||||
# Exclude packages
|
||||
bunx turbo test --filter=!@suaveplan/storybook
|
||||
```
|
||||
|
||||
## E2E Testing
|
||||
|
||||
9 packages have Playwright E2E tests:
|
||||
|
||||
@suaveplan/web, @suaveplan/admin, @suaveplan/auth-ui, @suaveplan/dashboard, @suaveplan/onboarding, @suaveplan/billing, @suaveplan/forms, @suaveplan/ui, @suaveplan/storybook
|
||||
|
||||
```bash
|
||||
# Single package E2E
|
||||
bun run e2e --filter=@suaveplan/web
|
||||
|
||||
# Multiple packages E2E
|
||||
bunx turbo e2e --filter=@suaveplan/{web,admin,auth-ui}
|
||||
|
||||
# All E2E tests
|
||||
bunx turbo e2e
|
||||
```
|
||||
|
||||
## Storybook Testing
|
||||
|
||||
```bash
|
||||
# Start Storybook dev server
|
||||
bun run storybook --filter=@suaveplan/ui
|
||||
|
||||
# Run visual tests against Storybook
|
||||
bun run test:storybook --filter=@suaveplan/ui
|
||||
```
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
- Write clear, focused test descriptions
|
||||
- Use arrange-act-assert pattern
|
||||
- Test edge cases and error conditions
|
||||
- Keep tests fast and deterministic
|
||||
- Always add `{ timeout: 5000 }` to `it()`/`test()` calls (30000 for integration/async)
|
||||
- Use `provider: 'istanbul'` for vitest coverage (not v8)
|
||||
- Install `@vitest/coverage-istanbul` (from `catalog:testing`)
|
||||
|
||||
### Timeout Syntax
|
||||
|
||||
```typescript
|
||||
// Unit test
|
||||
it('description', { timeout: 5000 }, () => {
|
||||
// test body
|
||||
});
|
||||
|
||||
// Async test
|
||||
it('async description', { timeout: 10000 }, async () => {
|
||||
// async test body
|
||||
});
|
||||
```
|
||||
|
||||
## Tree-Shaking Verification
|
||||
|
||||
Before publishing any package:
|
||||
|
||||
1. Verify `sideEffects` declaration in package.json
|
||||
2. Test bundle sizes: `bunx vite-bundle-visualizer`
|
||||
3. Confirm no side effects in entry points:
|
||||
- Review `src/index.ts` for immediate execution
|
||||
- Check for global mutations or polyfills
|
||||
- Ensure only export statements exist
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Starting Development
|
||||
|
||||
```bash
|
||||
bun install # Install dependencies
|
||||
bunx turbo build # Build all packages
|
||||
bunx turbo dev # Start development servers
|
||||
```
|
||||
|
||||
### Making Changes
|
||||
|
||||
```bash
|
||||
git checkout -b feature/my-feature # 1. Create feature branch
|
||||
# 2. Make changes to packages
|
||||
bunx turbo test --filter=...[HEAD^1] # 3. Run affected tests
|
||||
bunx turbo test:coverage --filter=...[HEAD^1] # 4. Check coverage
|
||||
bunx turbo lint typecheck --filter=...[HEAD^1] # 5. Lint and type check
|
||||
git add . && git commit -m "feat: msg" # 6. Commit changes
|
||||
```
|
||||
|
||||
### Phase Completion Requirements
|
||||
|
||||
Before marking a phase as complete, verify:
|
||||
|
||||
1. All tests passing: `bunx turbo test`
|
||||
2. 95%+ test coverage: `bunx turbo test:coverage`
|
||||
3. 90%+ documentation coverage: `bun run docs:check`
|
||||
4. No lint errors: `bunx turbo lint`
|
||||
5. No type errors: `bunx turbo typecheck`
|
||||
6. E2E tests passing: `bunx turbo e2e` (for applicable packages)
|
||||
7. Tree-shaking configured: All packages have `sideEffects` declaration
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions Example
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Build
|
||||
run: bunx turbo build
|
||||
|
||||
- name: Lint
|
||||
run: bunx turbo lint
|
||||
|
||||
- name: Type check
|
||||
run: bunx turbo typecheck
|
||||
|
||||
- name: Test
|
||||
run: bunx turbo test:coverage
|
||||
|
||||
- name: E2E tests
|
||||
run: bunx turbo e2e
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
```
|
||||
|
||||
### Remote Caching (Optional)
|
||||
|
||||
```bash
|
||||
bunx turbo login # Login to Vercel
|
||||
bunx turbo link # Link your repository
|
||||
bunx turbo build --remote-cache # Run with remote cache
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### React Version Conflicts
|
||||
|
||||
**Symptoms:** `Error: Minified React error #31`, `TypeError: Cannot read properties of null (reading 'useContext')`, build fails during static page generation.
|
||||
|
||||
**Root Cause:** Multiple React versions in monorepo.
|
||||
|
||||
**Solution:** Use Bun `overrides` with exact versions and nuclear clean install. See `suaveplan-dependency-management` skill for full steps.
|
||||
|
||||
### Clear Cache
|
||||
|
||||
```bash
|
||||
bunx turbo clean # Clear Turborepo cache
|
||||
rm -rf node_modules && rm bun.lockb && bun install # Clear Bun cache
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
```bash
|
||||
bunx turbo test --filter=@suaveplan/core --verbosity=3 # Verbose logging
|
||||
bunx turbo test --summarize # Task execution summary
|
||||
```
|
||||
|
||||
### Type Errors
|
||||
|
||||
```bash
|
||||
bunx turbo build --filter=...@suaveplan/types # Build types for all packages
|
||||
bun tsc --noEmit --pretty # Type check with verbose output
|
||||
```
|
||||
|
||||
## Tool Selection
|
||||
|
||||
- MCP servers > Native tools > Basic alternatives
|
||||
- Use specialized tools for their designed purpose
|
||||
- Prefer powerful search tools (Grep, Glob) over bash
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-theme-factory",
|
||||
"version": "1.0.0",
|
||||
"description": "Tailwind CSS + shadcn/ui theming toolkit with curated themes"
|
||||
}
|
||||
@@ -0,0 +1,723 @@
|
||||
---
|
||||
name: theme-factory
|
||||
description: Generate and apply Tailwind CSS + shadcn/ui themes for Suaveplan React applications using HSL CSS variables, dark mode variants, and component-level theming
|
||||
---
|
||||
|
||||
# Theme Factory for Suaveplan
|
||||
|
||||
Generate complete Tailwind CSS theme configurations compatible with `@suaveplan/react-ui` (shadcn/ui). Each theme defines HSL CSS variables for `:root` (light) and `.dark` (dark mode), extending the Tailwind config to provide consistent styling across all Suaveplan React components.
|
||||
|
||||
## Architecture
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Color Format | HSL (required for shadcn/ui compatibility) |
|
||||
| Variable Scope | CSS custom properties on `:root` and `.dark` |
|
||||
| Dark Mode | Tailwind `dark:` variant via `class` strategy |
|
||||
| Component Library | `@suaveplan/react-ui` (shadcn/ui-based) |
|
||||
| Class Merging | `cn()` from `@suaveplan/react-ui` |
|
||||
| Build | Tailwind CSS PostCSS pipeline via Vite |
|
||||
|
||||
## Theme Variables
|
||||
|
||||
Every Suaveplan theme must define these CSS variables in HSL format (hue saturation lightness without commas, no `hsl()` wrapper):
|
||||
|
||||
### Required Variables
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Page */
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
/* Card surfaces */
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
/* Popover surfaces */
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
/* Primary actions */
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
/* Secondary actions */
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
/* Subdued elements */
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
/* Highlights and hover states */
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
/* Errors and destructive actions */
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
/* Borders and separators */
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
|
||||
/* Focus rings */
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
/* Border radius scale */
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
```
|
||||
|
||||
### Optional Extended Variables
|
||||
|
||||
For advanced themes, add sidebar and chart variables:
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Sidebar (used by @suaveplan/react-ui sidebar component) */
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
|
||||
/* Charts (used by @suaveplan/react-charts) */
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
```
|
||||
|
||||
## Tailwind Configuration Extension
|
||||
|
||||
Extend the Tailwind config to consume CSS variables:
|
||||
|
||||
```typescript
|
||||
// tailwind.config.ts
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
const config: Config = {
|
||||
darkMode: 'class',
|
||||
content: ['./src/**/*.{ts,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
## 10 Built-In Themes
|
||||
|
||||
### 1. Default (Zinc)
|
||||
|
||||
The standard Suaveplan theme. Neutral zinc scale with sharp contrasts.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 5.9% 10%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Slate
|
||||
|
||||
Cool blue-gray tone. Professional, corporate.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Ocean Blue
|
||||
|
||||
Vibrant blue primary. Energetic, tech-forward.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Emerald
|
||||
|
||||
Green primary. Growth, health, sustainability.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--primary: 160.1 84.1% 39.4%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 140 30% 95%;
|
||||
--secondary-foreground: 160.1 84.1% 25%;
|
||||
--muted: 140 20% 96%;
|
||||
--muted-foreground: 150 10% 45%;
|
||||
--accent: 140 30% 95%;
|
||||
--accent-foreground: 160.1 84.1% 25%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 140 20% 90%;
|
||||
--input: 140 20% 90%;
|
||||
--ring: 160.1 84.1% 39.4%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 150 30% 4%;
|
||||
--foreground: 140 20% 98%;
|
||||
--primary: 160.1 84.1% 39.4%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 150 20% 15%;
|
||||
--secondary-foreground: 140 20% 98%;
|
||||
--muted: 150 20% 15%;
|
||||
--muted-foreground: 150 15% 60%;
|
||||
--accent: 150 20% 15%;
|
||||
--accent-foreground: 140 20% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 150 20% 15%;
|
||||
--input: 150 20% 15%;
|
||||
--ring: 160.1 84.1% 39.4%;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Rose
|
||||
|
||||
Pink-red primary. Bold, creative, expressive.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--primary: 346.8 77.2% 49.8%;
|
||||
--primary-foreground: 355.7 100% 97.3%;
|
||||
--secondary: 340 30% 96%;
|
||||
--secondary-foreground: 346.8 77.2% 35%;
|
||||
--muted: 340 20% 96%;
|
||||
--muted-foreground: 340 10% 45%;
|
||||
--accent: 340 30% 96%;
|
||||
--accent-foreground: 346.8 77.2% 35%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 340 20% 91%;
|
||||
--input: 340 20% 91%;
|
||||
--ring: 346.8 77.2% 49.8%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 340 30% 4%;
|
||||
--foreground: 340 10% 98%;
|
||||
--primary: 346.8 77.2% 49.8%;
|
||||
--primary-foreground: 355.7 100% 97.3%;
|
||||
--secondary: 340 20% 15%;
|
||||
--secondary-foreground: 340 10% 98%;
|
||||
--muted: 340 20% 15%;
|
||||
--muted-foreground: 340 15% 60%;
|
||||
--accent: 340 20% 15%;
|
||||
--accent-foreground: 340 10% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 340 20% 15%;
|
||||
--input: 340 20% 15%;
|
||||
--ring: 346.8 77.2% 49.8%;
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Amber
|
||||
|
||||
Warm amber/orange primary. Friendly, approachable, energetic.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 20 14.3% 4.1%;
|
||||
--primary: 24.6 95% 53.1%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
--secondary: 30 30% 96%;
|
||||
--secondary-foreground: 24.6 95% 30%;
|
||||
--muted: 30 20% 96%;
|
||||
--muted-foreground: 25 10% 45%;
|
||||
--accent: 30 30% 96%;
|
||||
--accent-foreground: 24.6 95% 30%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 30 20% 90%;
|
||||
--input: 30 20% 90%;
|
||||
--ring: 24.6 95% 53.1%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 20 14.3% 4.1%;
|
||||
--foreground: 60 9.1% 97.8%;
|
||||
--primary: 20.5 90.2% 48.2%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
--secondary: 20 20% 15%;
|
||||
--secondary-foreground: 60 9.1% 97.8%;
|
||||
--muted: 20 20% 15%;
|
||||
--muted-foreground: 25 15% 60%;
|
||||
--accent: 20 20% 15%;
|
||||
--accent-foreground: 60 9.1% 97.8%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 20 20% 15%;
|
||||
--input: 20 20% 15%;
|
||||
--ring: 20.5 90.2% 48.2%;
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Violet
|
||||
|
||||
Purple primary. Premium, creative, distinctive.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 224 71.4% 4.1%;
|
||||
--primary: 262.1 83.3% 57.8%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--secondary: 260 30% 96%;
|
||||
--secondary-foreground: 262.1 83.3% 35%;
|
||||
--muted: 260 20% 96%;
|
||||
--muted-foreground: 260 10% 45%;
|
||||
--accent: 260 30% 96%;
|
||||
--accent-foreground: 262.1 83.3% 35%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 260 20% 91%;
|
||||
--input: 260 20% 91%;
|
||||
--ring: 262.1 83.3% 57.8%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 260 30% 4%;
|
||||
--foreground: 260 10% 98%;
|
||||
--primary: 263.4 70% 50.4%;
|
||||
--primary-foreground: 210 20% 98%;
|
||||
--secondary: 260 20% 15%;
|
||||
--secondary-foreground: 260 10% 98%;
|
||||
--muted: 260 20% 15%;
|
||||
--muted-foreground: 260 15% 60%;
|
||||
--accent: 260 20% 15%;
|
||||
--accent-foreground: 260 10% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 260 20% 15%;
|
||||
--input: 260 20% 15%;
|
||||
--ring: 263.4 70% 50.4%;
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Neutral
|
||||
|
||||
Pure grayscale. Minimal, content-focused, photography-friendly.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Teal
|
||||
|
||||
Blue-green primary. Calming, trustworthy, health/wellness.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--primary: 175 84% 32%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 170 30% 95%;
|
||||
--secondary-foreground: 175 84% 22%;
|
||||
--muted: 170 20% 96%;
|
||||
--muted-foreground: 170 10% 45%;
|
||||
--accent: 170 30% 95%;
|
||||
--accent-foreground: 175 84% 22%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 170 20% 90%;
|
||||
--input: 170 20% 90%;
|
||||
--ring: 175 84% 32%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 175 30% 4%;
|
||||
--foreground: 170 20% 98%;
|
||||
--primary: 175 84% 32%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 175 20% 15%;
|
||||
--secondary-foreground: 170 20% 98%;
|
||||
--muted: 175 20% 15%;
|
||||
--muted-foreground: 170 15% 60%;
|
||||
--accent: 175 20% 15%;
|
||||
--accent-foreground: 170 20% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 175 20% 15%;
|
||||
--input: 175 20% 15%;
|
||||
--ring: 175 84% 32%;
|
||||
}
|
||||
```
|
||||
|
||||
### 10. Crimson
|
||||
|
||||
Deep red primary. Bold, authoritative, editorial.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--primary: 0 72.2% 50.6%;
|
||||
--primary-foreground: 0 85.7% 97.3%;
|
||||
--secondary: 0 20% 96%;
|
||||
--secondary-foreground: 0 72.2% 35%;
|
||||
--muted: 0 15% 96%;
|
||||
--muted-foreground: 0 8% 45%;
|
||||
--accent: 0 20% 96%;
|
||||
--accent-foreground: 0 72.2% 35%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 15% 91%;
|
||||
--input: 0 15% 91%;
|
||||
--ring: 0 72.2% 50.6%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 20% 4%;
|
||||
--foreground: 0 10% 98%;
|
||||
--primary: 0 72.2% 50.6%;
|
||||
--primary-foreground: 0 85.7% 97.3%;
|
||||
--secondary: 0 15% 15%;
|
||||
--secondary-foreground: 0 10% 98%;
|
||||
--muted: 0 15% 15%;
|
||||
--muted-foreground: 0 10% 60%;
|
||||
--accent: 0 15% 15%;
|
||||
--accent-foreground: 0 10% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 15% 15%;
|
||||
--input: 0 15% 15%;
|
||||
--ring: 0 72.2% 50.6%;
|
||||
}
|
||||
```
|
||||
|
||||
## Applying a Theme
|
||||
|
||||
### Method 1: Global CSS (Single Theme)
|
||||
|
||||
Place variables in the app's global CSS file:
|
||||
|
||||
```css
|
||||
/* apps/web/src/globals.css */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
/* ... rest of light theme variables */
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
/* ... rest of dark theme variables */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Method 2: Theme Switcher (Multiple Themes)
|
||||
|
||||
Use data attributes to switch between themes at runtime:
|
||||
|
||||
```css
|
||||
/* themes.css */
|
||||
[data-theme="default"] { /* ... zinc variables ... */ }
|
||||
[data-theme="ocean"] { /* ... ocean blue variables ... */ }
|
||||
[data-theme="emerald"] { /* ... emerald variables ... */ }
|
||||
/* ... */
|
||||
|
||||
[data-theme="default"].dark { /* ... zinc dark variables ... */ }
|
||||
[data-theme="ocean"].dark { /* ... ocean blue dark variables ... */ }
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Theme switcher component
|
||||
import * as React from 'react';
|
||||
|
||||
const themes = [
|
||||
'default', 'slate', 'ocean', 'emerald', 'rose',
|
||||
'amber', 'violet', 'neutral', 'teal', 'crimson',
|
||||
] as const;
|
||||
|
||||
type Theme = (typeof themes)[number];
|
||||
|
||||
export function useTheme() {
|
||||
const [theme, setTheme] = React.useState<Theme>('default');
|
||||
const [dark, setDark] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
document.documentElement.classList.toggle('dark', dark);
|
||||
}, [theme, dark]);
|
||||
|
||||
return { theme, setTheme, dark, setDark, themes };
|
||||
}
|
||||
```
|
||||
|
||||
### Method 3: Component-Level Theming
|
||||
|
||||
Override variables on a specific container for scoped theming:
|
||||
|
||||
```typescript
|
||||
import { cn } from '@suaveplan/react-ui';
|
||||
|
||||
export function BrandedSection({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<section
|
||||
className={cn('rounded-lg p-6', className)}
|
||||
style={{
|
||||
'--primary': '221.2 83.2% 53.3%',
|
||||
'--primary-foreground': '210 40% 98%',
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Creating a Custom Theme
|
||||
|
||||
### Step-by-Step
|
||||
|
||||
1. **Choose a primary hue** (0-360 on the HSL wheel)
|
||||
2. **Set primary saturation** (60-90% for vibrant, 30-50% for muted)
|
||||
3. **Derive secondary** from primary hue with lower saturation and higher lightness
|
||||
4. **Derive muted** from primary hue with very low saturation (15-25%) and high lightness (95-97%)
|
||||
5. **Set accent** equal to secondary (or slightly different hue for contrast)
|
||||
6. **Keep destructive** at red (hue 0) -- users expect red for errors
|
||||
7. **Derive border** from muted with slightly lower lightness
|
||||
8. **Set ring** equal to primary (focus rings should match primary actions)
|
||||
9. **Dark mode**: Invert lightness values -- backgrounds become dark (3-5%), foregrounds become light (95-98%)
|
||||
|
||||
### Contrast Requirements
|
||||
|
||||
All theme color pairs must meet WCAG 2.1 AA contrast ratios:
|
||||
|
||||
- **Normal text**: minimum 4.5:1 contrast ratio
|
||||
- **Large text** (18px+ or 14px+ bold): minimum 3:1 contrast ratio
|
||||
- **UI components**: minimum 3:1 against adjacent colors
|
||||
|
||||
Verify with: `foreground` against `background`, `primary-foreground` against `primary`, etc.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **HSL format only** -- no hex, rgb, or oklch in CSS variables
|
||||
- **No `hsl()` wrapper** in variable values -- Tailwind adds it via `hsl(var(--name))`
|
||||
- **All 14 required variables** must be defined in both `:root` and `.dark`
|
||||
- **WCAG AA contrast** on all foreground/background pairs
|
||||
- **`dark:` variant** must be supported -- no light-only themes
|
||||
- **`--radius`** must be included (default `0.5rem`)
|
||||
- **No Tailwind color utilities** (`bg-blue-500`) for themed elements -- always use semantic tokens (`bg-primary`)
|
||||
- Components must use `cn()` from `@suaveplan/react-ui` for class merging
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-threejs",
|
||||
"version": "1.0.0",
|
||||
"description": "React Three Fiber development skills for Suaveplan 3D packages"
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
---
|
||||
name: threejs-animation
|
||||
description: Three.js animation systems adapted for React Three Fiber and Suaveplan R3F packages. Covers useFrame-driven animation loops, AnimationMixer integration, skeletal animation with @suaveplan/r3f-character, keyframe tracks, morph targets, and declarative animation patterns in the R3F component model.
|
||||
---
|
||||
|
||||
This skill covers animation in Three.js within the React Three Fiber (R3F) context used by the Suaveplan monorepo. All animation code targets **React 19 + TypeScript + R3F** and follows Suaveplan CLAUDE.md conventions.
|
||||
|
||||
The user needs to animate 3D objects, characters, cameras, or properties in an R3F scene. They may need frame-loop animations, clip-based skeletal animations, morph target blending, or physics-driven motion.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Three.js Animation System
|
||||
|
||||
Three.js provides several animation primitives:
|
||||
|
||||
- **AnimationClip**: A reusable set of keyframe tracks (position, rotation, scale, morph targets)
|
||||
- **AnimationMixer**: The playback controller that drives clips on a specific object
|
||||
- **AnimationAction**: Controls a single clip's playback (play, pause, crossfade, weight, speed)
|
||||
- **KeyframeTrack**: Raw keyframe data (times + values) for a single property
|
||||
- **Clock**: High-resolution timer for delta time calculation
|
||||
|
||||
### R3F Animation Approach
|
||||
|
||||
In R3F, animation integrates with React's component lifecycle:
|
||||
|
||||
| Three.js (Imperative) | R3F (Declarative) | When to Use |
|
||||
|---|---|---|
|
||||
| `requestAnimationFrame` loop | `useFrame` hook | Per-frame updates |
|
||||
| `new AnimationMixer(model)` | `useAnimations` from drei | GLTF clip playback |
|
||||
| Manual clock management | `useFrame((state, delta) => {})` | Delta time access |
|
||||
| `mixer.update(delta)` | Automatic in `useAnimations` | Mixer ticking |
|
||||
| Event listeners | React refs + useEffect | Lifecycle binding |
|
||||
|
||||
### Suaveplan R3F Packages for Animation
|
||||
|
||||
| Package | Purpose | Import |
|
||||
|---|---|---|
|
||||
| `@suaveplan/r3f` | Core utilities, scene context | `import { useR3F } from '@suaveplan/r3f'` |
|
||||
| `@suaveplan/r3f-character` | Skeletal animation, character controllers | `import { useCharacterAnimation } from '@suaveplan/r3f-character'` |
|
||||
| `@suaveplan/r3f-performance` | Animation performance monitoring | `import { usePerformanceMonitor } from '@suaveplan/r3f-performance'` |
|
||||
| `@suaveplan/r3f-shaders` | Animated shader materials | `import { useAnimatedShader } from '@suaveplan/r3f-shaders'` |
|
||||
|
||||
## useFrame: The R3F Animation Loop
|
||||
|
||||
`useFrame` is the primary animation hook. It runs every frame inside the R3F render loop and provides access to the Three.js state and delta time.
|
||||
|
||||
```tsx
|
||||
import { useRef } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import type { Mesh } from 'three';
|
||||
|
||||
interface SpinningBoxProps {
|
||||
speed?: number;
|
||||
axis?: 'x' | 'y' | 'z';
|
||||
}
|
||||
|
||||
export function SpinningBox({ speed = 1, axis = 'y' }: SpinningBoxProps) {
|
||||
const meshRef = useRef<Mesh>(null);
|
||||
|
||||
useFrame((_state, delta) => {
|
||||
if (!meshRef.current) return;
|
||||
meshRef.current.rotation[axis] += delta * speed;
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh ref={meshRef}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color="tomato" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### useFrame Priority System
|
||||
|
||||
R3F processes `useFrame` callbacks in priority order. Lower numbers run first. This is critical for animation ordering (e.g., physics before rendering).
|
||||
|
||||
```tsx
|
||||
// Physics runs first (priority -1)
|
||||
useFrame(() => { /* update physics */ }, -1);
|
||||
|
||||
// Animation runs at default priority (0)
|
||||
useFrame(() => { /* update animations */ });
|
||||
|
||||
// Camera follows after animation (priority 1)
|
||||
useFrame(() => { /* update camera */ }, 1);
|
||||
```
|
||||
|
||||
### Conditional useFrame
|
||||
|
||||
Avoid creating/destroying hooks dynamically. Instead, gate logic inside the callback:
|
||||
|
||||
```tsx
|
||||
export function ConditionalAnimation({ active }: { active: boolean }) {
|
||||
const meshRef = useRef<Mesh>(null);
|
||||
|
||||
useFrame((_state, delta) => {
|
||||
if (!active || !meshRef.current) return;
|
||||
meshRef.current.rotation.y += delta;
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh ref={meshRef}>
|
||||
<sphereGeometry args={[0.5, 32, 32]} />
|
||||
<meshStandardMaterial color="dodgerblue" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## AnimationMixer in R3F
|
||||
|
||||
For GLTF models with embedded animations, use `useAnimations` from `@react-three/drei` which wraps `AnimationMixer`.
|
||||
|
||||
```tsx
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useGLTF, useAnimations } from '@react-three/drei';
|
||||
import type { Group } from 'three';
|
||||
|
||||
interface AnimatedModelProps {
|
||||
url: string;
|
||||
clipName: string;
|
||||
playing?: boolean;
|
||||
}
|
||||
|
||||
export function AnimatedModel({ url, clipName, playing = true }: AnimatedModelProps) {
|
||||
const groupRef = useRef<Group>(null);
|
||||
const { scene, animations } = useGLTF(url);
|
||||
const { actions, mixer } = useAnimations(animations, groupRef);
|
||||
|
||||
useEffect(() => {
|
||||
const action = actions[clipName];
|
||||
if (!action) return;
|
||||
|
||||
if (playing) {
|
||||
action.reset().fadeIn(0.3).play();
|
||||
} else {
|
||||
action.fadeOut(0.3);
|
||||
}
|
||||
|
||||
return () => {
|
||||
action.fadeOut(0.3);
|
||||
};
|
||||
}, [clipName, playing, actions]);
|
||||
|
||||
return <primitive ref={groupRef} object={scene} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Crossfading Between Clips
|
||||
|
||||
```tsx
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
import { useGLTF, useAnimations } from '@react-three/drei';
|
||||
import type { Group, AnimationAction } from 'three';
|
||||
|
||||
interface CrossfadeModelProps {
|
||||
url: string;
|
||||
currentClip: string;
|
||||
fadeDuration?: number;
|
||||
}
|
||||
|
||||
export function CrossfadeModel({ url, currentClip, fadeDuration = 0.4 }: CrossfadeModelProps) {
|
||||
const groupRef = useRef<Group>(null);
|
||||
const { scene, animations } = useGLTF(url);
|
||||
const { actions } = useAnimations(animations, groupRef);
|
||||
const [previousClip, setPreviousClip] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const nextAction = actions[currentClip];
|
||||
if (!nextAction) return;
|
||||
|
||||
if (previousClip && actions[previousClip]) {
|
||||
const prevAction = actions[previousClip] as AnimationAction;
|
||||
nextAction.reset().fadeIn(fadeDuration).play();
|
||||
prevAction.fadeOut(fadeDuration);
|
||||
} else {
|
||||
nextAction.reset().fadeIn(fadeDuration).play();
|
||||
}
|
||||
|
||||
setPreviousClip(currentClip);
|
||||
}, [currentClip, actions, fadeDuration]);
|
||||
|
||||
return <primitive ref={groupRef} object={scene} />;
|
||||
}
|
||||
```
|
||||
|
||||
## @suaveplan/r3f-character: Skeletal Animation
|
||||
|
||||
The `@suaveplan/r3f-character` package provides higher-level character animation utilities built on top of Three.js's `AnimationMixer`.
|
||||
|
||||
### Character Animation Controller
|
||||
|
||||
```tsx
|
||||
import { useCharacterAnimation } from '@suaveplan/r3f-character';
|
||||
import type { CharacterAnimationConfig } from '@suaveplan/r3f-character';
|
||||
|
||||
const animationConfig: CharacterAnimationConfig = {
|
||||
clips: {
|
||||
idle: { url: '/models/character/idle.glb', loop: true },
|
||||
walk: { url: '/models/character/walk.glb', loop: true },
|
||||
run: { url: '/models/character/run.glb', loop: true },
|
||||
jump: { url: '/models/character/jump.glb', loop: false },
|
||||
},
|
||||
transitions: {
|
||||
idle_to_walk: { duration: 0.3 },
|
||||
walk_to_run: { duration: 0.2 },
|
||||
any_to_jump: { duration: 0.1 },
|
||||
},
|
||||
defaultClip: 'idle',
|
||||
};
|
||||
|
||||
export function Character() {
|
||||
const { groupRef, playClip, currentClip } = useCharacterAnimation(animationConfig);
|
||||
|
||||
return (
|
||||
<group ref={groupRef}>
|
||||
{/* Character mesh loaded via the hook */}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Blending Mixamo Animations
|
||||
|
||||
When working with Mixamo animations on a shared skeleton:
|
||||
|
||||
```tsx
|
||||
import { useRef, useMemo } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { useGLTF, useAnimations } from '@react-three/drei';
|
||||
import { AnimationUtils } from 'three';
|
||||
import type { Group } from 'three';
|
||||
|
||||
interface MixamoCharacterProps {
|
||||
modelUrl: string;
|
||||
animationUrls: Record<string, string>;
|
||||
currentAnimation: string;
|
||||
}
|
||||
|
||||
export function MixamoCharacter({
|
||||
modelUrl,
|
||||
animationUrls,
|
||||
currentAnimation,
|
||||
}: MixamoCharacterProps) {
|
||||
const groupRef = useRef<Group>(null);
|
||||
const { scene, animations: baseAnimations } = useGLTF(modelUrl);
|
||||
|
||||
// Load additional animation files
|
||||
const additionalAnims = Object.entries(animationUrls).map(([_name, url]) => {
|
||||
return useGLTF(url).animations;
|
||||
});
|
||||
|
||||
const allAnimations = useMemo(() => {
|
||||
const merged = [...baseAnimations];
|
||||
Object.keys(animationUrls).forEach((name, index) => {
|
||||
const clips = additionalAnims[index];
|
||||
if (clips?.[0]) {
|
||||
const clip = AnimationUtils.subclip(clips[0], name, 0, clips[0].duration * 30, 30);
|
||||
merged.push(clip);
|
||||
}
|
||||
});
|
||||
return merged;
|
||||
}, [baseAnimations, additionalAnims]);
|
||||
|
||||
const { actions } = useAnimations(allAnimations, groupRef);
|
||||
|
||||
useFrame(() => {
|
||||
const action = actions[currentAnimation];
|
||||
if (action && !action.isRunning()) {
|
||||
Object.values(actions).forEach((a) => a?.fadeOut(0.3));
|
||||
action.reset().fadeIn(0.3).play();
|
||||
}
|
||||
});
|
||||
|
||||
return <primitive ref={groupRef} object={scene} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Morph Target Animation
|
||||
|
||||
Morph targets (blend shapes) animate vertex positions for facial expressions, deformations, etc.
|
||||
|
||||
```tsx
|
||||
import { useRef } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { useGLTF } from '@react-three/drei';
|
||||
import type { Mesh } from 'three';
|
||||
|
||||
interface MorphAnimationProps {
|
||||
url: string;
|
||||
/** Map of morph target name to weight (0-1) */
|
||||
morphWeights: Record<string, number>;
|
||||
/** Transition speed for weight interpolation */
|
||||
lerpSpeed?: number;
|
||||
}
|
||||
|
||||
export function MorphAnimation({ url, morphWeights, lerpSpeed = 5 }: MorphAnimationProps) {
|
||||
const { scene } = useGLTF(url);
|
||||
const meshRef = useRef<Mesh>(null);
|
||||
|
||||
useFrame((_state, delta) => {
|
||||
if (!meshRef.current?.morphTargetDictionary || !meshRef.current.morphTargetInfluences) return;
|
||||
|
||||
const dict = meshRef.current.morphTargetDictionary;
|
||||
const influences = meshRef.current.morphTargetInfluences;
|
||||
|
||||
for (const [name, targetWeight] of Object.entries(morphWeights)) {
|
||||
const index = dict[name];
|
||||
if (index !== undefined) {
|
||||
const current = influences[index] ?? 0;
|
||||
influences[index] = current + (targetWeight - current) * Math.min(delta * lerpSpeed, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return <primitive ref={meshRef} object={scene} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Procedural Animation Patterns
|
||||
|
||||
### Spring-Based Animation
|
||||
|
||||
```tsx
|
||||
import { useRef } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { MathUtils, Vector3 } from 'three';
|
||||
import type { Mesh } from 'three';
|
||||
|
||||
interface SpringConfig {
|
||||
stiffness: number;
|
||||
damping: number;
|
||||
}
|
||||
|
||||
export function SpringFollower({ target, config }: { target: Vector3; config: SpringConfig }) {
|
||||
const meshRef = useRef<Mesh>(null);
|
||||
const velocity = useRef(new Vector3());
|
||||
|
||||
useFrame((_state, delta) => {
|
||||
if (!meshRef.current) return;
|
||||
|
||||
const pos = meshRef.current.position;
|
||||
const force = target.clone().sub(pos).multiplyScalar(config.stiffness);
|
||||
velocity.current.add(force.multiplyScalar(delta));
|
||||
velocity.current.multiplyScalar(Math.max(0, 1 - config.damping * delta));
|
||||
pos.add(velocity.current.clone().multiplyScalar(delta));
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh ref={meshRef}>
|
||||
<sphereGeometry args={[0.2, 16, 16]} />
|
||||
<meshStandardMaterial color="limegreen" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Easing with MathUtils
|
||||
|
||||
```tsx
|
||||
import { useRef } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { MathUtils } from 'three';
|
||||
import type { Mesh } from 'three';
|
||||
|
||||
export function SmoothRotation({ targetAngle }: { targetAngle: number }) {
|
||||
const meshRef = useRef<Mesh>(null);
|
||||
|
||||
useFrame((_state, delta) => {
|
||||
if (!meshRef.current) return;
|
||||
meshRef.current.rotation.y = MathUtils.damp(
|
||||
meshRef.current.rotation.y,
|
||||
targetAngle,
|
||||
4, // lambda (smoothing factor)
|
||||
delta
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh ref={meshRef}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshNormalMaterial />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Animation Budget
|
||||
|
||||
Use `@suaveplan/r3f-performance` to monitor animation cost:
|
||||
|
||||
```tsx
|
||||
import { usePerformanceMonitor } from '@suaveplan/r3f-performance';
|
||||
|
||||
export function MonitoredScene() {
|
||||
usePerformanceMonitor({
|
||||
onDecline: ({ fps }) => {
|
||||
// Reduce animation quality when FPS drops
|
||||
if (fps < 30) {
|
||||
// Switch to simpler animations, reduce bone count, etc.
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return <>{/* scene content */}</>;
|
||||
}
|
||||
```
|
||||
|
||||
### Optimization Guidelines
|
||||
|
||||
1. **Avoid allocations in useFrame**: Never create `new Vector3()` or `new Quaternion()` inside `useFrame`. Allocate in `useRef` or module scope.
|
||||
2. **Use delta time**: Always multiply by `delta` for frame-rate independent animation.
|
||||
3. **Limit active mixers**: Each `AnimationMixer` updates every frame. Pool or disable off-screen mixers.
|
||||
4. **LOD-aware animation**: Reduce bone count or switch to procedural animation for distant characters.
|
||||
5. **Pause when invisible**: Use `useIntersect` from drei or Intersection Observer to pause off-screen animations.
|
||||
|
||||
```tsx
|
||||
// Pre-allocate reusable objects outside useFrame
|
||||
const _tempVec = new Vector3();
|
||||
const _tempQuat = new Quaternion();
|
||||
|
||||
useFrame(() => {
|
||||
// Reuse allocated objects
|
||||
_tempVec.set(0, 1, 0);
|
||||
mesh.current?.position.lerp(_tempVec, 0.1);
|
||||
});
|
||||
```
|
||||
|
||||
### Three.js KeyframeTrack Types
|
||||
|
||||
When building animation clips programmatically:
|
||||
|
||||
| Track Type | Property | Value Type |
|
||||
|---|---|---|
|
||||
| `VectorKeyframeTrack` | `.position`, `.scale` | `[x,y,z]` per keyframe |
|
||||
| `QuaternionKeyframeTrack` | `.quaternion` | `[x,y,z,w]` per keyframe |
|
||||
| `NumberKeyframeTrack` | `.opacity`, morph targets | Single float per keyframe |
|
||||
| `BooleanKeyframeTrack` | `.visible` | Boolean per keyframe |
|
||||
| `ColorKeyframeTrack` | `.color` | `[r,g,b]` per keyframe |
|
||||
| `StringKeyframeTrack` | Any string property | String per keyframe |
|
||||
|
||||
### Programmatic Clip Creation in R3F
|
||||
|
||||
```tsx
|
||||
import { useMemo } from 'react';
|
||||
import { AnimationClip, VectorKeyframeTrack, NumberKeyframeTrack } from 'three';
|
||||
import { useAnimations } from '@react-three/drei';
|
||||
|
||||
function useBounceClip(name: string, height: number, duration: number) {
|
||||
return useMemo(() => {
|
||||
const times = [0, duration * 0.5, duration];
|
||||
const posValues = [0, 0, 0, 0, height, 0, 0, 0, 0];
|
||||
const scaleValues = [1, 1, 1, 1, 0.8, 1.2, 1, 1, 1];
|
||||
|
||||
return new AnimationClip(name, duration, [
|
||||
new VectorKeyframeTrack('.position', times, posValues),
|
||||
new VectorKeyframeTrack('.scale', times, scaleValues),
|
||||
]);
|
||||
}, [name, height, duration]);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Animations
|
||||
|
||||
### Vitest Unit Tests
|
||||
|
||||
```tsx
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
describe('useBounceClip', () => {
|
||||
it('creates a clip with correct duration', () => {
|
||||
const { result } = renderHook(() => useBounceClip('bounce', 2, 1));
|
||||
expect(result.current.duration).toBe(1);
|
||||
expect(result.current.tracks).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('includes position and scale tracks', () => {
|
||||
const { result } = renderHook(() => useBounceClip('bounce', 2, 1));
|
||||
const trackNames = result.current.tracks.map((t) => t.name);
|
||||
expect(trackNames).toContain('.position');
|
||||
expect(trackNames).toContain('.scale');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Storybook Stories
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { SpinningBox } from './spinning-box';
|
||||
|
||||
const meta: Meta<typeof SpinningBox> = {
|
||||
title: 'R3F/Animation/SpinningBox',
|
||||
component: SpinningBox,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Canvas style={{ height: 400 }}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[5, 5, 5]} />
|
||||
<Story />
|
||||
</Canvas>
|
||||
),
|
||||
],
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof SpinningBox>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const FastSpin: Story = {
|
||||
args: { speed: 5, axis: 'y' },
|
||||
};
|
||||
|
||||
export const XAxis: Story = {
|
||||
args: { speed: 2, axis: 'x' },
|
||||
};
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
When implementing R3F animations:
|
||||
|
||||
1. Use `useFrame` with delta time for all per-frame updates
|
||||
2. Pre-allocate math objects outside `useFrame` callbacks
|
||||
3. Use `useAnimations` for GLTF clip playback rather than manual mixer management
|
||||
4. Implement crossfade transitions between animation clips
|
||||
5. Gate animation logic on `active` flags rather than conditionally calling hooks
|
||||
6. Monitor performance with `@suaveplan/r3f-performance`
|
||||
7. Write Vitest tests for animation clip creation and hook behavior
|
||||
8. Write Storybook stories with Canvas decorator for visual verification
|
||||
9. Keep animation component files under 250 lines
|
||||
10. Document morph target names and clip names in `.md` files
|
||||
@@ -0,0 +1,522 @@
|
||||
---
|
||||
name: threejs-fundamentals
|
||||
description: Three.js fundamentals adapted for React Three Fiber and Suaveplan R3F packages. Covers the Canvas component, declarative scene graph composition, Object3D hierarchy as JSX, coordinate systems, camera setup, and @suaveplan/r3f core utilities.
|
||||
---
|
||||
|
||||
This skill covers Three.js fundamentals within the React Three Fiber (R3F) context used by the Suaveplan monorepo. All code targets **React 19 + TypeScript + R3F** and follows Suaveplan CLAUDE.md conventions.
|
||||
|
||||
The user needs to set up 3D scenes, understand the scene graph, work with coordinate systems, or configure the rendering pipeline in an R3F application.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Three.js Scene Architecture
|
||||
|
||||
Every Three.js application has three fundamental objects:
|
||||
|
||||
- **Scene**: The root container (a tree of `Object3D` nodes)
|
||||
- **Camera**: Defines the viewpoint (perspective or orthographic)
|
||||
- **Renderer**: Draws the scene from the camera's perspective using WebGL/WebGPU
|
||||
|
||||
### R3F Declarative Model
|
||||
|
||||
R3F replaces imperative Three.js setup with a React component tree. The `<Canvas>` component creates the renderer, scene, and default camera automatically.
|
||||
|
||||
| Three.js (Imperative) | R3F (Declarative) |
|
||||
|---|---|
|
||||
| `new THREE.Scene()` | `<Canvas>` (implicit) |
|
||||
| `new THREE.PerspectiveCamera(75, aspect, 0.1, 1000)` | `<Canvas camera={{ fov: 75, near: 0.1, far: 1000 }}>` |
|
||||
| `new THREE.WebGLRenderer({ antialias: true })` | `<Canvas gl={{ antialias: true }}>` |
|
||||
| `scene.add(mesh)` | `<mesh>` as JSX child |
|
||||
| `renderer.setSize(w, h)` | Automatic resize handling |
|
||||
| `requestAnimationFrame(render)` | Automatic render loop |
|
||||
|
||||
### Suaveplan R3F Packages
|
||||
|
||||
| Package | Purpose | Import |
|
||||
|---|---|---|
|
||||
| `@suaveplan/r3f` | Core utilities, scene context, helpers | `import { SceneProvider } from '@suaveplan/r3f'` |
|
||||
| `@suaveplan/r3f-camera` | Camera controllers | `import { OrbitControls } from '@suaveplan/r3f-camera'` |
|
||||
| `@suaveplan/r3f-performance` | FPS monitoring, adaptive quality | `import { PerformanceMonitor } from '@suaveplan/r3f-performance'` |
|
||||
|
||||
## Canvas: The R3F Entry Point
|
||||
|
||||
The `<Canvas>` component is the root of every R3F scene. It creates a WebGL context, sets up the render loop, and provides React context for all Three.js state.
|
||||
|
||||
### Basic Scene Setup
|
||||
|
||||
```tsx
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
|
||||
export function BasicScene() {
|
||||
return (
|
||||
<Canvas
|
||||
camera={{ position: [0, 2, 5], fov: 60 }}
|
||||
gl={{ antialias: true, toneMapping: 3 /* ACESFilmic */ }}
|
||||
style={{ width: '100%', height: '100vh' }}
|
||||
>
|
||||
<ambientLight intensity={0.4} />
|
||||
<directionalLight position={[5, 5, 5]} intensity={1} />
|
||||
<mesh position={[0, 0.5, 0]}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color="#e74c3c" />
|
||||
</mesh>
|
||||
<gridHelper args={[10, 10]} />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Canvas Props Reference
|
||||
|
||||
```tsx
|
||||
<Canvas
|
||||
// Camera configuration
|
||||
camera={{
|
||||
position: [0, 5, 10], // World position
|
||||
fov: 50, // Field of view (degrees)
|
||||
near: 0.1, // Near clipping plane
|
||||
far: 1000, // Far clipping plane
|
||||
zoom: 1, // Zoom level (orthographic)
|
||||
}}
|
||||
|
||||
// Renderer configuration
|
||||
gl={{
|
||||
antialias: true,
|
||||
alpha: false, // Transparent background
|
||||
toneMapping: 3, // ACESFilmicToneMapping
|
||||
toneMappingExposure: 1.0,
|
||||
outputColorSpace: 'srgb',
|
||||
}}
|
||||
|
||||
// Performance
|
||||
dpr={[1, 2]} // Device pixel ratio range
|
||||
frameloop="demand" // 'always' | 'demand' | 'never'
|
||||
performance={{ min: 0.5 }} // Adaptive DPR threshold
|
||||
|
||||
// Behavior
|
||||
shadows // Enable shadow maps
|
||||
flat // Disable tone mapping
|
||||
orthographic // Use orthographic camera
|
||||
linear // Disable sRGB color management
|
||||
|
||||
// Sizing
|
||||
style={{ width: '100%', height: '100vh' }}
|
||||
/>
|
||||
```
|
||||
|
||||
## Scene Graph as JSX
|
||||
|
||||
In R3F, the Three.js scene graph maps directly to JSX elements. Every Three.js class that extends `Object3D` can be used as a JSX element in lowercase.
|
||||
|
||||
### Object3D Hierarchy
|
||||
|
||||
```tsx
|
||||
{/* Three.js: const group = new Group(); scene.add(group); */}
|
||||
<group position={[0, 1, 0]} rotation={[0, Math.PI / 4, 0]}>
|
||||
|
||||
{/* Three.js: const mesh = new Mesh(geometry, material); group.add(mesh); */}
|
||||
<mesh position={[0, 0, 0]}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color="blue" />
|
||||
</mesh>
|
||||
|
||||
{/* Nested groups create parent-child transforms */}
|
||||
<group position={[2, 0, 0]}>
|
||||
<mesh>
|
||||
<sphereGeometry args={[0.5, 32, 32]} />
|
||||
<meshStandardMaterial color="green" />
|
||||
</mesh>
|
||||
</group>
|
||||
|
||||
</group>
|
||||
```
|
||||
|
||||
### JSX to Three.js Mapping
|
||||
|
||||
R3F automatically maps JSX elements to Three.js constructors:
|
||||
|
||||
| JSX Element | Three.js Class | Constructor Args |
|
||||
|---|---|---|
|
||||
| `<mesh>` | `new Mesh()` | None (geometry + material as children) |
|
||||
| `<group>` | `new Group()` | None |
|
||||
| `<points>` | `new Points()` | None |
|
||||
| `<line>` | `new Line()` | None |
|
||||
| `<lineSegments>` | `new LineSegments()` | None |
|
||||
| `<sprite>` | `new Sprite()` | None |
|
||||
| `<instancedMesh args={[null, null, count]}>` | `new InstancedMesh(g, m, count)` | Geometry, Material, Count |
|
||||
| `<primitive object={obj}>` | Direct insertion | Existing Object3D |
|
||||
|
||||
### Transform Props
|
||||
|
||||
Every R3F element that maps to an `Object3D` accepts transform props:
|
||||
|
||||
```tsx
|
||||
<mesh
|
||||
position={[x, y, z]} // Vector3 or [number, number, number]
|
||||
rotation={[x, y, z]} // Euler or [number, number, number]
|
||||
scale={[x, y, z]} // Vector3 or number (uniform scale)
|
||||
quaternion={[x, y, z, w]} // Quaternion rotation
|
||||
visible={true} // Visibility toggle
|
||||
frustumCulled={true} // Frustum culling
|
||||
renderOrder={0} // Render ordering
|
||||
castShadow // Cast shadows
|
||||
receiveShadow // Receive shadows
|
||||
name="myMesh" // Object name for debugging
|
||||
userData={{ id: 'custom' }} // Custom data
|
||||
/>
|
||||
```
|
||||
|
||||
## Coordinate System
|
||||
|
||||
Three.js uses a **right-handed coordinate system**:
|
||||
|
||||
```text
|
||||
Y (up)
|
||||
|
|
||||
|
|
||||
+---- X (right)
|
||||
/
|
||||
Z (toward viewer)
|
||||
```
|
||||
|
||||
- **X**: Positive to the right
|
||||
- **Y**: Positive upward
|
||||
- **Z**: Positive toward the viewer (out of the screen)
|
||||
|
||||
### Units and Scale
|
||||
|
||||
Three.js is unit-agnostic. By convention, 1 unit = 1 meter for physics and VR compatibility:
|
||||
|
||||
```tsx
|
||||
// A 2m tall character standing on a 10m x 10m ground plane
|
||||
<group>
|
||||
<mesh position={[0, 1, 0]}> {/* Center at 1m (half height) */}
|
||||
<boxGeometry args={[0.5, 2, 0.3]} /> {/* 0.5m wide, 2m tall, 0.3m deep */}
|
||||
<meshStandardMaterial color="tan" />
|
||||
</mesh>
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]}> {/* Rotate to be flat */}
|
||||
<planeGeometry args={[10, 10]} />
|
||||
<meshStandardMaterial color="forestgreen" />
|
||||
</mesh>
|
||||
</group>
|
||||
```
|
||||
|
||||
## Accessing Three.js State
|
||||
|
||||
### useThree Hook
|
||||
|
||||
Access the full Three.js state from any R3F child component:
|
||||
|
||||
```tsx
|
||||
import { useThree } from '@react-three/fiber';
|
||||
|
||||
export function SceneInfo() {
|
||||
const { scene, camera, gl, size, viewport, clock } = useThree();
|
||||
|
||||
// scene: THREE.Scene instance
|
||||
// camera: Active camera
|
||||
// gl: WebGLRenderer instance
|
||||
// size: { width, height } in pixels
|
||||
// viewport: { width, height } in Three.js units at camera distance
|
||||
// clock: THREE.Clock instance
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
### Imperative Escape Hatch with Refs
|
||||
|
||||
When you need direct Three.js API access:
|
||||
|
||||
```tsx
|
||||
import { useRef, useEffect } from 'react';
|
||||
import type { Mesh, MeshStandardMaterial } from 'three';
|
||||
|
||||
export function ImperativeMesh() {
|
||||
const meshRef = useRef<Mesh>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!meshRef.current) return;
|
||||
|
||||
// Full imperative Three.js API access
|
||||
const mesh = meshRef.current;
|
||||
mesh.geometry.computeBoundingBox();
|
||||
const box = mesh.geometry.boundingBox;
|
||||
|
||||
const material = mesh.material as MeshStandardMaterial;
|
||||
material.needsUpdate = true;
|
||||
|
||||
// Traverse children
|
||||
mesh.traverse((child) => {
|
||||
if (child instanceof Mesh) {
|
||||
child.castShadow = true;
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<mesh ref={meshRef}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color="coral" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## @suaveplan/r3f Core Utilities
|
||||
|
||||
The `@suaveplan/r3f` package provides scene-level context and shared utilities:
|
||||
|
||||
```tsx
|
||||
import { SceneProvider, useSceneContext } from '@suaveplan/r3f';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<Canvas>
|
||||
<SceneProvider config={{ units: 'meters', upAxis: 'y' }}>
|
||||
<SceneContent />
|
||||
</SceneProvider>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
|
||||
function SceneContent() {
|
||||
const { config } = useSceneContext();
|
||||
// Access shared scene configuration
|
||||
return <>{/* scene objects */}</>;
|
||||
}
|
||||
```
|
||||
|
||||
## Render Modes
|
||||
|
||||
### On-Demand Rendering
|
||||
|
||||
For scenes that only change on interaction, use `frameloop="demand"` to save GPU cycles:
|
||||
|
||||
```tsx
|
||||
import { Canvas, invalidate } from '@react-three/fiber';
|
||||
|
||||
export function OnDemandScene() {
|
||||
return (
|
||||
<Canvas frameloop="demand">
|
||||
<InteractiveObject />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
|
||||
function InteractiveObject() {
|
||||
return (
|
||||
<mesh
|
||||
onClick={(e) => {
|
||||
e.object.rotation.y += 0.5;
|
||||
invalidate(); // Request a re-render
|
||||
}}
|
||||
>
|
||||
<boxGeometry />
|
||||
<meshStandardMaterial color="hotpink" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Adaptive Performance
|
||||
|
||||
```tsx
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { PerformanceMonitor } from '@suaveplan/r3f-performance';
|
||||
|
||||
export function AdaptiveScene() {
|
||||
return (
|
||||
<Canvas dpr={[1, 2]} performance={{ min: 0.5 }}>
|
||||
<PerformanceMonitor
|
||||
onDecline={() => { /* reduce quality */ }}
|
||||
onIncline={() => { /* increase quality */ }}
|
||||
/>
|
||||
{/* scene content */}
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Scene Organization Patterns
|
||||
|
||||
### Layer-Based Architecture
|
||||
|
||||
```tsx
|
||||
export function GameScene() {
|
||||
return (
|
||||
<Canvas shadows camera={{ position: [0, 10, 20], fov: 50 }}>
|
||||
{/* Environment layer */}
|
||||
<group name="environment">
|
||||
<ambientLight intensity={0.3} />
|
||||
<directionalLight position={[10, 10, 5]} castShadow />
|
||||
<Sky />
|
||||
<Ground />
|
||||
</group>
|
||||
|
||||
{/* Static geometry layer */}
|
||||
<group name="static">
|
||||
<Buildings />
|
||||
<Terrain />
|
||||
</group>
|
||||
|
||||
{/* Dynamic objects layer */}
|
||||
<group name="dynamic">
|
||||
<Characters />
|
||||
<Vehicles />
|
||||
<Projectiles />
|
||||
</group>
|
||||
|
||||
{/* UI overlay layer */}
|
||||
<group name="ui">
|
||||
<HUD />
|
||||
<Markers />
|
||||
</group>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Suspense for Loading
|
||||
|
||||
```tsx
|
||||
import { Suspense } from 'react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
|
||||
function LoadingFallback() {
|
||||
return (
|
||||
<mesh>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshBasicMaterial color="gray" wireframe />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
export function SceneWithLoading() {
|
||||
return (
|
||||
<Canvas>
|
||||
<ambientLight />
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<HeavyModel url="/models/scene.glb" />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Color Management
|
||||
|
||||
R3F defaults to sRGB color management. Colors specified in JSX are treated as sRGB and converted to linear for rendering.
|
||||
|
||||
```tsx
|
||||
// sRGB colors (default) - what you see in design tools
|
||||
<meshStandardMaterial color="#ff6600" />
|
||||
<meshStandardMaterial color="tomato" />
|
||||
<meshStandardMaterial color={new Color(1, 0.4, 0)} /> // Linear!
|
||||
|
||||
// For linear workflow (textures already linear)
|
||||
<Canvas linear>
|
||||
{/* Colors not converted */}
|
||||
</Canvas>
|
||||
|
||||
// Tone mapping options
|
||||
<Canvas gl={{ toneMapping: ACESFilmicToneMapping, toneMappingExposure: 1.2 }}>
|
||||
{/* Scene with filmic tone mapping */}
|
||||
</Canvas>
|
||||
```
|
||||
|
||||
## Disposal and Cleanup
|
||||
|
||||
R3F automatically disposes of geometries, materials, and textures when components unmount. For manual control:
|
||||
|
||||
```tsx
|
||||
import { useEffect } from 'react';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
|
||||
export function CleanupExample() {
|
||||
const { scene } = useThree();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Manual cleanup for imperatively created resources
|
||||
scene.traverse((object) => {
|
||||
if (object instanceof Mesh) {
|
||||
object.geometry.dispose();
|
||||
if (Array.isArray(object.material)) {
|
||||
object.material.forEach((m) => m.dispose());
|
||||
} else {
|
||||
object.material.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [scene]);
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Fundamentals
|
||||
|
||||
### Vitest Scene Tests
|
||||
|
||||
```tsx
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createRoot } from '@react-three/fiber';
|
||||
import { Scene } from 'three';
|
||||
|
||||
describe('BasicScene', () => {
|
||||
it('renders without crashing', () => {
|
||||
const scene = new Scene();
|
||||
// Use @react-three/test-renderer for component testing
|
||||
expect(scene.children).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Storybook Scene Stories
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { OrbitControls } from '@react-three/drei';
|
||||
|
||||
function SceneTemplate() {
|
||||
return (
|
||||
<Canvas camera={{ position: [3, 3, 3] }} style={{ height: 400 }}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[5, 5, 5]} />
|
||||
<mesh>
|
||||
<boxGeometry />
|
||||
<meshStandardMaterial color="royalblue" />
|
||||
</mesh>
|
||||
<gridHelper />
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'R3F/Fundamentals/Scene',
|
||||
component: SceneTemplate,
|
||||
};
|
||||
export default meta;
|
||||
|
||||
export const Default: StoryObj = {};
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
When setting up an R3F scene:
|
||||
|
||||
1. Use `<Canvas>` as the root with appropriate camera and renderer config
|
||||
2. Structure scene graph with named `<group>` layers
|
||||
3. Set `dpr={[1, 2]}` for responsive pixel ratio
|
||||
4. Wrap heavy models in `<Suspense>` with fallback geometry
|
||||
5. Use `useThree` for state access, refs for imperative escape hatches
|
||||
6. Enable shadows on Canvas and on individual lights/meshes
|
||||
7. Configure tone mapping and color space for the project
|
||||
8. Use `@suaveplan/r3f` SceneProvider for shared configuration
|
||||
9. Use `@suaveplan/r3f-performance` for adaptive quality
|
||||
10. Keep scene component files under 250 lines, split into sub-components
|
||||
@@ -0,0 +1,576 @@
|
||||
---
|
||||
name: threejs-geometry
|
||||
description: Three.js geometry creation and manipulation adapted for React Three Fiber and Suaveplan R3F packages. Covers declarative mesh/geometry JSX, BufferGeometry with React refs, InstancedMesh patterns for high-performance rendering, custom geometry construction, and @react-three/drei geometry helpers.
|
||||
---
|
||||
|
||||
This skill covers geometry creation and manipulation in Three.js within the React Three Fiber (R3F) context used by the Suaveplan monorepo. All code targets **React 19 + TypeScript + R3F** and follows Suaveplan CLAUDE.md conventions.
|
||||
|
||||
The user needs to create, modify, or optimize 3D geometry in an R3F scene. This includes built-in primitives, custom BufferGeometry, instanced rendering, and parametric shapes.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Three.js Geometry System
|
||||
|
||||
Three.js geometry is based on `BufferGeometry`, which stores vertex data in typed arrays:
|
||||
|
||||
- **position**: `Float32Array` of `[x, y, z]` per vertex
|
||||
- **normal**: `Float32Array` of `[nx, ny, nz]` per vertex
|
||||
- **uv**: `Float32Array` of `[u, v]` per vertex
|
||||
- **index**: `Uint16Array` or `Uint32Array` of triangle vertex indices
|
||||
- **color**: `Float32Array` of `[r, g, b]` per vertex (optional)
|
||||
|
||||
### R3F Geometry Approach
|
||||
|
||||
In R3F, geometries are declared as JSX children of `<mesh>`:
|
||||
|
||||
| Three.js (Imperative) | R3F (Declarative) |
|
||||
|---|---|
|
||||
| `new BoxGeometry(1, 1, 1)` | `<boxGeometry args={[1, 1, 1]} />` |
|
||||
| `mesh.geometry = geometry` | `<mesh><boxGeometry /></mesh>` |
|
||||
| `geometry.setAttribute(...)` | Via ref or attach prop |
|
||||
| `new InstancedMesh(g, m, n)` | `<instancedMesh args={[null, null, n]}>` |
|
||||
|
||||
### Suaveplan R3F Packages
|
||||
|
||||
| Package | Purpose | Import |
|
||||
|---|---|---|
|
||||
| `@suaveplan/r3f` | Core utilities, geometry helpers | `import { useR3F } from '@suaveplan/r3f'` |
|
||||
| `@suaveplan/r3f-performance` | Geometry LOD, draw call monitoring | `import { usePerformanceMonitor } from '@suaveplan/r3f-performance'` |
|
||||
|
||||
## Built-in Geometry Primitives
|
||||
|
||||
All Three.js geometry classes are available as lowercase JSX elements. Constructor arguments pass via `args`.
|
||||
|
||||
### Box
|
||||
|
||||
```tsx
|
||||
<mesh>
|
||||
<boxGeometry args={[
|
||||
1, // width
|
||||
1, // height
|
||||
1, // depth
|
||||
1, // widthSegments (optional)
|
||||
1, // heightSegments (optional)
|
||||
1, // depthSegments (optional)
|
||||
]} />
|
||||
<meshStandardMaterial color="tomato" />
|
||||
</mesh>
|
||||
```
|
||||
|
||||
### Sphere
|
||||
|
||||
```tsx
|
||||
<mesh>
|
||||
<sphereGeometry args={[
|
||||
0.5, // radius
|
||||
32, // widthSegments
|
||||
32, // heightSegments
|
||||
0, // phiStart (optional)
|
||||
Math.PI * 2, // phiLength (optional)
|
||||
0, // thetaStart (optional)
|
||||
Math.PI, // thetaLength (optional)
|
||||
]} />
|
||||
<meshStandardMaterial color="dodgerblue" />
|
||||
</mesh>
|
||||
```
|
||||
|
||||
### Common Primitives Reference
|
||||
|
||||
| Geometry | JSX | Key Args |
|
||||
|---|---|---|
|
||||
| Box | `<boxGeometry args={[w, h, d]} />` | width, height, depth |
|
||||
| Sphere | `<sphereGeometry args={[r, wSeg, hSeg]} />` | radius, segments |
|
||||
| Cylinder | `<cylinderGeometry args={[rTop, rBot, h, rSeg]} />` | radii, height, segments |
|
||||
| Cone | `<coneGeometry args={[r, h, rSeg]} />` | radius, height |
|
||||
| Torus | `<torusGeometry args={[r, tube, rSeg, tSeg]} />` | radius, tube radius |
|
||||
| Plane | `<planeGeometry args={[w, h, wSeg, hSeg]} />` | width, height |
|
||||
| Ring | `<ringGeometry args={[inner, outer, tSeg]} />` | inner/outer radius |
|
||||
| Circle | `<circleGeometry args={[r, seg]} />` | radius, segments |
|
||||
| TorusKnot | `<torusKnotGeometry args={[r, tube, tSeg, rSeg, p, q]} />` | radius, tube, knot params |
|
||||
| Dodecahedron | `<dodecahedronGeometry args={[r, detail]} />` | radius, detail level |
|
||||
| Icosahedron | `<icosahedronGeometry args={[r, detail]} />` | radius, detail level |
|
||||
| Octahedron | `<octahedronGeometry args={[r, detail]} />` | radius, detail level |
|
||||
| Tetrahedron | `<tetrahedronGeometry args={[r, detail]} />` | radius, detail level |
|
||||
|
||||
## Custom BufferGeometry
|
||||
|
||||
### Declarative with Refs
|
||||
|
||||
```tsx
|
||||
import { useRef, useEffect, useMemo } from 'react';
|
||||
import { BufferAttribute } from 'three';
|
||||
import type { BufferGeometry } from 'three';
|
||||
|
||||
interface CustomTriangleProps {
|
||||
size?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export function CustomTriangle({ size = 1, color = 'limegreen' }: CustomTriangleProps) {
|
||||
const geometryRef = useRef<BufferGeometry>(null);
|
||||
|
||||
const { positions, normals, uvs } = useMemo(() => {
|
||||
const s = size;
|
||||
return {
|
||||
positions: new Float32Array([
|
||||
-s, -s, 0, // bottom-left
|
||||
s, -s, 0, // bottom-right
|
||||
0, s, 0, // top-center
|
||||
]),
|
||||
normals: new Float32Array([
|
||||
0, 0, 1,
|
||||
0, 0, 1,
|
||||
0, 0, 1,
|
||||
]),
|
||||
uvs: new Float32Array([
|
||||
0, 0,
|
||||
1, 0,
|
||||
0.5, 1,
|
||||
]),
|
||||
};
|
||||
}, [size]);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<bufferGeometry ref={geometryRef}>
|
||||
<bufferAttribute
|
||||
attach="attributes-position"
|
||||
args={[positions, 3]}
|
||||
/>
|
||||
<bufferAttribute
|
||||
attach="attributes-normal"
|
||||
args={[normals, 3]}
|
||||
/>
|
||||
<bufferAttribute
|
||||
attach="attributes-uv"
|
||||
args={[uvs, 2]}
|
||||
/>
|
||||
</bufferGeometry>
|
||||
<meshStandardMaterial color={color} side={2} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Indexed Geometry
|
||||
|
||||
For geometry with shared vertices (reduces memory and improves performance):
|
||||
|
||||
```tsx
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function IndexedQuad({ size = 1 }: { size?: number }) {
|
||||
const { positions, indices, uvs } = useMemo(() => {
|
||||
const s = size / 2;
|
||||
return {
|
||||
positions: new Float32Array([
|
||||
-s, -s, 0, // 0: bottom-left
|
||||
s, -s, 0, // 1: bottom-right
|
||||
s, s, 0, // 2: top-right
|
||||
-s, s, 0, // 3: top-left
|
||||
]),
|
||||
indices: new Uint16Array([
|
||||
0, 1, 2, // first triangle
|
||||
0, 2, 3, // second triangle
|
||||
]),
|
||||
uvs: new Float32Array([
|
||||
0, 0,
|
||||
1, 0,
|
||||
1, 1,
|
||||
0, 1,
|
||||
]),
|
||||
};
|
||||
}, [size]);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<bufferGeometry>
|
||||
<bufferAttribute attach="attributes-position" args={[positions, 3]} />
|
||||
<bufferAttribute attach="attributes-uv" args={[uvs, 2]} />
|
||||
<bufferAttribute attach="index" args={[indices, 1]} />
|
||||
</bufferGeometry>
|
||||
<meshStandardMaterial color="coral" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Geometry Updates
|
||||
|
||||
### Animating Vertices in useFrame
|
||||
|
||||
```tsx
|
||||
import { useRef, useMemo } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import type { BufferGeometry } from 'three';
|
||||
|
||||
export function WavingPlane({ width = 5, segments = 32 }: { width?: number; segments?: number }) {
|
||||
const geometryRef = useRef<BufferGeometry>(null);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!geometryRef.current) return;
|
||||
|
||||
const positions = geometryRef.current.attributes.position;
|
||||
const time = clock.elapsedTime;
|
||||
|
||||
for (let i = 0; i < positions.count; i++) {
|
||||
const x = positions.getX(i);
|
||||
const z = positions.getZ(i);
|
||||
const y = Math.sin(x * 2 + time) * 0.3 + Math.cos(z * 2 + time * 0.7) * 0.2;
|
||||
positions.setY(i, y);
|
||||
}
|
||||
|
||||
positions.needsUpdate = true;
|
||||
geometryRef.current.computeVertexNormals();
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<planeGeometry ref={geometryRef} args={[width, width, segments, segments]} />
|
||||
<meshStandardMaterial color="steelblue" wireframe />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## InstancedMesh: High-Performance Rendering
|
||||
|
||||
`InstancedMesh` renders many copies of the same geometry with a single draw call. Each instance can have its own transform and color.
|
||||
|
||||
### Basic Instancing
|
||||
|
||||
```tsx
|
||||
import { useRef, useEffect, useMemo } from 'react';
|
||||
import { Object3D, Color, Matrix4 } from 'three';
|
||||
import type { InstancedMesh } from 'three';
|
||||
|
||||
interface InstanceFieldProps {
|
||||
count: number;
|
||||
spread?: number;
|
||||
}
|
||||
|
||||
export function InstanceField({ count, spread = 10 }: InstanceFieldProps) {
|
||||
const meshRef = useRef<InstancedMesh>(null);
|
||||
const dummy = useMemo(() => new Object3D(), []);
|
||||
const color = useMemo(() => new Color(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!meshRef.current) return;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
dummy.position.set(
|
||||
(Math.random() - 0.5) * spread,
|
||||
Math.random() * 2,
|
||||
(Math.random() - 0.5) * spread
|
||||
);
|
||||
dummy.rotation.set(
|
||||
Math.random() * Math.PI,
|
||||
Math.random() * Math.PI,
|
||||
0
|
||||
);
|
||||
dummy.scale.setScalar(0.2 + Math.random() * 0.8);
|
||||
dummy.updateMatrix();
|
||||
meshRef.current.setMatrixAt(i, dummy.matrix);
|
||||
|
||||
color.setHSL(Math.random(), 0.7, 0.5);
|
||||
meshRef.current.setColorAt(i, color);
|
||||
}
|
||||
|
||||
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||
if (meshRef.current.instanceColor) {
|
||||
meshRef.current.instanceColor.needsUpdate = true;
|
||||
}
|
||||
}, [count, spread, dummy, color]);
|
||||
|
||||
return (
|
||||
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial />
|
||||
</instancedMesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Animated Instances
|
||||
|
||||
```tsx
|
||||
import { useRef, useMemo } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { Object3D } from 'three';
|
||||
import type { InstancedMesh } from 'three';
|
||||
|
||||
export function FloatingParticles({ count = 500 }: { count?: number }) {
|
||||
const meshRef = useRef<InstancedMesh>(null);
|
||||
const dummy = useMemo(() => new Object3D(), []);
|
||||
|
||||
const offsets = useMemo(() => {
|
||||
return Array.from({ length: count }, () => ({
|
||||
x: (Math.random() - 0.5) * 20,
|
||||
y: Math.random() * 10,
|
||||
z: (Math.random() - 0.5) * 20,
|
||||
speed: 0.2 + Math.random() * 0.8,
|
||||
phase: Math.random() * Math.PI * 2,
|
||||
}));
|
||||
}, [count]);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!meshRef.current) return;
|
||||
const t = clock.elapsedTime;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const { x, y, z, speed, phase } = offsets[i];
|
||||
dummy.position.set(
|
||||
x + Math.sin(t * speed + phase) * 0.5,
|
||||
y + Math.sin(t * speed * 0.7 + phase) * 0.3,
|
||||
z + Math.cos(t * speed + phase) * 0.5
|
||||
);
|
||||
dummy.scale.setScalar(0.05 + Math.sin(t * speed + phase) * 0.02);
|
||||
dummy.updateMatrix();
|
||||
meshRef.current.setMatrixAt(i, dummy.matrix);
|
||||
}
|
||||
|
||||
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||
});
|
||||
|
||||
return (
|
||||
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
|
||||
<sphereGeometry args={[1, 8, 8]} />
|
||||
<meshBasicMaterial color="white" transparent opacity={0.6} />
|
||||
</instancedMesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Drei Geometry Helpers
|
||||
|
||||
`@react-three/drei` provides higher-level geometry components:
|
||||
|
||||
### RoundedBox
|
||||
|
||||
```tsx
|
||||
import { RoundedBox } from '@react-three/drei';
|
||||
|
||||
<RoundedBox args={[1, 1, 1]} radius={0.1} smoothness={4}>
|
||||
<meshStandardMaterial color="royalblue" />
|
||||
</RoundedBox>
|
||||
```
|
||||
|
||||
### Text3D
|
||||
|
||||
```tsx
|
||||
import { Text3D, Center } from '@react-three/drei';
|
||||
|
||||
<Center>
|
||||
<Text3D
|
||||
font="/fonts/helvetiker_regular.typeface.json"
|
||||
size={0.5}
|
||||
height={0.1}
|
||||
bevelEnabled
|
||||
bevelSize={0.02}
|
||||
bevelThickness={0.01}
|
||||
>
|
||||
Hello R3F
|
||||
<meshStandardMaterial color="gold" />
|
||||
</Text3D>
|
||||
</Center>
|
||||
```
|
||||
|
||||
### Parametric Shapes
|
||||
|
||||
```tsx
|
||||
import { Extrude, Lathe } from '@react-three/drei';
|
||||
import { Shape, Vector2 } from 'three';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function CustomExtrusion() {
|
||||
const shape = useMemo(() => {
|
||||
const s = new Shape();
|
||||
s.moveTo(0, 0);
|
||||
s.lineTo(1, 0);
|
||||
s.lineTo(1, 1);
|
||||
s.lineTo(0.5, 1.5);
|
||||
s.lineTo(0, 1);
|
||||
s.closePath();
|
||||
return s;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Extrude args={[shape, { depth: 0.5, bevelEnabled: true, bevelSize: 0.05 }]}>
|
||||
<meshStandardMaterial color="mediumpurple" />
|
||||
</Extrude>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Geometry Merging for Performance
|
||||
|
||||
Merge static geometries to reduce draw calls:
|
||||
|
||||
```tsx
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { BoxGeometry, Matrix4 } from 'three';
|
||||
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
|
||||
import type { BufferGeometry } from 'three';
|
||||
|
||||
export function MergedBoxes({ positions }: { positions: [number, number, number][] }) {
|
||||
const geometryRef = useRef<BufferGeometry>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const geometries = positions.map(([x, y, z]) => {
|
||||
const box = new BoxGeometry(1, 1, 1);
|
||||
box.applyMatrix4(new Matrix4().makeTranslation(x, y, z));
|
||||
return box;
|
||||
});
|
||||
|
||||
const merged = mergeGeometries(geometries, false);
|
||||
if (merged && geometryRef.current) {
|
||||
geometryRef.current.copy(merged);
|
||||
geometries.forEach((g) => g.dispose());
|
||||
merged.dispose();
|
||||
}
|
||||
}, [positions]);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<bufferGeometry ref={geometryRef} />
|
||||
<meshStandardMaterial color="sandybrown" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Geometry Bounding Volumes
|
||||
|
||||
Bounding volumes are essential for culling, collision detection, and raycasting:
|
||||
|
||||
```tsx
|
||||
import { useRef, useEffect } from 'react';
|
||||
import type { Mesh } from 'three';
|
||||
|
||||
export function BoundedMesh() {
|
||||
const meshRef = useRef<Mesh>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!meshRef.current) return;
|
||||
const geometry = meshRef.current.geometry;
|
||||
|
||||
// Compute bounding box
|
||||
geometry.computeBoundingBox();
|
||||
const box = geometry.boundingBox;
|
||||
// box.min, box.max, box.getCenter(), box.getSize()
|
||||
|
||||
// Compute bounding sphere
|
||||
geometry.computeBoundingSphere();
|
||||
const sphere = geometry.boundingSphere;
|
||||
// sphere.center, sphere.radius
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<mesh ref={meshRef}>
|
||||
<torusKnotGeometry args={[1, 0.3, 128, 32]} />
|
||||
<meshStandardMaterial color="orchid" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Level of Detail (LOD)
|
||||
|
||||
Use `<Detailed>` from drei or Three.js LOD for distance-based geometry switching:
|
||||
|
||||
```tsx
|
||||
import { Detailed } from '@react-three/drei';
|
||||
|
||||
export function LODObject() {
|
||||
return (
|
||||
<Detailed distances={[0, 10, 25, 50]}>
|
||||
{/* Closest: high detail */}
|
||||
<mesh><sphereGeometry args={[1, 64, 64]} /><meshStandardMaterial /></mesh>
|
||||
{/* Medium distance */}
|
||||
<mesh><sphereGeometry args={[1, 32, 32]} /><meshStandardMaterial /></mesh>
|
||||
{/* Far distance */}
|
||||
<mesh><sphereGeometry args={[1, 16, 16]} /><meshStandardMaterial /></mesh>
|
||||
{/* Very far: billboard or point */}
|
||||
<mesh><sphereGeometry args={[1, 8, 8]} /><meshStandardMaterial /></mesh>
|
||||
</Detailed>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Geometry
|
||||
|
||||
### Vitest Tests
|
||||
|
||||
```tsx
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { BoxGeometry, SphereGeometry } from 'three';
|
||||
|
||||
describe('Geometry fundamentals', () => {
|
||||
it('creates box with correct vertex count', () => {
|
||||
const geom = new BoxGeometry(1, 1, 1);
|
||||
expect(geom.attributes.position.count).toBe(24); // 6 faces x 4 vertices
|
||||
});
|
||||
|
||||
it('creates indexed sphere', () => {
|
||||
const geom = new SphereGeometry(1, 16, 16);
|
||||
expect(geom.index).not.toBeNull();
|
||||
expect(geom.attributes.position.count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('computes bounding box', () => {
|
||||
const geom = new BoxGeometry(2, 3, 4);
|
||||
geom.computeBoundingBox();
|
||||
expect(geom.boundingBox!.max.x).toBeCloseTo(1);
|
||||
expect(geom.boundingBox!.max.y).toBeCloseTo(1.5);
|
||||
expect(geom.boundingBox!.max.z).toBeCloseTo(2);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Storybook Stories
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { OrbitControls } from '@react-three/drei';
|
||||
import { InstanceField } from './instance-field';
|
||||
|
||||
const meta: Meta<typeof InstanceField> = {
|
||||
title: 'R3F/Geometry/InstanceField',
|
||||
component: InstanceField,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Canvas camera={{ position: [5, 5, 5] }} style={{ height: 400 }}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[5, 5, 5]} />
|
||||
<Story />
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
),
|
||||
],
|
||||
};
|
||||
export default meta;
|
||||
|
||||
export const Default: StoryObj<typeof InstanceField> = {
|
||||
args: { count: 100, spread: 10 },
|
||||
};
|
||||
|
||||
export const Dense: StoryObj<typeof InstanceField> = {
|
||||
args: { count: 1000, spread: 5 },
|
||||
};
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
When working with geometry in R3F:
|
||||
|
||||
1. Use built-in geometry JSX elements with `args` for standard shapes
|
||||
2. Use `useMemo` for custom BufferGeometry data arrays
|
||||
3. Set `needsUpdate = true` on attributes after modifying vertex data
|
||||
4. Use InstancedMesh for rendering many identical objects (1 draw call)
|
||||
5. Pre-allocate `Object3D` and `Color` objects outside `useFrame` for instancing
|
||||
6. Merge static geometries with `mergeGeometries` to reduce draw calls
|
||||
7. Compute bounding volumes after geometry modifications
|
||||
8. Use LOD components for distance-based detail levels
|
||||
9. Index geometry when vertices are shared between triangles
|
||||
10. Keep geometry component files under 250 lines
|
||||
@@ -0,0 +1,487 @@
|
||||
---
|
||||
name: threejs-interaction
|
||||
description: Three.js interaction and event handling adapted for React Three Fiber and Suaveplan R3F packages. Covers R3F pointer events (onClick, onPointerOver), raycasting via the R3F event system, @suaveplan/r3f-camera for orbit/first-person controls, drag interactions, hover effects, and input handling patterns.
|
||||
---
|
||||
|
||||
This skill covers user interaction with 3D objects in Three.js within the React Three Fiber (R3F) context used by the Suaveplan monorepo. All code targets **React 19 + TypeScript + R3F** and follows Suaveplan CLAUDE.md conventions.
|
||||
|
||||
The user needs to make 3D objects interactive: clickable, hoverable, draggable, or responsive to pointer/touch input. They may need camera controls, raycasting, or input mapping.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Three.js Raycasting
|
||||
|
||||
Three.js uses `Raycaster` to detect which objects a ray intersects. In vanilla Three.js this requires manual setup:
|
||||
|
||||
1. Convert mouse coordinates to normalized device coordinates (NDC)
|
||||
2. Cast a ray from the camera through the NDC point
|
||||
3. Test intersections against scene objects
|
||||
4. Handle the intersection results
|
||||
|
||||
### R3F Event System
|
||||
|
||||
R3F abstracts all raycasting into React-style event handlers on mesh elements. No manual raycaster setup required.
|
||||
|
||||
| Three.js (Imperative) | R3F (Declarative) |
|
||||
|---|---|
|
||||
| `raycaster.setFromCamera(mouse, camera)` | Automatic |
|
||||
| `raycaster.intersectObjects(scene.children)` | Automatic per frame |
|
||||
| Custom event dispatch | `onClick`, `onPointerOver`, etc. |
|
||||
| `addEventListener('mousemove', ...)` | `onPointerMove` on mesh |
|
||||
| Manual NDC conversion | Automatic coordinate mapping |
|
||||
|
||||
### Suaveplan R3F Packages
|
||||
|
||||
| Package | Purpose | Import |
|
||||
|---|---|---|
|
||||
| `@suaveplan/r3f` | Core interaction utilities | `import { useR3F } from '@suaveplan/r3f'` |
|
||||
| `@suaveplan/r3f-camera` | Camera controls (orbit, FPS, TPS) | `import { OrbitCamera, FirstPersonCamera } from '@suaveplan/r3f-camera'` |
|
||||
| `@suaveplan/r3f-booking` | Interactive 3D venue maps | `import { VenueMap } from '@suaveplan/r3f-booking'` |
|
||||
|
||||
## R3F Pointer Events
|
||||
|
||||
Every `<mesh>`, `<group>`, or `<primitive>` in R3F can receive pointer events. These work identically to React DOM events.
|
||||
|
||||
### Available Events
|
||||
|
||||
```tsx
|
||||
<mesh
|
||||
onClick={(e) => {}} // Click (pointerdown + pointerup)
|
||||
onContextMenu={(e) => {}} // Right-click
|
||||
onDoubleClick={(e) => {}} // Double-click
|
||||
onPointerDown={(e) => {}} // Pointer pressed
|
||||
onPointerUp={(e) => {}} // Pointer released
|
||||
onPointerOver={(e) => {}} // Pointer enters object
|
||||
onPointerOut={(e) => {}} // Pointer leaves object
|
||||
onPointerEnter={(e) => {}} // Pointer enters (no bubbling)
|
||||
onPointerLeave={(e) => {}} // Pointer leaves (no bubbling)
|
||||
onPointerMove={(e) => {}} // Pointer moves over object
|
||||
onPointerMissed={() => {}} // Click missed this object
|
||||
onWheel={(e) => {}} // Scroll wheel
|
||||
/>
|
||||
```
|
||||
|
||||
### Event Object Properties
|
||||
|
||||
The R3F event object extends the Three.js intersection result:
|
||||
|
||||
```tsx
|
||||
interface ThreeEvent {
|
||||
// Three.js intersection data
|
||||
object: Object3D; // The intersected object
|
||||
point: Vector3; // World-space intersection point
|
||||
distance: number; // Distance from camera
|
||||
face: Face | null; // Intersected face
|
||||
faceIndex: number; // Face index
|
||||
uv: Vector2; // UV coordinates at intersection
|
||||
uv1: Vector2; // Second UV set
|
||||
|
||||
// R3F additions
|
||||
eventObject: Object3D; // The object with the event handler
|
||||
camera: Camera; // Active camera
|
||||
delta: number; // Distance moved (for click threshold)
|
||||
ray: Ray; // The casting ray
|
||||
intersections: Intersection[]; // All intersections along the ray
|
||||
stopped: boolean; // Whether propagation was stopped
|
||||
|
||||
// DOM event
|
||||
nativeEvent: PointerEvent; // Original DOM event
|
||||
|
||||
// Methods
|
||||
stopPropagation(): void; // Stop event bubbling
|
||||
}
|
||||
```
|
||||
|
||||
### Basic Click Interaction
|
||||
|
||||
```tsx
|
||||
import { useState, useCallback } from 'react';
|
||||
import type { ThreeEvent } from '@react-three/fiber';
|
||||
|
||||
interface ClickableBoxProps {
|
||||
position?: [number, number, number];
|
||||
onSelect?: (point: [number, number, number]) => void;
|
||||
}
|
||||
|
||||
export function ClickableBox({ position = [0, 0, 0], onSelect }: ClickableBoxProps) {
|
||||
const [active, setActive] = useState(false);
|
||||
|
||||
const handleClick = useCallback((event: ThreeEvent<PointerEvent>) => {
|
||||
event.stopPropagation(); // Prevent event from reaching objects behind
|
||||
setActive((prev) => !prev);
|
||||
const p = event.point;
|
||||
onSelect?.([p.x, p.y, p.z]);
|
||||
}, [onSelect]);
|
||||
|
||||
return (
|
||||
<mesh position={position} onClick={handleClick} scale={active ? 1.2 : 1}>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color={active ? 'hotpink' : 'royalblue'} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Hover Effects
|
||||
|
||||
```tsx
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export function HoverableSphere({ position }: { position: [number, number, number] }) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const handlePointerOver = useCallback(() => {
|
||||
setHovered(true);
|
||||
document.body.style.cursor = 'pointer';
|
||||
}, []);
|
||||
|
||||
const handlePointerOut = useCallback(() => {
|
||||
setHovered(false);
|
||||
document.body.style.cursor = 'auto';
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<mesh
|
||||
position={position}
|
||||
onPointerOver={handlePointerOver}
|
||||
onPointerOut={handlePointerOut}
|
||||
scale={hovered ? 1.1 : 1}
|
||||
>
|
||||
<sphereGeometry args={[0.5, 32, 32]} />
|
||||
<meshStandardMaterial
|
||||
color={hovered ? '#ff6b6b' : '#4ecdc4'}
|
||||
emissive={hovered ? '#ff6b6b' : '#000000'}
|
||||
emissiveIntensity={hovered ? 0.3 : 0}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Event Propagation
|
||||
|
||||
R3F events bubble through the scene graph just like React DOM events. Objects closer to the camera receive events first.
|
||||
|
||||
### stopPropagation
|
||||
|
||||
```tsx
|
||||
// The inner mesh receives the click first
|
||||
// stopPropagation prevents the outer group from receiving it
|
||||
<group onClick={() => console.log('group clicked')}>
|
||||
<mesh onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
console.log('mesh clicked'); // Only this fires
|
||||
}}>
|
||||
<boxGeometry />
|
||||
<meshStandardMaterial />
|
||||
</mesh>
|
||||
</group>
|
||||
```
|
||||
|
||||
### Event Filtering
|
||||
|
||||
Filter events based on intersection properties:
|
||||
|
||||
```tsx
|
||||
<mesh onClick={(e) => {
|
||||
// Only respond to front-face clicks
|
||||
if (e.face && e.face.normal.z > 0) {
|
||||
handleFrontClick(e.point);
|
||||
}
|
||||
}}>
|
||||
<boxGeometry />
|
||||
<meshStandardMaterial />
|
||||
</mesh>
|
||||
```
|
||||
|
||||
## Camera Controls
|
||||
|
||||
### @suaveplan/r3f-camera
|
||||
|
||||
The `@suaveplan/r3f-camera` package provides camera controllers optimized for different interaction modes:
|
||||
|
||||
```tsx
|
||||
import { OrbitCamera } from '@suaveplan/r3f-camera';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
|
||||
export function OrbitScene() {
|
||||
return (
|
||||
<Canvas camera={{ position: [5, 5, 5] }}>
|
||||
<OrbitCamera
|
||||
enableDamping
|
||||
dampingFactor={0.05}
|
||||
minDistance={2}
|
||||
maxDistance={20}
|
||||
minPolarAngle={0.1}
|
||||
maxPolarAngle={Math.PI / 2 - 0.1}
|
||||
/>
|
||||
{/* scene content */}
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### First-Person Camera
|
||||
|
||||
```tsx
|
||||
import { FirstPersonCamera } from '@suaveplan/r3f-camera';
|
||||
|
||||
export function FPSScene() {
|
||||
return (
|
||||
<Canvas>
|
||||
<FirstPersonCamera
|
||||
moveSpeed={5}
|
||||
lookSpeed={0.002}
|
||||
jumpForce={4}
|
||||
gravity={9.8}
|
||||
heightOffset={1.7}
|
||||
/>
|
||||
{/* scene content */}
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Third-Person Camera
|
||||
|
||||
```tsx
|
||||
import { ThirdPersonCamera } from '@suaveplan/r3f-camera';
|
||||
import { useRef } from 'react';
|
||||
import type { Group } from 'three';
|
||||
|
||||
export function TPSScene() {
|
||||
const targetRef = useRef<Group>(null);
|
||||
|
||||
return (
|
||||
<Canvas>
|
||||
<ThirdPersonCamera
|
||||
target={targetRef}
|
||||
distance={5}
|
||||
height={2}
|
||||
smoothing={0.1}
|
||||
/>
|
||||
<group ref={targetRef} position={[0, 0, 0]}>
|
||||
{/* Character mesh */}
|
||||
</group>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Drei Camera Controls
|
||||
|
||||
For simpler use cases, `@react-three/drei` provides ready-made controls:
|
||||
|
||||
```tsx
|
||||
import { OrbitControls, MapControls, FlyControls } from '@react-three/drei';
|
||||
|
||||
// Orbit: rotate around a target
|
||||
<OrbitControls makeDefault />
|
||||
|
||||
// Map: pan/zoom (top-down views)
|
||||
<MapControls makeDefault />
|
||||
|
||||
// Fly: free-flight movement
|
||||
<FlyControls movementSpeed={5} rollSpeed={0.5} />
|
||||
```
|
||||
|
||||
## Drag Interactions
|
||||
|
||||
### Basic Drag with useDrag
|
||||
|
||||
Use `@use-gesture/react`'s `useDrag` hook. In the drag callback, intersect the event ray with a `Plane` to constrain movement to a surface. Update mesh position via state.
|
||||
|
||||
### DragControls from Drei
|
||||
|
||||
Wrap draggable content in `<DragControls>` from drei. Use `onDragStart`/`onDragEnd` callbacks to update cursor style. Objects inside become draggable automatically.
|
||||
|
||||
## Raycasting: Imperative Escape Hatch
|
||||
|
||||
When you need custom raycasting beyond R3F events:
|
||||
|
||||
```tsx
|
||||
import { useRef, useCallback } from 'react';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { Raycaster, Vector2 } from 'three';
|
||||
|
||||
export function useCustomRaycast() {
|
||||
const { camera, scene } = useThree();
|
||||
const raycaster = useRef(new Raycaster());
|
||||
const pointer = useRef(new Vector2());
|
||||
|
||||
const castRay = useCallback((clientX: number, clientY: number) => {
|
||||
const canvas = document.querySelector('canvas');
|
||||
if (!canvas) return [];
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
pointer.current.x = ((clientX - rect.left) / rect.width) * 2 - 1;
|
||||
pointer.current.y = -((clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
raycaster.current.setFromCamera(pointer.current, camera);
|
||||
return raycaster.current.intersectObjects(scene.children, true);
|
||||
}, [camera, scene]);
|
||||
|
||||
return { castRay };
|
||||
}
|
||||
```
|
||||
|
||||
### Raycasting Configuration
|
||||
|
||||
```tsx
|
||||
// Limit ray distance
|
||||
raycaster.far = 100;
|
||||
raycaster.near = 0.1;
|
||||
|
||||
// Configure line threshold (for Line objects)
|
||||
raycaster.params.Line.threshold = 0.1;
|
||||
|
||||
// Configure points threshold (for Points objects)
|
||||
raycaster.params.Points.threshold = 0.1;
|
||||
|
||||
// Layer-based filtering
|
||||
raycaster.layers.set(1); // Only intersect objects on layer 1
|
||||
```
|
||||
|
||||
### Layer-Based Interaction Filtering
|
||||
|
||||
Use `meshRef.current.layers.enable(1)` in `useEffect` to place interactable objects on layer 1. Configure the raycaster to only intersect specific layers with `raycaster.layers.set(1)`.
|
||||
|
||||
## Selection Patterns
|
||||
|
||||
### Single Selection
|
||||
|
||||
Track `selectedId` in state. Each `SelectableItem` calls `onSelect(id)` on click. Toggle selection by comparing current vs previous ID. Apply visual feedback (color, emissive) based on `isSelected` prop.
|
||||
|
||||
### Multi-Selection with Shift-Click
|
||||
|
||||
Use `event.nativeEvent.shiftKey` to detect Shift held during click. Maintain a `Set<string>` of selected IDs. On shift-click, toggle the item in the existing set. Without shift, replace the entire selection.
|
||||
|
||||
## Touch and Mobile Interaction
|
||||
|
||||
R3F pointer events work with touch input automatically. Configure the `events` prop on Canvas to filter intersections (e.g., only closest) for touch optimization.
|
||||
|
||||
## Booking Venue Interaction
|
||||
|
||||
The `@suaveplan/r3f-booking` package provides interactive 3D venue maps with built-in seat selection, zone highlighting, and pricing display:
|
||||
|
||||
```tsx
|
||||
import { VenueMap } from '@suaveplan/r3f-booking';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { OrbitCamera } from '@suaveplan/r3f-camera';
|
||||
|
||||
export function BookingScene() {
|
||||
return (
|
||||
<Canvas>
|
||||
<OrbitCamera minDistance={5} maxDistance={50} />
|
||||
<VenueMap
|
||||
venueUrl="/models/venue.glb"
|
||||
onSeatSelect={(seatId) => { /* handle booking */ }}
|
||||
onZoneHover={(zoneId) => { /* show pricing */ }}
|
||||
selectedSeats={['A1', 'A2']}
|
||||
zoneColors={{
|
||||
vip: '#ffd700',
|
||||
standard: '#4a90d9',
|
||||
economy: '#7cb342',
|
||||
}}
|
||||
/>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Event Computation Cost
|
||||
|
||||
R3F raycasts on every pointer event by default. Optimize with:
|
||||
|
||||
```tsx
|
||||
<Canvas
|
||||
raycaster={{
|
||||
// Increase threshold for easier touch targeting
|
||||
params: { Line: { threshold: 5 }, Points: { threshold: 5 } },
|
||||
}}
|
||||
events={(store) => ({
|
||||
...store,
|
||||
// Only process closest intersection
|
||||
filter: (intersections) => intersections.slice(0, 1),
|
||||
})}
|
||||
>
|
||||
```
|
||||
|
||||
### Disabling Events on Non-Interactive Objects
|
||||
|
||||
```tsx
|
||||
// Skip raycasting for decorative objects
|
||||
<mesh raycast={() => null}>
|
||||
<sphereGeometry />
|
||||
<meshBasicMaterial />
|
||||
</mesh>
|
||||
|
||||
// Or use a wrapper
|
||||
function Decorative({ children }: { children: React.ReactNode }) {
|
||||
return <group raycast={() => null}>{children}</group>;
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Interactions
|
||||
|
||||
### Vitest Tests
|
||||
|
||||
```tsx
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
describe('ClickableBox', () => {
|
||||
it('calls onSelect with intersection point', () => {
|
||||
const onSelect = vi.fn();
|
||||
// Use @react-three/test-renderer for R3F component testing
|
||||
// Verify callback receives correct coordinates
|
||||
expect(onSelect).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Storybook Stories
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { OrbitControls } from '@react-three/drei';
|
||||
import { ClickableBox } from './clickable-box';
|
||||
|
||||
const meta: Meta<typeof ClickableBox> = {
|
||||
title: 'R3F/Interaction/ClickableBox',
|
||||
component: ClickableBox,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Canvas camera={{ position: [3, 3, 3] }} style={{ height: 400 }}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[5, 5, 5]} />
|
||||
<Story />
|
||||
<OrbitControls />
|
||||
<gridHelper />
|
||||
</Canvas>
|
||||
),
|
||||
],
|
||||
};
|
||||
export default meta;
|
||||
|
||||
export const Default: StoryObj<typeof ClickableBox> = {
|
||||
args: { position: [0, 0.5, 0] },
|
||||
};
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
When implementing interactions in R3F:
|
||||
|
||||
1. Use R3F pointer events (`onClick`, `onPointerOver`) instead of manual raycasting
|
||||
2. Always call `event.stopPropagation()` to prevent unintended event bubbling
|
||||
3. Set `document.body.style.cursor` on hover for visual feedback
|
||||
4. Use `@suaveplan/r3f-camera` for camera controls (orbit, FPS, TPS)
|
||||
5. Disable raycasting on decorative objects with `raycast={() => null}`
|
||||
6. Handle both click and touch input (R3F normalizes automatically)
|
||||
7. Use layers for raycasting categories (interactive vs. background)
|
||||
8. Debounce expensive operations in `onPointerMove` handlers
|
||||
9. Test interaction callbacks with Vitest mocks
|
||||
10. Write Storybook stories with camera controls for interactive testing
|
||||
@@ -0,0 +1,513 @@
|
||||
---
|
||||
name: threejs-lighting
|
||||
description: Three.js lighting systems adapted for React Three Fiber and Suaveplan R3F packages. Covers declarative light JSX components, shadow map configuration, environment maps with @react-three/drei, image-based lighting, light helpers, and @suaveplan/r3f-performance for light optimization and adaptive quality.
|
||||
---
|
||||
|
||||
This skill covers lighting in Three.js within the React Three Fiber (R3F) context used by the Suaveplan monorepo. All code targets **React 19 + TypeScript + R3F** and follows Suaveplan CLAUDE.md conventions.
|
||||
|
||||
The user needs to light a 3D scene: configure light types, enable shadows, use environment maps for image-based lighting, or optimize light performance.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Three.js Lighting Model
|
||||
|
||||
Three.js provides physically-based rendering (PBR) with several light types. Lights contribute to material shading based on the Lambertian/GGX BRDF model.
|
||||
|
||||
### R3F Lighting Approach
|
||||
|
||||
In R3F, lights are JSX elements added to the scene graph:
|
||||
|
||||
| Three.js (Imperative) | R3F (Declarative) |
|
||||
|---|---|
|
||||
| `new AmbientLight(0xffffff, 0.5)` | `<ambientLight intensity={0.5} />` |
|
||||
| `scene.add(light)` | Place inside `<Canvas>` tree |
|
||||
| `light.position.set(5, 5, 5)` | `position={[5, 5, 5]}` prop |
|
||||
| `light.castShadow = true` | `castShadow` prop |
|
||||
| `light.shadow.mapSize.set(2048, 2048)` | `shadow-mapSize={[2048, 2048]}` |
|
||||
|
||||
### Suaveplan R3F Packages
|
||||
|
||||
| Package | Purpose | Import |
|
||||
|---|---|---|
|
||||
| `@suaveplan/r3f` | Core scene utilities | `import { useR3F } from '@suaveplan/r3f'` |
|
||||
| `@suaveplan/r3f-performance` | Light budget monitoring, adaptive shadows | `import { usePerformanceMonitor } from '@suaveplan/r3f-performance'` |
|
||||
| `@suaveplan/r3f-shaders` | Custom light-reactive materials | `import { useAnimatedShader } from '@suaveplan/r3f-shaders'` |
|
||||
|
||||
## Light Types
|
||||
|
||||
### AmbientLight
|
||||
|
||||
Uniform light from all directions. No shadows. Use for base illumination.
|
||||
|
||||
```tsx
|
||||
<ambientLight
|
||||
color="#ffffff" // Light color
|
||||
intensity={0.4} // Brightness (0-N, default 1)
|
||||
/>
|
||||
```
|
||||
|
||||
### DirectionalLight
|
||||
|
||||
Parallel rays from a direction (like the sun). Casts shadows.
|
||||
|
||||
```tsx
|
||||
<directionalLight
|
||||
position={[10, 10, 5]} // Direction = position - target
|
||||
intensity={1.5}
|
||||
color="#fffbe6"
|
||||
castShadow
|
||||
shadow-mapSize={[2048, 2048]}
|
||||
shadow-camera-left={-10}
|
||||
shadow-camera-right={10}
|
||||
shadow-camera-top={10}
|
||||
shadow-camera-bottom={-10}
|
||||
shadow-camera-near={0.1}
|
||||
shadow-camera-far={50}
|
||||
shadow-bias={-0.0001}
|
||||
/>
|
||||
```
|
||||
|
||||
### PointLight
|
||||
|
||||
Emits light in all directions from a point (like a light bulb). Casts shadows.
|
||||
|
||||
```tsx
|
||||
<pointLight
|
||||
position={[0, 3, 0]}
|
||||
intensity={100} // In candela (physical mode)
|
||||
distance={10} // Maximum range (0 = infinite)
|
||||
decay={2} // Physical falloff (2 = realistic)
|
||||
color="#ffd700"
|
||||
castShadow
|
||||
shadow-mapSize={[1024, 1024]}
|
||||
shadow-bias={-0.001}
|
||||
/>
|
||||
```
|
||||
|
||||
### SpotLight
|
||||
|
||||
Cone-shaped light (like a flashlight or stage light). Casts shadows.
|
||||
|
||||
```tsx
|
||||
<spotLight
|
||||
position={[0, 5, 0]}
|
||||
target-position={[0, 0, 0]} // Aim point
|
||||
angle={Math.PI / 6} // Cone angle (radians)
|
||||
penumbra={0.5} // Soft edge (0-1)
|
||||
intensity={200}
|
||||
distance={20}
|
||||
decay={2}
|
||||
color="#ffffff"
|
||||
castShadow
|
||||
shadow-mapSize={[2048, 2048]}
|
||||
shadow-bias={-0.0001}
|
||||
/>
|
||||
```
|
||||
|
||||
### HemisphereLight
|
||||
|
||||
Two-color gradient light (sky + ground). No shadows.
|
||||
|
||||
```tsx
|
||||
<hemisphereLight
|
||||
color="#87ceeb" // Sky color
|
||||
groundColor="#8b4513" // Ground color
|
||||
intensity={0.6}
|
||||
/>
|
||||
```
|
||||
|
||||
### RectAreaLight
|
||||
|
||||
Rectangular area light (like a window or fluorescent panel). No shadow map support.
|
||||
|
||||
```tsx
|
||||
import { RectAreaLightHelper } from 'three/addons/helpers/RectAreaLightHelper.js';
|
||||
import { RectAreaLightUniformsLib } from 'three/addons/lights/RectAreaLightUniformsLib.js';
|
||||
|
||||
// Must initialize uniforms before use
|
||||
RectAreaLightUniformsLib.init();
|
||||
|
||||
<rectAreaLight
|
||||
position={[0, 3, -2]}
|
||||
rotation={[-Math.PI / 2, 0, 0]}
|
||||
width={4}
|
||||
height={2}
|
||||
intensity={5}
|
||||
color="#ffffff"
|
||||
/>
|
||||
```
|
||||
|
||||
## Shadow Configuration
|
||||
|
||||
### Enabling Shadows
|
||||
|
||||
Shadows require three things: Canvas shadow prop, light `castShadow`, and mesh `receiveShadow`/`castShadow`.
|
||||
|
||||
```tsx
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
|
||||
export function ShadowScene() {
|
||||
return (
|
||||
<Canvas
|
||||
shadows // Enable shadow maps
|
||||
// Or configure shadow type:
|
||||
// shadows={{ type: PCFSoftShadowMap }}
|
||||
>
|
||||
{/* Light that casts shadows */}
|
||||
<directionalLight
|
||||
position={[5, 5, 5]}
|
||||
castShadow
|
||||
shadow-mapSize={[2048, 2048]}
|
||||
shadow-camera-far={20}
|
||||
shadow-bias={-0.0001}
|
||||
/>
|
||||
|
||||
{/* Object that casts shadows */}
|
||||
<mesh position={[0, 1, 0]} castShadow>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial color="tomato" />
|
||||
</mesh>
|
||||
|
||||
{/* Ground that receives shadows */}
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
|
||||
<planeGeometry args={[20, 20]} />
|
||||
<meshStandardMaterial color="#444" />
|
||||
</mesh>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Shadow Map Types
|
||||
|
||||
```tsx
|
||||
import { PCFShadowMap, PCFSoftShadowMap, VSMShadowMap, BasicShadowMap } from 'three';
|
||||
|
||||
// BasicShadowMap: fastest, hard edges
|
||||
// PCFShadowMap: default, some softness
|
||||
// PCFSoftShadowMap: softer, more expensive
|
||||
// VSMShadowMap: variance shadow map, supports blurring
|
||||
|
||||
<Canvas shadows={{ type: PCFSoftShadowMap }}>
|
||||
```
|
||||
|
||||
### Shadow Camera Frustum
|
||||
|
||||
For directional lights, the shadow camera is orthographic. Size the frustum tightly around the scene for better shadow quality:
|
||||
|
||||
```tsx
|
||||
<directionalLight
|
||||
castShadow
|
||||
shadow-camera-left={-8}
|
||||
shadow-camera-right={8}
|
||||
shadow-camera-top={8}
|
||||
shadow-camera-bottom={-8}
|
||||
shadow-camera-near={0.5}
|
||||
shadow-camera-far={30}
|
||||
/>
|
||||
```
|
||||
|
||||
### Cascaded Shadow Maps (CSM)
|
||||
|
||||
For large scenes, use drei's CSM:
|
||||
|
||||
```tsx
|
||||
import { AccumulativeShadows, RandomizedLight } from '@react-three/drei';
|
||||
|
||||
export function SoftShadows() {
|
||||
return (
|
||||
<AccumulativeShadows
|
||||
temporal
|
||||
frames={60}
|
||||
alphaTest={0.85}
|
||||
scale={10}
|
||||
position={[0, 0.01, 0]}
|
||||
>
|
||||
<RandomizedLight
|
||||
amount={8}
|
||||
radius={2}
|
||||
ambient={0.5}
|
||||
position={[5, 5, -3]}
|
||||
bias={0.001}
|
||||
/>
|
||||
</AccumulativeShadows>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Maps and IBL
|
||||
|
||||
### Environment Component (drei)
|
||||
|
||||
The simplest way to add image-based lighting:
|
||||
|
||||
```tsx
|
||||
import { Environment } from '@react-three/drei';
|
||||
|
||||
// Preset environments
|
||||
<Environment preset="sunset" />
|
||||
<Environment preset="city" />
|
||||
<Environment preset="forest" />
|
||||
<Environment preset="studio" />
|
||||
|
||||
// Custom HDR
|
||||
<Environment files="/textures/env.hdr" />
|
||||
|
||||
// As scene background
|
||||
<Environment preset="sunset" background />
|
||||
|
||||
// Blur the background
|
||||
<Environment preset="sunset" background blur={0.5} />
|
||||
```
|
||||
|
||||
### Available Presets
|
||||
|
||||
| Preset | Description | Use Case |
|
||||
|---|---|---|
|
||||
| `apartment` | Indoor apartment | Interior scenes |
|
||||
| `city` | Urban skyline | Outdoor urban |
|
||||
| `dawn` | Early morning sky | Warm exterior |
|
||||
| `forest` | Dense forest | Nature scenes |
|
||||
| `lobby` | Hotel lobby | Indoor formal |
|
||||
| `night` | Night sky | Dark scenes |
|
||||
| `park` | Park setting | Outdoor casual |
|
||||
| `studio` | Photography studio | Product shots |
|
||||
| `sunset` | Golden hour | Dramatic exterior |
|
||||
| `warehouse` | Industrial space | Gritty interior |
|
||||
|
||||
### Custom Environment Map
|
||||
|
||||
```tsx
|
||||
import { useRef } from 'react';
|
||||
import { useThree } from '@react-three/fiber';
|
||||
import { CubeCamera } from '@react-three/drei';
|
||||
|
||||
export function ReflectiveSphere() {
|
||||
return (
|
||||
<CubeCamera resolution={256} frames={Infinity}>
|
||||
{(texture) => (
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<meshStandardMaterial
|
||||
envMap={texture}
|
||||
metalness={1}
|
||||
roughness={0}
|
||||
/>
|
||||
</mesh>
|
||||
)}
|
||||
</CubeCamera>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Light Helpers
|
||||
|
||||
Visualize light positions and frustums during development:
|
||||
|
||||
```tsx
|
||||
import { useHelper } from '@react-three/drei';
|
||||
import { useRef } from 'react';
|
||||
import {
|
||||
DirectionalLightHelper,
|
||||
SpotLightHelper,
|
||||
PointLightHelper,
|
||||
CameraHelper,
|
||||
} from 'three';
|
||||
import type { DirectionalLight, SpotLight, PointLight } from 'three';
|
||||
|
||||
export function DebugLights({ debug = false }: { debug?: boolean }) {
|
||||
const dirRef = useRef<DirectionalLight>(null);
|
||||
const spotRef = useRef<SpotLight>(null);
|
||||
const pointRef = useRef<PointLight>(null);
|
||||
|
||||
// Helpers only rendered when debug is true
|
||||
useHelper(debug && dirRef, DirectionalLightHelper, 1, 'red');
|
||||
useHelper(debug && spotRef, SpotLightHelper, 'green');
|
||||
useHelper(debug && pointRef, PointLightHelper, 0.5, 'blue');
|
||||
|
||||
return (
|
||||
<>
|
||||
<directionalLight ref={dirRef} position={[5, 5, 5]} castShadow />
|
||||
<spotLight ref={spotRef} position={[0, 5, 0]} angle={Math.PI / 6} />
|
||||
<pointLight ref={pointRef} position={[-3, 2, 0]} intensity={50} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Lighting Recipes
|
||||
|
||||
### Outdoor Scene (Sun + Sky)
|
||||
|
||||
```tsx
|
||||
import { Sky, Environment } from '@react-three/drei';
|
||||
|
||||
export function OutdoorLighting() {
|
||||
return (
|
||||
<>
|
||||
{/* Sun light */}
|
||||
<directionalLight
|
||||
position={[50, 50, 25]}
|
||||
intensity={2}
|
||||
color="#fff5e1"
|
||||
castShadow
|
||||
shadow-mapSize={[4096, 4096]}
|
||||
shadow-camera-left={-30}
|
||||
shadow-camera-right={30}
|
||||
shadow-camera-top={30}
|
||||
shadow-camera-bottom={-30}
|
||||
/>
|
||||
|
||||
{/* Fill light (sky) */}
|
||||
<hemisphereLight
|
||||
color="#87ceeb"
|
||||
groundColor="#362312"
|
||||
intensity={0.4}
|
||||
/>
|
||||
|
||||
{/* Sky dome */}
|
||||
<Sky
|
||||
sunPosition={[50, 50, 25]}
|
||||
turbidity={3}
|
||||
rayleigh={0.5}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Indoor Scene (Area Lights + Ambient)
|
||||
|
||||
```tsx
|
||||
export function IndoorLighting() {
|
||||
return (
|
||||
<>
|
||||
{/* Ambient base */}
|
||||
<ambientLight intensity={0.2} color="#f0e6d3" />
|
||||
|
||||
{/* Ceiling lights */}
|
||||
<pointLight position={[0, 2.8, 0]} intensity={80} decay={2} distance={8} castShadow />
|
||||
<pointLight position={[3, 2.8, 0]} intensity={60} decay={2} distance={6} />
|
||||
|
||||
{/* Window light */}
|
||||
<directionalLight
|
||||
position={[-5, 3, 2]}
|
||||
intensity={1.5}
|
||||
color="#b8d4e3"
|
||||
castShadow
|
||||
shadow-mapSize={[2048, 2048]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Product Studio Lighting
|
||||
|
||||
Use `<Environment>` with nested `<Lightformer>` components for key, fill, and rim lighting. Add `<ContactShadows>` from drei for soft ground shadows without shadow maps. Configure `form="rect"` or `form="ring"`, position, intensity, and scale for each lightformer.
|
||||
|
||||
## Adaptive Lighting with @suaveplan/r3f-performance
|
||||
|
||||
Use `usePerformanceMonitor` to track FPS and switch between quality presets (high/medium/low). Each preset configures shadow map size (4096/2048/1024), env map resolution, and max light count. Scale down on `onDecline`, scale up on `onIncline`.
|
||||
|
||||
## Light Animation
|
||||
|
||||
### Flickering Light
|
||||
|
||||
```tsx
|
||||
import { useRef } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import type { PointLight } from 'three';
|
||||
|
||||
export function FlickeringLight({ position }: { position: [number, number, number] }) {
|
||||
const lightRef = useRef<PointLight>(null);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!lightRef.current) return;
|
||||
const t = clock.elapsedTime;
|
||||
const flicker = Math.sin(t * 20) * 0.1 + Math.sin(t * 37) * 0.05 + Math.sin(t * 53) * 0.03;
|
||||
lightRef.current.intensity = 80 + flicker * 40;
|
||||
});
|
||||
|
||||
return (
|
||||
<pointLight
|
||||
ref={lightRef}
|
||||
position={position}
|
||||
color="#ff9500"
|
||||
distance={8}
|
||||
decay={2}
|
||||
castShadow
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Lighting
|
||||
|
||||
### Vitest Tests
|
||||
|
||||
```tsx
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DirectionalLight, PointLight } from 'three';
|
||||
|
||||
describe('Lighting setup', () => {
|
||||
it('creates directional light with shadow', () => {
|
||||
const light = new DirectionalLight(0xffffff, 1.5);
|
||||
light.castShadow = true;
|
||||
light.shadow.mapSize.set(2048, 2048);
|
||||
expect(light.castShadow).toBe(true);
|
||||
expect(light.shadow.mapSize.x).toBe(2048);
|
||||
});
|
||||
|
||||
it('configures point light physical decay', () => {
|
||||
const light = new PointLight(0xffd700, 100, 10, 2);
|
||||
expect(light.decay).toBe(2);
|
||||
expect(light.distance).toBe(10);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Storybook Stories
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { OrbitControls, Environment } from '@react-three/drei';
|
||||
|
||||
function LightingDemo() {
|
||||
return (
|
||||
<Canvas shadows camera={{ position: [5, 5, 5] }} style={{ height: 400 }}>
|
||||
<ambientLight intensity={0.3} />
|
||||
<directionalLight position={[5, 5, 5]} castShadow />
|
||||
<mesh castShadow position={[0, 0.5, 0]}>
|
||||
<boxGeometry />
|
||||
<meshStandardMaterial color="coral" />
|
||||
</mesh>
|
||||
<mesh receiveShadow rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<planeGeometry args={[10, 10]} />
|
||||
<meshStandardMaterial color="#555" />
|
||||
</mesh>
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
|
||||
const meta: Meta = { title: 'R3F/Lighting/ShadowDemo', component: LightingDemo };
|
||||
export default meta;
|
||||
export const Default: StoryObj = {};
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
When setting up lighting in R3F:
|
||||
|
||||
1. Enable `shadows` on `<Canvas>` and choose a shadow map type
|
||||
2. Use `castShadow` on lights and objects, `receiveShadow` on surfaces
|
||||
3. Size shadow camera frustum tightly around the visible scene
|
||||
4. Set `shadow-bias` to prevent shadow acne (start with `-0.0001`)
|
||||
5. Use `<Environment>` from drei for quick IBL and reflections
|
||||
6. Use `useHelper` to visualize light positions during development
|
||||
7. Limit active shadow-casting lights (expensive: 1 shadow map per light per frame)
|
||||
8. Use `@suaveplan/r3f-performance` for adaptive shadow quality
|
||||
9. Combine ambient/hemisphere with directional for natural-looking scenes
|
||||
10. Keep lighting configuration components under 250 lines
|
||||
@@ -0,0 +1,519 @@
|
||||
---
|
||||
name: threejs-loaders
|
||||
description: Three.js asset loading adapted for React Three Fiber and Suaveplan R3F packages. Covers useLoader, useGLTF, and useTexture hooks, React Suspense boundaries for async loading, preloading strategies, @suaveplan/r3f loading utilities, Draco/KTX2 mesh and texture compression, and progressive loading patterns.
|
||||
---
|
||||
|
||||
This skill covers asset loading in Three.js within the React Three Fiber (R3F) context used by the Suaveplan monorepo. All code targets **React 19 + TypeScript + R3F** and follows Suaveplan CLAUDE.md conventions.
|
||||
|
||||
The user needs to load 3D models, textures, fonts, or other assets into an R3F scene. They may need GLTF models, compressed assets, preloading, or progress tracking.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Three.js Loader System
|
||||
|
||||
Three.js provides loader classes for different asset types:
|
||||
|
||||
- **GLTFLoader**: Loads `.gltf`/`.glb` files (the primary 3D model format)
|
||||
- **TextureLoader**: Loads images as GPU textures
|
||||
- **CubeTextureLoader**: Loads cubemap textures (6 faces)
|
||||
- **FBXLoader**: Loads FBX files
|
||||
- **OBJLoader**: Loads OBJ files
|
||||
- **DRACOLoader**: Decompresses Draco-compressed geometry
|
||||
- **KTX2Loader**: Loads KTX2 GPU-compressed textures
|
||||
- **EXRLoader/RGBELoader**: Loads HDR environment maps
|
||||
|
||||
### R3F Loading Approach
|
||||
|
||||
R3F replaces manual loader management with hooks and React Suspense:
|
||||
|
||||
| Three.js (Imperative) | R3F (Declarative) |
|
||||
|---|---|
|
||||
| `new GLTFLoader().load(url, cb)` | `useGLTF(url)` + `<Suspense>` |
|
||||
| `new TextureLoader().load(url, cb)` | `useTexture(url)` + `<Suspense>` |
|
||||
| Manual progress tracking | `useProgress()` hook |
|
||||
| Manual error handling | Error boundaries |
|
||||
| Manual disposal | Automatic on unmount |
|
||||
|
||||
### Suaveplan R3F Packages
|
||||
|
||||
| Package | Purpose | Import |
|
||||
|---|---|---|
|
||||
| `@suaveplan/r3f` | Loading utilities, asset management | `import { useAssetLoader } from '@suaveplan/r3f'` |
|
||||
| `@suaveplan/r3f-character` | Character model loading | `import { useCharacterModel } from '@suaveplan/r3f-character'` |
|
||||
| `@suaveplan/r3f-performance` | Loading performance monitoring | `import { usePerformanceMonitor } from '@suaveplan/r3f-performance'` |
|
||||
|
||||
## useGLTF: Loading 3D Models
|
||||
|
||||
The primary way to load 3D models in R3F. Returns the full GLTF structure.
|
||||
|
||||
### Basic Model Loading
|
||||
|
||||
```tsx
|
||||
import { Suspense } from 'react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { useGLTF } from '@react-three/drei';
|
||||
|
||||
interface ModelProps {
|
||||
url: string;
|
||||
position?: [number, number, number];
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
function Model({ url, position = [0, 0, 0], scale = 1 }: ModelProps) {
|
||||
const { scene } = useGLTF(url);
|
||||
|
||||
return (
|
||||
<primitive
|
||||
object={scene}
|
||||
position={position}
|
||||
scale={scale}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModelScene() {
|
||||
return (
|
||||
<Canvas>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[5, 5, 5]} />
|
||||
<Suspense fallback={<LoadingPlaceholder />}>
|
||||
<Model url="/models/building.glb" />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingPlaceholder() {
|
||||
return (
|
||||
<mesh>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshBasicMaterial color="gray" wireframe />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### GLTF Return Structure
|
||||
|
||||
```tsx
|
||||
const gltf = useGLTF('/model.glb');
|
||||
|
||||
// gltf.scene - THREE.Group (the root scene)
|
||||
// gltf.nodes - Named nodes (meshes, bones, empties)
|
||||
// gltf.materials - Named materials
|
||||
// gltf.animations - AnimationClip[] (for useAnimations)
|
||||
// gltf.asset - GLTF metadata
|
||||
```
|
||||
|
||||
### Accessing Named Nodes
|
||||
|
||||
GLTF files export named objects. Access them via `gltf.nodes`:
|
||||
|
||||
```tsx
|
||||
import { useGLTF } from '@react-three/drei';
|
||||
|
||||
function Character({ url }: { url: string }) {
|
||||
const { nodes, materials } = useGLTF(url);
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Access specific meshes by name */}
|
||||
<mesh
|
||||
geometry={(nodes.Body as THREE.Mesh).geometry}
|
||||
material={materials.Skin}
|
||||
castShadow
|
||||
/>
|
||||
<mesh
|
||||
geometry={(nodes.Eyes as THREE.Mesh).geometry}
|
||||
material={materials.Eye}
|
||||
/>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### GLTF with TypeScript (gltfjsx)
|
||||
|
||||
Use `gltfjsx` to generate typed React components from GLTF files:
|
||||
|
||||
```bash
|
||||
# Generate typed component (do NOT run tsc, this outputs .tsx directly)
|
||||
bunx gltfjsx model.glb --types --transform
|
||||
```
|
||||
|
||||
Generated output pattern:
|
||||
|
||||
```tsx
|
||||
import { useRef } from 'react';
|
||||
import { useGLTF } from '@react-three/drei';
|
||||
import type { Group, Mesh, MeshStandardMaterial } from 'three';
|
||||
import type { GLTF } from 'three-stdlib';
|
||||
|
||||
interface GLTFResult extends GLTF {
|
||||
nodes: {
|
||||
Body: Mesh;
|
||||
Head: Mesh;
|
||||
};
|
||||
materials: {
|
||||
Skin: MeshStandardMaterial;
|
||||
Hair: MeshStandardMaterial;
|
||||
};
|
||||
}
|
||||
|
||||
export function CharacterModel(props: JSX.IntrinsicElements['group']) {
|
||||
const groupRef = useRef<Group>(null);
|
||||
const { nodes, materials } = useGLTF('/character.glb') as GLTFResult;
|
||||
|
||||
return (
|
||||
<group ref={groupRef} {...props} dispose={null}>
|
||||
<mesh geometry={nodes.Body.geometry} material={materials.Skin} castShadow />
|
||||
<mesh geometry={nodes.Head.geometry} material={materials.Hair} castShadow />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
useGLTF.preload('/character.glb');
|
||||
```
|
||||
|
||||
## Draco Compression
|
||||
|
||||
Draco compresses mesh geometry by 80-90%. R3F handles decompression automatically.
|
||||
|
||||
### Using Draco with useGLTF
|
||||
|
||||
```tsx
|
||||
import { useGLTF } from '@react-three/drei';
|
||||
|
||||
function CompressedModel({ url }: { url: string }) {
|
||||
// useGLTF auto-detects Draco and uses the bundled decoder
|
||||
const { scene } = useGLTF(url, '/draco/');
|
||||
// Second argument is the Draco decoder path
|
||||
|
||||
return <primitive object={scene} />;
|
||||
}
|
||||
|
||||
// Default Draco decoder from drei CDN (no path needed usually)
|
||||
function SimpleModel() {
|
||||
const { scene } = useGLTF('/model-draco.glb');
|
||||
return <primitive object={scene} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Draco Decoder Path
|
||||
|
||||
Pass a second argument to `useGLTF` for the Draco decoder path: `useGLTF('/model.glb', '/libs/draco/gltf/')`.
|
||||
|
||||
## KTX2 Texture Compression
|
||||
|
||||
KTX2 provides GPU-compressed textures (ASTC, ETC2, BC7) for smaller VRAM footprint. Load with `useLoader(KTX2Loader, url, (loader) => { loader.setTranscoderPath('/libs/basis/'); loader.detectSupport(gl); })`.
|
||||
|
||||
## useTexture: Loading Textures
|
||||
|
||||
```tsx
|
||||
import { useTexture } from '@react-three/drei';
|
||||
import { RepeatWrapping, SRGBColorSpace } from 'three';
|
||||
|
||||
function TexturedMesh() {
|
||||
const texture = useTexture('/textures/wood.jpg');
|
||||
|
||||
// Configure texture properties
|
||||
texture.wrapS = texture.wrapT = RepeatWrapping;
|
||||
texture.repeat.set(2, 2);
|
||||
texture.colorSpace = SRGBColorSpace;
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial map={texture} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Loading Multiple Textures (PBR Set)
|
||||
|
||||
```tsx
|
||||
import { useTexture } from '@react-three/drei';
|
||||
|
||||
function PBRMaterial() {
|
||||
const textures = useTexture({
|
||||
map: '/textures/brick/diffuse.jpg',
|
||||
normalMap: '/textures/brick/normal.jpg',
|
||||
roughnessMap: '/textures/brick/roughness.jpg',
|
||||
aoMap: '/textures/brick/ao.jpg',
|
||||
displacementMap: '/textures/brick/displacement.jpg',
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<planeGeometry args={[4, 4, 64, 64]} />
|
||||
<meshStandardMaterial
|
||||
{...textures}
|
||||
displacementScale={0.1}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
// Preload all textures
|
||||
useTexture.preload({
|
||||
map: '/textures/brick/diffuse.jpg',
|
||||
normalMap: '/textures/brick/normal.jpg',
|
||||
roughnessMap: '/textures/brick/roughness.jpg',
|
||||
aoMap: '/textures/brick/ao.jpg',
|
||||
displacementMap: '/textures/brick/displacement.jpg',
|
||||
});
|
||||
```
|
||||
|
||||
## useLoader: Generic Loader Hook
|
||||
|
||||
For loaders not covered by drei helpers:
|
||||
|
||||
```tsx
|
||||
import { useLoader } from '@react-three/fiber';
|
||||
import { FBXLoader } from 'three-stdlib';
|
||||
import { OBJLoader } from 'three-stdlib';
|
||||
import { FontLoader } from 'three-stdlib';
|
||||
|
||||
// FBX model
|
||||
function FBXModel({ url }: { url: string }) {
|
||||
const fbx = useLoader(FBXLoader, url);
|
||||
return <primitive object={fbx} scale={0.01} />;
|
||||
}
|
||||
|
||||
// OBJ model
|
||||
function OBJModel({ url }: { url: string }) {
|
||||
const obj = useLoader(OBJLoader, url);
|
||||
return <primitive object={obj} />;
|
||||
}
|
||||
|
||||
// Font
|
||||
function LoadedFont({ url }: { url: string }) {
|
||||
const font = useLoader(FontLoader, url);
|
||||
// Use with TextGeometry
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
### Loader with Extensions
|
||||
|
||||
Pass a configuration callback as the third argument to `useLoader`. Inside, attach `DRACOLoader` via `loader.setDRACOLoader(dracoLoader)` and `MeshoptDecoder` via `loader.setMeshoptDecoder(MeshoptDecoder)`.
|
||||
|
||||
## Suspense and Loading UI
|
||||
|
||||
### Loading Progress
|
||||
|
||||
```tsx
|
||||
import { Suspense } from 'react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { useProgress, Html } from '@react-three/drei';
|
||||
|
||||
function Loader() {
|
||||
const { progress, active, errors, item, loaded, total } = useProgress();
|
||||
|
||||
return (
|
||||
<Html center>
|
||||
<div style={{ color: 'white', fontSize: '1.5rem' }}>
|
||||
{progress.toFixed(0)}% loaded
|
||||
<div style={{ fontSize: '0.8rem', opacity: 0.6 }}>
|
||||
{loaded}/{total} assets
|
||||
</div>
|
||||
</div>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
export function SceneWithProgress() {
|
||||
return (
|
||||
<Canvas>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<HeavyScene />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Suspense Boundaries
|
||||
|
||||
Load critical assets first, then progressively load detail:
|
||||
|
||||
```tsx
|
||||
import { Suspense } from 'react';
|
||||
|
||||
export function ProgressiveScene() {
|
||||
return (
|
||||
<Canvas>
|
||||
{/* Critical: load immediately */}
|
||||
<ambientLight />
|
||||
<directionalLight position={[5, 5, 5]} />
|
||||
|
||||
{/* Tier 1: primary content */}
|
||||
<Suspense fallback={<PlaceholderBuilding />}>
|
||||
<Building url="/models/building.glb" />
|
||||
|
||||
{/* Tier 2: secondary content (loads after building) */}
|
||||
<Suspense fallback={null}>
|
||||
<Furniture url="/models/furniture.glb" />
|
||||
|
||||
{/* Tier 3: decorative (loads last) */}
|
||||
<Suspense fallback={null}>
|
||||
<Plants url="/models/plants.glb" />
|
||||
<Decorations url="/models/decor.glb" />
|
||||
</Suspense>
|
||||
</Suspense>
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Preloading
|
||||
|
||||
Preload assets before they are needed (e.g., during a loading screen or on hover):
|
||||
|
||||
```tsx
|
||||
import { useGLTF, useTexture } from '@react-three/drei';
|
||||
|
||||
// Preload at module level (loads immediately)
|
||||
useGLTF.preload('/models/character.glb');
|
||||
useGLTF.preload('/models/environment.glb');
|
||||
useTexture.preload('/textures/ground.jpg');
|
||||
|
||||
// Conditional preloading (e.g., on hover or route change)
|
||||
function PreloadOnHover({ modelUrl }: { modelUrl: string }) {
|
||||
return (
|
||||
<button
|
||||
onMouseEnter={() => useGLTF.preload(modelUrl)}
|
||||
onClick={() => { /* navigate to 3D view */ }}
|
||||
>
|
||||
View in 3D
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Preloading Multiple Assets
|
||||
|
||||
```tsx
|
||||
import { useGLTF } from '@react-three/drei';
|
||||
|
||||
const MODEL_URLS = [
|
||||
'/models/scene.glb',
|
||||
'/models/character.glb',
|
||||
'/models/props.glb',
|
||||
];
|
||||
|
||||
// Preload all at once
|
||||
MODEL_URLS.forEach(useGLTF.preload);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Boundaries for Loading Failures
|
||||
|
||||
Wrap `<Suspense>` in a React error boundary class. Override `getDerivedStateFromError` to catch loading failures. Render a fallback mesh (e.g., red wireframe box) when errors occur. Nest as `<ErrorBoundary><Suspense fallback={<Loader />}><Model /></Suspense></ErrorBoundary>`.
|
||||
|
||||
## Asset Optimization Guidelines
|
||||
|
||||
### Model Optimization
|
||||
|
||||
| Technique | Savings | Tool |
|
||||
|---|---|---|
|
||||
| Draco compression | 80-90% geometry | `gltf-transform draco` |
|
||||
| Meshopt compression | 60-80% geometry | `gltf-transform meshopt` |
|
||||
| Texture compression (KTX2) | 75% texture VRAM | `gltf-transform ktx2` |
|
||||
| Mesh simplification | 50-90% triangles | `gltf-transform simplify` |
|
||||
| Texture resize | Variable | `gltf-transform resize` |
|
||||
| Remove unused data | Variable | `gltf-transform prune` |
|
||||
|
||||
### Pipeline Command
|
||||
|
||||
```bash
|
||||
# Optimize a GLTF file (run via bun, not tsc)
|
||||
bunx gltf-transform optimize input.glb output.glb \
|
||||
--compress draco \
|
||||
--texture-compress ktx2 \
|
||||
--simplify-ratio 0.5
|
||||
```
|
||||
|
||||
## Disposal
|
||||
|
||||
R3F automatically disposes of loaded assets when components unmount. For manual control:
|
||||
|
||||
```tsx
|
||||
import { useGLTF } from '@react-three/drei';
|
||||
|
||||
function DisposableModel({ url }: { url: string }) {
|
||||
const { scene } = useGLTF(url);
|
||||
|
||||
// Pass dispose={null} to prevent auto-disposal (useful for shared assets)
|
||||
return <primitive object={scene} dispose={null} />;
|
||||
}
|
||||
|
||||
// Manually clear the cache
|
||||
function clearModelCache(url: string) {
|
||||
useGLTF.clear(url);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Loading
|
||||
|
||||
### Vitest Tests
|
||||
|
||||
```tsx
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('Asset loading', () => {
|
||||
it('preload function exists on useGLTF', async () => {
|
||||
const { useGLTF } = await import('@react-three/drei');
|
||||
expect(typeof useGLTF.preload).toBe('function');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Storybook Stories
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Suspense } from 'react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { OrbitControls, useProgress, Html } from '@react-three/drei';
|
||||
|
||||
function LoaderUI() {
|
||||
const { progress } = useProgress();
|
||||
return <Html center><span>{progress.toFixed(0)}%</span></Html>;
|
||||
}
|
||||
|
||||
function ModelStory() {
|
||||
return (
|
||||
<Canvas camera={{ position: [3, 3, 3] }} style={{ height: 400 }}>
|
||||
<ambientLight />
|
||||
<directionalLight position={[5, 5, 5]} />
|
||||
<Suspense fallback={<LoaderUI />}>
|
||||
{/* Model component here */}
|
||||
</Suspense>
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
|
||||
const meta: Meta = { title: 'R3F/Loaders/ModelLoader', component: ModelStory };
|
||||
export default meta;
|
||||
export const Default: StoryObj = {};
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
When loading assets in R3F:
|
||||
|
||||
1. Use `useGLTF` for GLTF/GLB models (the standard 3D format)
|
||||
2. Wrap loaded components in `<Suspense>` with a fallback
|
||||
3. Use `useProgress` for loading progress UI
|
||||
4. Preload assets with `useGLTF.preload()` at module level or on interaction
|
||||
5. Use Draco compression for mesh geometry (80-90% savings)
|
||||
6. Use KTX2 for GPU-compressed textures
|
||||
7. Generate typed components with `gltfjsx --types`
|
||||
8. Use nested Suspense for progressive loading (critical first)
|
||||
9. Add error boundaries for loading failures
|
||||
10. Keep loader component files under 250 lines
|
||||
@@ -0,0 +1,579 @@
|
||||
---
|
||||
name: threejs-materials
|
||||
description: Three.js materials adapted for React Three Fiber and Suaveplan R3F packages. Covers declarative material props in JSX, physically-based rendering (PBR) with MeshStandardMaterial and MeshPhysicalMaterial, @suaveplan/r3f-shaders for custom materials, material refs and animation via useFrame, transparency, and material instancing.
|
||||
---
|
||||
|
||||
This skill covers materials in Three.js within the React Three Fiber (R3F) context used by the Suaveplan monorepo. All code targets **React 19 + TypeScript + R3F** and follows Suaveplan CLAUDE.md conventions.
|
||||
|
||||
The user needs to configure material appearance for 3D objects: PBR properties, textures, transparency, custom shaders, or animated material properties.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Three.js Material System
|
||||
|
||||
Materials define how surfaces respond to light. Three.js provides a hierarchy from basic to physically-based:
|
||||
|
||||
- **MeshBasicMaterial**: Unlit, flat color (no light interaction)
|
||||
- **MeshLambertMaterial**: Diffuse shading only (fast, non-glossy)
|
||||
- **MeshPhongMaterial**: Diffuse + specular highlights (legacy)
|
||||
- **MeshStandardMaterial**: PBR with metalness/roughness workflow
|
||||
- **MeshPhysicalMaterial**: Extended PBR (clearcoat, transmission, sheen, iridescence)
|
||||
- **MeshToonMaterial**: Cel-shaded / cartoon look
|
||||
- **ShaderMaterial**: Fully custom GLSL shaders
|
||||
- **RawShaderMaterial**: Custom GLSL without Three.js built-in uniforms
|
||||
|
||||
### R3F Material Approach
|
||||
|
||||
In R3F, materials are JSX children of mesh elements:
|
||||
|
||||
| Three.js (Imperative) | R3F (Declarative) |
|
||||
|---|---|
|
||||
| `new MeshStandardMaterial({ color: 'red' })` | `<meshStandardMaterial color="red" />` |
|
||||
| `mesh.material = material` | `<mesh><meshStandardMaterial /></mesh>` |
|
||||
| `material.metalness = 0.8` | `metalness={0.8}` prop |
|
||||
| `material.needsUpdate = true` | Automatic on prop change |
|
||||
| `material.dispose()` | Automatic on unmount |
|
||||
|
||||
### Suaveplan R3F Packages
|
||||
|
||||
| Package | Purpose | Import |
|
||||
|---|---|---|
|
||||
| `@suaveplan/r3f` | Core utilities | `import { useR3F } from '@suaveplan/r3f'` |
|
||||
| `@suaveplan/r3f-shaders` | Custom shader materials | `import { GlassMaterial, HologramMaterial } from '@suaveplan/r3f-shaders'` |
|
||||
| `@suaveplan/r3f-performance` | Material LOD, adaptive quality | `import { usePerformanceMonitor } from '@suaveplan/r3f-performance'` |
|
||||
|
||||
## MeshStandardMaterial: The PBR Default
|
||||
|
||||
The standard PBR material using the metalness/roughness workflow. This should be the default choice for most objects.
|
||||
|
||||
```tsx
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<meshStandardMaterial
|
||||
color="#e74c3c" // Base color (albedo)
|
||||
metalness={0.0} // 0 = dielectric, 1 = metal
|
||||
roughness={0.4} // 0 = mirror, 1 = matte
|
||||
emissive="#000000" // Self-illumination color
|
||||
emissiveIntensity={0} // Emissive strength
|
||||
envMapIntensity={1} // Environment reflection strength
|
||||
flatShading={false} // Faceted vs smooth normals
|
||||
wireframe={false} // Wireframe rendering
|
||||
side={0} // 0=Front, 1=Back, 2=Double
|
||||
/>
|
||||
</mesh>
|
||||
```
|
||||
|
||||
### PBR with Texture Maps
|
||||
|
||||
```tsx
|
||||
import { useTexture } from '@react-three/drei';
|
||||
|
||||
function PBRSurface() {
|
||||
const textures = useTexture({
|
||||
map: '/textures/metal/diffuse.jpg',
|
||||
normalMap: '/textures/metal/normal.jpg',
|
||||
roughnessMap: '/textures/metal/roughness.jpg',
|
||||
metalnessMap: '/textures/metal/metalness.jpg',
|
||||
aoMap: '/textures/metal/ao.jpg',
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<planeGeometry args={[4, 4]} />
|
||||
<meshStandardMaterial
|
||||
{...textures}
|
||||
normalScale={[1, 1]}
|
||||
aoMapIntensity={1}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## MeshPhysicalMaterial: Advanced PBR
|
||||
|
||||
Extends `MeshStandardMaterial` with physically-accurate properties for glass, car paint, fabric, etc.
|
||||
|
||||
### Glass / Transmission
|
||||
|
||||
```tsx
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<meshPhysicalMaterial
|
||||
color="#ffffff"
|
||||
transmission={1} // 0 = opaque, 1 = fully transparent
|
||||
thickness={0.5} // Refraction depth
|
||||
roughness={0.05} // Surface roughness
|
||||
ior={1.5} // Index of refraction (glass = 1.5)
|
||||
chromaticAberration={0.02} // Color fringing
|
||||
envMapIntensity={1}
|
||||
/>
|
||||
</mesh>
|
||||
```
|
||||
|
||||
### Car Paint / Clearcoat
|
||||
|
||||
```tsx
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<meshPhysicalMaterial
|
||||
color="#1a1a2e"
|
||||
metalness={0.9}
|
||||
roughness={0.1}
|
||||
clearcoat={1} // Clearcoat layer strength
|
||||
clearcoatRoughness={0.1} // Clearcoat roughness
|
||||
reflectivity={1}
|
||||
/>
|
||||
</mesh>
|
||||
```
|
||||
|
||||
### Fabric / Sheen
|
||||
|
||||
```tsx
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<meshPhysicalMaterial
|
||||
color="#2d1b69"
|
||||
roughness={0.8}
|
||||
sheen={1} // Sheen intensity (velvet/fabric)
|
||||
sheenColor="#8b5cf6" // Sheen tint color
|
||||
sheenRoughness={0.3} // Sheen roughness
|
||||
/>
|
||||
</mesh>
|
||||
```
|
||||
|
||||
### Iridescence
|
||||
|
||||
```tsx
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<meshPhysicalMaterial
|
||||
color="#333333"
|
||||
metalness={1}
|
||||
roughness={0.2}
|
||||
iridescence={1} // Iridescence intensity
|
||||
iridescenceIOR={1.3} // Thin film IOR
|
||||
iridescenceThicknessRange={[100, 400]} // nm range
|
||||
/>
|
||||
</mesh>
|
||||
```
|
||||
|
||||
## Material Properties Reference
|
||||
|
||||
### Common Properties (All Materials)
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `color` | string/Color | `'#ffffff'` | Base color |
|
||||
| `opacity` | number | `1` | Transparency (requires `transparent={true}`) |
|
||||
| `transparent` | boolean | `false` | Enable transparency |
|
||||
| `alphaTest` | number | `0` | Discard pixels below threshold |
|
||||
| `side` | number | `FrontSide` | Which face to render (0/1/2) |
|
||||
| `visible` | boolean | `true` | Whether to render |
|
||||
| `depthTest` | boolean | `true` | Test against depth buffer |
|
||||
| `depthWrite` | boolean | `true` | Write to depth buffer |
|
||||
| `wireframe` | boolean | `false` | Render as wireframe |
|
||||
| `toneMapped` | boolean | `true` | Apply tone mapping |
|
||||
|
||||
### MeshStandardMaterial Specific
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `metalness` | number | `0` | Metallic appearance (0-1) |
|
||||
| `roughness` | number | `1` | Surface roughness (0-1) |
|
||||
| `emissive` | string/Color | `'#000000'` | Self-illumination color |
|
||||
| `emissiveIntensity` | number | `1` | Emissive strength multiplier |
|
||||
| `envMapIntensity` | number | `1` | Environment map reflection strength |
|
||||
| `flatShading` | boolean | `false` | Use face normals vs vertex normals |
|
||||
| `normalScale` | [x, y] | `[1, 1]` | Normal map strength |
|
||||
| `map` | Texture | `null` | Albedo/diffuse texture |
|
||||
| `normalMap` | Texture | `null` | Normal map texture |
|
||||
| `roughnessMap` | Texture | `null` | Roughness texture (green channel) |
|
||||
| `metalnessMap` | Texture | `null` | Metalness texture (blue channel) |
|
||||
| `aoMap` | Texture | `null` | Ambient occlusion map |
|
||||
| `displacementMap` | Texture | `null` | Displacement/height map |
|
||||
| `emissiveMap` | Texture | `null` | Emissive map |
|
||||
|
||||
## Animated Materials
|
||||
|
||||
### Animating Properties with useFrame
|
||||
|
||||
```tsx
|
||||
import { useRef } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { MathUtils } from 'three';
|
||||
import type { MeshStandardMaterial } from 'three';
|
||||
|
||||
export function PulsingMaterial() {
|
||||
const materialRef = useRef<MeshStandardMaterial>(null);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (!materialRef.current) return;
|
||||
const t = clock.elapsedTime;
|
||||
materialRef.current.emissiveIntensity = (Math.sin(t * 2) + 1) * 0.5;
|
||||
materialRef.current.roughness = MathUtils.lerp(0.1, 0.9, (Math.sin(t) + 1) * 0.5);
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 32, 32]} />
|
||||
<meshStandardMaterial
|
||||
ref={materialRef}
|
||||
color="#4ecdc4"
|
||||
emissive="#4ecdc4"
|
||||
emissiveIntensity={0}
|
||||
metalness={0.5}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Color Transitions
|
||||
|
||||
```tsx
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { Color } from 'three';
|
||||
import type { MeshStandardMaterial } from 'three';
|
||||
|
||||
interface ColorTransitionProps {
|
||||
targetColor: string;
|
||||
speed?: number;
|
||||
}
|
||||
|
||||
export function ColorTransition({ targetColor, speed = 3 }: ColorTransitionProps) {
|
||||
const materialRef = useRef<MeshStandardMaterial>(null);
|
||||
const target = useRef(new Color(targetColor));
|
||||
|
||||
useEffect(() => {
|
||||
target.current.set(targetColor);
|
||||
}, [targetColor]);
|
||||
|
||||
useFrame((_state, delta) => {
|
||||
if (!materialRef.current) return;
|
||||
materialRef.current.color.lerp(target.current, Math.min(delta * speed, 1));
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<boxGeometry />
|
||||
<meshStandardMaterial ref={materialRef} color={targetColor} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## @suaveplan/r3f-shaders Custom Materials
|
||||
|
||||
The `@suaveplan/r3f-shaders` package provides pre-built custom materials:
|
||||
|
||||
```tsx
|
||||
import { GlassMaterial, HologramMaterial, NoiseMaterial } from '@suaveplan/r3f-shaders';
|
||||
|
||||
// Glass with custom refraction
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<GlassMaterial
|
||||
color="#88ccff"
|
||||
ior={1.45}
|
||||
thickness={0.3}
|
||||
distortion={0.1}
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
// Hologram effect
|
||||
<mesh>
|
||||
<boxGeometry args={[1, 2, 1]} />
|
||||
<HologramMaterial
|
||||
color="#00ff88"
|
||||
scanlineSpeed={2}
|
||||
fresnelIntensity={1.5}
|
||||
glitchIntensity={0.3}
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
// Animated noise
|
||||
<mesh>
|
||||
<planeGeometry args={[4, 4, 64, 64]} />
|
||||
<NoiseMaterial
|
||||
color="#4a90d9"
|
||||
scale={3}
|
||||
speed={0.5}
|
||||
displacement={0.2}
|
||||
/>
|
||||
</mesh>
|
||||
```
|
||||
|
||||
## Material Sharing and Instancing
|
||||
|
||||
### Shared Material Across Meshes
|
||||
|
||||
```tsx
|
||||
import { useMemo } from 'react';
|
||||
import { MeshStandardMaterial } from 'three';
|
||||
|
||||
export function SharedMaterialScene() {
|
||||
const sharedMaterial = useMemo(
|
||||
() => new MeshStandardMaterial({ color: 'tomato', roughness: 0.3 }),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<mesh position={[-2, 0, 0]} material={sharedMaterial}>
|
||||
<boxGeometry />
|
||||
</mesh>
|
||||
<mesh position={[0, 0, 0]} material={sharedMaterial}>
|
||||
<sphereGeometry args={[0.5, 32, 32]} />
|
||||
</mesh>
|
||||
<mesh position={[2, 0, 0]} material={sharedMaterial}>
|
||||
<cylinderGeometry args={[0.5, 0.5, 1]} />
|
||||
</mesh>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Material Variants
|
||||
|
||||
```tsx
|
||||
import { useMemo } from 'react';
|
||||
import { MeshStandardMaterial, Color } from 'three';
|
||||
|
||||
type MaterialVariant = 'default' | 'hover' | 'selected' | 'disabled';
|
||||
|
||||
function useMaterialVariants(): Record<MaterialVariant, MeshStandardMaterial> {
|
||||
return useMemo(() => ({
|
||||
default: new MeshStandardMaterial({ color: new Color('#6b7280'), roughness: 0.5 }),
|
||||
hover: new MeshStandardMaterial({ color: new Color('#3b82f6'), roughness: 0.3 }),
|
||||
selected: new MeshStandardMaterial({
|
||||
color: new Color('#10b981'),
|
||||
emissive: new Color('#10b981'),
|
||||
emissiveIntensity: 0.2,
|
||||
roughness: 0.3,
|
||||
}),
|
||||
disabled: new MeshStandardMaterial({
|
||||
color: new Color('#9ca3af'),
|
||||
roughness: 0.8,
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
}),
|
||||
}), []);
|
||||
}
|
||||
```
|
||||
|
||||
## Transparency Patterns
|
||||
|
||||
### Basic Transparency
|
||||
|
||||
```tsx
|
||||
<meshStandardMaterial
|
||||
color="white"
|
||||
transparent
|
||||
opacity={0.5}
|
||||
/>
|
||||
```
|
||||
|
||||
### Alpha Map Transparency
|
||||
|
||||
```tsx
|
||||
import { useTexture } from '@react-three/drei';
|
||||
|
||||
function AlphaMasked() {
|
||||
const alphaMap = useTexture('/textures/leaf-alpha.png');
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<planeGeometry args={[1, 1]} />
|
||||
<meshStandardMaterial
|
||||
color="green"
|
||||
alphaMap={alphaMap}
|
||||
transparent
|
||||
alphaTest={0.5} // Discard pixels below 50% alpha
|
||||
side={2} // DoubleSide for leaves
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Sorting and Depth Issues
|
||||
|
||||
Transparent objects must render back-to-front. Control with `renderOrder` and `depthWrite`:
|
||||
|
||||
```tsx
|
||||
{/* Render behind other transparent objects */}
|
||||
<mesh renderOrder={-1}>
|
||||
<meshStandardMaterial transparent opacity={0.3} depthWrite={false} />
|
||||
</mesh>
|
||||
|
||||
{/* Render in front */}
|
||||
<mesh renderOrder={1}>
|
||||
<meshStandardMaterial transparent opacity={0.6} depthWrite={false} />
|
||||
</mesh>
|
||||
```
|
||||
|
||||
## Special Material Types
|
||||
|
||||
### MeshBasicMaterial (Unlit)
|
||||
|
||||
Use for UI elements, wireframes, or objects that should not be affected by lighting:
|
||||
|
||||
```tsx
|
||||
<meshBasicMaterial color="white" /> // Flat color, no lighting
|
||||
<meshBasicMaterial map={texture} /> // Textured, no lighting
|
||||
<meshBasicMaterial wireframe color="cyan" /> // Wireframe
|
||||
```
|
||||
|
||||
### MeshToonMaterial (Cel Shading)
|
||||
|
||||
```tsx
|
||||
import { useTexture } from '@react-three/drei';
|
||||
|
||||
function ToonObject() {
|
||||
// 3-step gradient ramp texture (3x1 pixels: dark, medium, light)
|
||||
const gradientMap = useTexture('/textures/threeTone.jpg');
|
||||
gradientMap.minFilter = gradientMap.magFilter = 1003; // NearestFilter
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 32, 32]} />
|
||||
<meshToonMaterial color="#ff6b6b" gradientMap={gradientMap} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### MeshMatcapMaterial
|
||||
|
||||
Fast, no lights needed. Appearance defined entirely by a matcap texture:
|
||||
|
||||
```tsx
|
||||
import { useTexture } from '@react-three/drei';
|
||||
|
||||
function MatcapObject() {
|
||||
const matcap = useTexture('/textures/matcap-gold.png');
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<torusKnotGeometry args={[1, 0.3, 128, 32]} />
|
||||
<meshMatcapMaterial matcap={matcap} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Adaptive Material Quality
|
||||
|
||||
```tsx
|
||||
import { useState } from 'react';
|
||||
import { usePerformanceMonitor } from '@suaveplan/r3f-performance';
|
||||
|
||||
export function AdaptiveMaterial() {
|
||||
const [quality, setQuality] = useState<'high' | 'low'>('high');
|
||||
|
||||
usePerformanceMonitor({
|
||||
onDecline: () => setQuality('low'),
|
||||
onIncline: () => setQuality('high'),
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, quality === 'high' ? 64 : 16, quality === 'high' ? 64 : 16]} />
|
||||
{quality === 'high' ? (
|
||||
<meshPhysicalMaterial
|
||||
color="#4ecdc4"
|
||||
metalness={0.8}
|
||||
roughness={0.2}
|
||||
clearcoat={1}
|
||||
clearcoatRoughness={0.1}
|
||||
/>
|
||||
) : (
|
||||
<meshStandardMaterial
|
||||
color="#4ecdc4"
|
||||
metalness={0.8}
|
||||
roughness={0.2}
|
||||
/>
|
||||
)}
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Materials
|
||||
|
||||
### Vitest Tests
|
||||
|
||||
```tsx
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { MeshStandardMaterial, MeshPhysicalMaterial, Color } from 'three';
|
||||
|
||||
describe('Material configuration', () => {
|
||||
it('creates PBR material with correct defaults', () => {
|
||||
const mat = new MeshStandardMaterial({ color: '#ff0000', metalness: 0.8 });
|
||||
expect(mat.metalness).toBe(0.8);
|
||||
expect(mat.color.getHexString()).toBe('ff0000');
|
||||
});
|
||||
|
||||
it('configures glass material', () => {
|
||||
const mat = new MeshPhysicalMaterial({ transmission: 1, ior: 1.5 });
|
||||
expect(mat.transmission).toBe(1);
|
||||
expect(mat.ior).toBe(1.5);
|
||||
});
|
||||
|
||||
it('disposes material correctly', () => {
|
||||
const mat = new MeshStandardMaterial();
|
||||
mat.dispose();
|
||||
expect(mat.disposed).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Storybook Material Gallery
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { OrbitControls, Environment } from '@react-three/drei';
|
||||
|
||||
function MaterialGallery() {
|
||||
const materials = [
|
||||
{ name: 'Metal', props: { metalness: 1, roughness: 0.1, color: '#888' } },
|
||||
{ name: 'Plastic', props: { metalness: 0, roughness: 0.3, color: '#e74c3c' } },
|
||||
{ name: 'Rubber', props: { metalness: 0, roughness: 0.9, color: '#2c3e50' } },
|
||||
];
|
||||
|
||||
return (
|
||||
<Canvas camera={{ position: [0, 0, 6] }} style={{ height: 400 }}>
|
||||
<Environment preset="studio" />
|
||||
{materials.map((m, i) => (
|
||||
<mesh key={m.name} position={[(i - 1) * 2.5, 0, 0]}>
|
||||
<sphereGeometry args={[0.8, 64, 64]} />
|
||||
<meshStandardMaterial {...m.props} />
|
||||
</mesh>
|
||||
))}
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
|
||||
const meta: Meta = { title: 'R3F/Materials/Gallery', component: MaterialGallery };
|
||||
export default meta;
|
||||
export const Default: StoryObj = {};
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
When configuring materials in R3F:
|
||||
|
||||
1. Use `meshStandardMaterial` as the default PBR material
|
||||
2. Use `meshPhysicalMaterial` only when clearcoat, transmission, sheen, or iridescence is needed
|
||||
3. Share materials with `useMemo` to avoid creating duplicates per mesh
|
||||
4. Set `transparent={true}` before using `opacity` or `alphaTest`
|
||||
5. Use `depthWrite={false}` on transparent materials to prevent sorting artifacts
|
||||
6. Animate material properties via refs in `useFrame` (not re-rendering)
|
||||
7. Use `@suaveplan/r3f-shaders` for pre-built custom materials
|
||||
8. Use `@suaveplan/r3f-performance` for adaptive material quality
|
||||
9. Load texture maps with `useTexture` inside `<Suspense>`
|
||||
10. Keep material configuration components under 250 lines
|
||||
@@ -0,0 +1,576 @@
|
||||
---
|
||||
name: threejs-postprocessing
|
||||
description: Three.js post-processing effects adapted for React Three Fiber and Suaveplan R3F packages. Covers @react-three/postprocessing EffectComposer, bloom, SSAO, depth-of-field, vignette, chromatic aberration, custom effects, render passes, and @suaveplan/r3f-performance for monitoring post-processing cost.
|
||||
---
|
||||
|
||||
This skill covers post-processing in Three.js within the React Three Fiber (R3F) context used by the Suaveplan monorepo. All code targets **React 19 + TypeScript + R3F** and follows Suaveplan CLAUDE.md conventions.
|
||||
|
||||
The user needs to add screen-space visual effects to a 3D scene: bloom, ambient occlusion, depth of field, color grading, or custom post-processing passes.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Three.js Post-Processing Pipeline
|
||||
|
||||
Post-processing applies full-screen image effects after the scene is rendered:
|
||||
|
||||
1. Scene renders to a framebuffer (render target) instead of the screen
|
||||
2. The framebuffer texture passes through a chain of shader passes (effects)
|
||||
3. The final result renders to the screen
|
||||
|
||||
### R3F Post-Processing Libraries
|
||||
|
||||
Two libraries exist for R3F post-processing:
|
||||
|
||||
| Library | Approach | Performance | Recommendation |
|
||||
|---|---|---|---|
|
||||
| `@react-three/postprocessing` | Effect merging (pmndrs/postprocessing) | Faster (fewer draw calls) | **Use this** |
|
||||
| `three/examples/jsm/postprocessing` | Pass-based (EffectComposer) | Slower (1 pass per effect) | Legacy only |
|
||||
|
||||
`@react-three/postprocessing` merges compatible effects into a single shader pass, reducing GPU overhead significantly.
|
||||
|
||||
### Suaveplan R3F Packages
|
||||
|
||||
| Package | Purpose | Import |
|
||||
|---|---|---|
|
||||
| `@suaveplan/r3f` | Core scene utilities | `import { useR3F } from '@suaveplan/r3f'` |
|
||||
| `@suaveplan/r3f-performance` | Effect budget monitoring | `import { usePerformanceMonitor } from '@suaveplan/r3f-performance'` |
|
||||
| `@suaveplan/r3f-shaders` | Custom effect shaders | `import { useAnimatedShader } from '@suaveplan/r3f-shaders'` |
|
||||
|
||||
## EffectComposer Setup
|
||||
|
||||
The `<EffectComposer>` component wraps all post-processing effects:
|
||||
|
||||
```tsx
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { EffectComposer, Bloom, Vignette } from '@react-three/postprocessing';
|
||||
|
||||
export function PostProcessedScene() {
|
||||
return (
|
||||
<Canvas>
|
||||
<ambientLight />
|
||||
<directionalLight position={[5, 5, 5]} />
|
||||
<mesh>
|
||||
<boxGeometry />
|
||||
<meshStandardMaterial color="tomato" />
|
||||
</mesh>
|
||||
|
||||
<EffectComposer>
|
||||
<Bloom intensity={0.5} luminanceThreshold={0.8} />
|
||||
<Vignette darkness={0.5} offset={0.3} />
|
||||
</EffectComposer>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### EffectComposer Props
|
||||
|
||||
```tsx
|
||||
<EffectComposer
|
||||
enabled={true} // Toggle all effects
|
||||
depthBuffer={true} // Enable depth buffer (needed for DOF, SSAO)
|
||||
stencilBuffer={false} // Enable stencil buffer
|
||||
autoClear={true} // Clear before render
|
||||
multisampling={8} // MSAA samples (0 to disable)
|
||||
frameBufferType={1016} // HalfFloatType for HDR
|
||||
>
|
||||
{/* Effects here */}
|
||||
</EffectComposer>
|
||||
```
|
||||
|
||||
## Bloom
|
||||
|
||||
Adds glow to bright areas. Pairs with emissive materials for neon/light effects.
|
||||
|
||||
```tsx
|
||||
import { Bloom } from '@react-three/postprocessing';
|
||||
import { BlendFunction } from 'postprocessing';
|
||||
|
||||
<EffectComposer>
|
||||
<Bloom
|
||||
intensity={1.0} // Bloom strength
|
||||
luminanceThreshold={0.9} // Brightness cutoff (0-1)
|
||||
luminanceSmoothing={0.025} // Softness of the threshold
|
||||
mipmapBlur={true} // Use mipmap blur (better quality)
|
||||
radius={0.8} // Blur radius
|
||||
blendFunction={BlendFunction.ADD} // How to composite
|
||||
/>
|
||||
</EffectComposer>
|
||||
```
|
||||
|
||||
### Selective Bloom with Emissive Materials
|
||||
|
||||
Only emissive objects produce bloom when `luminanceThreshold` is high:
|
||||
|
||||
```tsx
|
||||
export function NeonScene() {
|
||||
return (
|
||||
<>
|
||||
{/* This glows (emissive > threshold) */}
|
||||
<mesh position={[0, 1, 0]}>
|
||||
<sphereGeometry args={[0.3, 32, 32]} />
|
||||
<meshStandardMaterial
|
||||
color="#ff0080"
|
||||
emissive="#ff0080"
|
||||
emissiveIntensity={5}
|
||||
toneMapped={false} // Allow values > 1 for HDR bloom
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
{/* This does not glow (no emissive) */}
|
||||
<mesh>
|
||||
<boxGeometry />
|
||||
<meshStandardMaterial color="gray" />
|
||||
</mesh>
|
||||
|
||||
<EffectComposer>
|
||||
<Bloom
|
||||
luminanceThreshold={1}
|
||||
intensity={1.5}
|
||||
mipmapBlur
|
||||
/>
|
||||
</EffectComposer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Screen-Space Ambient Occlusion (SSAO)
|
||||
|
||||
Adds soft shadows in crevices and corners. Provides depth and realism.
|
||||
|
||||
```tsx
|
||||
import { SSAO } from '@react-three/postprocessing';
|
||||
import { BlendFunction } from 'postprocessing';
|
||||
|
||||
<EffectComposer>
|
||||
<SSAO
|
||||
blendFunction={BlendFunction.MULTIPLY}
|
||||
samples={30} // Quality (more = slower)
|
||||
radius={5} // AO radius in world units
|
||||
intensity={30} // AO intensity
|
||||
luminanceInfluence={0.6} // Bright areas occlude less
|
||||
bias={0.035} // Depth bias
|
||||
worldDistanceThreshold={20}
|
||||
worldDistanceFalloff={5}
|
||||
worldProximityThreshold={0.5}
|
||||
worldProximityFalloff={0.3}
|
||||
/>
|
||||
</EffectComposer>
|
||||
```
|
||||
|
||||
### N8AO (Alternative SSAO)
|
||||
|
||||
Higher quality SSAO from drei:
|
||||
|
||||
```tsx
|
||||
import { N8AO } from '@react-three/postprocessing';
|
||||
|
||||
<EffectComposer>
|
||||
<N8AO
|
||||
aoRadius={1}
|
||||
intensity={2}
|
||||
distanceFalloff={1}
|
||||
color="black"
|
||||
quality="medium" // 'low' | 'medium' | 'high' | 'ultra'
|
||||
/>
|
||||
</EffectComposer>
|
||||
```
|
||||
|
||||
## Depth of Field (DOF)
|
||||
|
||||
Simulates camera lens focus with bokeh blur on out-of-focus areas.
|
||||
|
||||
```tsx
|
||||
import { DepthOfField } from '@react-three/postprocessing';
|
||||
|
||||
<EffectComposer>
|
||||
<DepthOfField
|
||||
focusDistance={0} // Normalized focus distance (0-1)
|
||||
focalLength={0.02} // Focal length (affects DOF range)
|
||||
bokehScale={6} // Bokeh size
|
||||
height={480} // Resolution of the effect
|
||||
/>
|
||||
</EffectComposer>
|
||||
```
|
||||
|
||||
### Auto-Focus DOF
|
||||
|
||||
Focus on what the user is looking at:
|
||||
|
||||
```tsx
|
||||
import { useRef } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { DepthOfField } from '@react-three/postprocessing';
|
||||
import { Vector3 } from 'three';
|
||||
|
||||
export function AutoFocusDOF() {
|
||||
const depthRef = useRef<any>(null);
|
||||
const focusPoint = useRef(new Vector3());
|
||||
|
||||
useFrame(({ camera, raycaster, pointer, scene }) => {
|
||||
if (!depthRef.current) return;
|
||||
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const hits = raycaster.intersectObjects(scene.children, true);
|
||||
|
||||
if (hits.length > 0) {
|
||||
const distance = hits[0].distance;
|
||||
const normalizedDistance = distance / camera.far;
|
||||
depthRef.current.focusDistance = normalizedDistance;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<EffectComposer>
|
||||
<DepthOfField
|
||||
ref={depthRef}
|
||||
focusDistance={0.01}
|
||||
focalLength={0.02}
|
||||
bokehScale={4}
|
||||
/>
|
||||
</EffectComposer>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Color Grading and Tone
|
||||
|
||||
### Tone Mapping
|
||||
|
||||
```tsx
|
||||
import { ToneMapping } from '@react-three/postprocessing';
|
||||
import { ToneMappingMode } from 'postprocessing';
|
||||
|
||||
<EffectComposer>
|
||||
<ToneMapping
|
||||
mode={ToneMappingMode.ACES_FILMIC}
|
||||
// Options: LINEAR, REINHARD, REINHARD2, REINHARD2_ADAPTIVE,
|
||||
// UNCHARTED2, OPTIMIZED_CINEON, ACES_FILMIC, AGX, NEUTRAL
|
||||
/>
|
||||
</EffectComposer>
|
||||
```
|
||||
|
||||
### Brightness/Contrast
|
||||
|
||||
```tsx
|
||||
import { BrightnessContrast } from '@react-three/postprocessing';
|
||||
|
||||
<EffectComposer>
|
||||
<BrightnessContrast brightness={0.05} contrast={0.1} />
|
||||
</EffectComposer>
|
||||
```
|
||||
|
||||
### Hue/Saturation
|
||||
|
||||
```tsx
|
||||
import { HueSaturation } from '@react-three/postprocessing';
|
||||
|
||||
<EffectComposer>
|
||||
<HueSaturation hue={0} saturation={0.15} />
|
||||
</EffectComposer>
|
||||
```
|
||||
|
||||
### LUT (Lookup Table) Color Grading
|
||||
|
||||
```tsx
|
||||
import { LUT3D } from '@react-three/postprocessing';
|
||||
import { useLoader } from '@react-three/fiber';
|
||||
import { LUTCubeLoader } from 'postprocessing';
|
||||
|
||||
function CinematicGrade() {
|
||||
const texture = useLoader(LUTCubeLoader, '/luts/cinematic.cube');
|
||||
|
||||
return (
|
||||
<EffectComposer>
|
||||
<LUT3D lut={texture} />
|
||||
</EffectComposer>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Screen Effects
|
||||
|
||||
### Vignette
|
||||
|
||||
```tsx
|
||||
import { Vignette } from '@react-three/postprocessing';
|
||||
import { BlendFunction } from 'postprocessing';
|
||||
|
||||
<Vignette
|
||||
offset={0.3} // Inner radius
|
||||
darkness={0.7} // Edge darkness
|
||||
blendFunction={BlendFunction.NORMAL}
|
||||
/>
|
||||
```
|
||||
|
||||
### Chromatic Aberration
|
||||
|
||||
```tsx
|
||||
import { ChromaticAberration } from '@react-three/postprocessing';
|
||||
import { BlendFunction } from 'postprocessing';
|
||||
import { Vector2 } from 'three';
|
||||
|
||||
<ChromaticAberration
|
||||
offset={new Vector2(0.002, 0.002)} // R/G/B channel offset
|
||||
blendFunction={BlendFunction.NORMAL}
|
||||
radialModulation={true} // Stronger at edges
|
||||
modulationOffset={0.5}
|
||||
/>
|
||||
```
|
||||
|
||||
### Noise / Film Grain
|
||||
|
||||
```tsx
|
||||
import { Noise } from '@react-three/postprocessing';
|
||||
import { BlendFunction } from 'postprocessing';
|
||||
|
||||
<Noise
|
||||
premultiply // Multiply with scene color
|
||||
blendFunction={BlendFunction.ADD}
|
||||
opacity={0.5}
|
||||
/>
|
||||
```
|
||||
|
||||
### Scanlines
|
||||
|
||||
```tsx
|
||||
import { Scanline } from '@react-three/postprocessing';
|
||||
import { BlendFunction } from 'postprocessing';
|
||||
|
||||
<Scanline
|
||||
density={1.5}
|
||||
blendFunction={BlendFunction.OVERLAY}
|
||||
opacity={0.1}
|
||||
/>
|
||||
```
|
||||
|
||||
## Effect Combinations: Recipes
|
||||
|
||||
### Cinematic Look
|
||||
|
||||
```tsx
|
||||
<EffectComposer>
|
||||
<Bloom luminanceThreshold={0.8} intensity={0.5} mipmapBlur />
|
||||
<DepthOfField focusDistance={0.01} focalLength={0.02} bokehScale={3} />
|
||||
<Vignette offset={0.3} darkness={0.6} />
|
||||
<HueSaturation saturation={-0.1} />
|
||||
<BrightnessContrast contrast={0.05} />
|
||||
<Noise opacity={0.03} premultiply />
|
||||
</EffectComposer>
|
||||
```
|
||||
|
||||
### Neon / Cyberpunk
|
||||
|
||||
```tsx
|
||||
<EffectComposer>
|
||||
<Bloom luminanceThreshold={0.5} intensity={2} mipmapBlur radius={0.9} />
|
||||
<ChromaticAberration offset={new Vector2(0.003, 0.003)} radialModulation />
|
||||
<Scanline density={1.2} opacity={0.05} />
|
||||
<Noise opacity={0.04} premultiply />
|
||||
<Vignette darkness={0.8} />
|
||||
</EffectComposer>
|
||||
```
|
||||
|
||||
### Clean Product Visualization
|
||||
|
||||
```tsx
|
||||
<EffectComposer multisampling={8}>
|
||||
<N8AO aoRadius={0.5} intensity={1} quality="high" />
|
||||
<Bloom luminanceThreshold={1.2} intensity={0.3} mipmapBlur />
|
||||
<ToneMapping mode={ToneMappingMode.ACES_FILMIC} />
|
||||
</EffectComposer>
|
||||
```
|
||||
|
||||
## Custom Effects
|
||||
|
||||
### Writing a Custom Effect
|
||||
|
||||
```tsx
|
||||
import { forwardRef, useMemo } from 'react';
|
||||
import { Effect, BlendFunction } from 'postprocessing';
|
||||
import { Uniform } from 'three';
|
||||
|
||||
const fragmentShader = /* glsl */ `
|
||||
uniform float intensity;
|
||||
uniform float time;
|
||||
|
||||
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
|
||||
vec2 center = uv - 0.5;
|
||||
float dist = length(center);
|
||||
float wave = sin(dist * 20.0 - time * 3.0) * intensity;
|
||||
vec2 distortedUv = uv + center * wave * 0.02;
|
||||
outputColor = texture2D(inputBuffer, distortedUv);
|
||||
}
|
||||
`;
|
||||
|
||||
class RippleEffectImpl extends Effect {
|
||||
constructor({ intensity = 0.5, blendFunction = BlendFunction.NORMAL } = {}) {
|
||||
super('RippleEffect', fragmentShader, {
|
||||
blendFunction,
|
||||
uniforms: new Map([
|
||||
['intensity', new Uniform(intensity)],
|
||||
['time', new Uniform(0)],
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
||||
update(_renderer: any, _inputBuffer: any, deltaTime: number) {
|
||||
const time = this.uniforms.get('time')!;
|
||||
time.value += deltaTime;
|
||||
}
|
||||
}
|
||||
|
||||
export const RippleEffect = forwardRef<RippleEffectImpl, { intensity?: number }>(
|
||||
({ intensity = 0.5 }, ref) => {
|
||||
const effect = useMemo(() => new RippleEffectImpl({ intensity }), [intensity]);
|
||||
return <primitive ref={ref} object={effect} dispose={null} />;
|
||||
}
|
||||
);
|
||||
|
||||
// Usage:
|
||||
// <EffectComposer>
|
||||
// <RippleEffect intensity={0.3} />
|
||||
// </EffectComposer>
|
||||
```
|
||||
|
||||
## Adaptive Post-Processing
|
||||
|
||||
Use `@suaveplan/r3f-performance` to scale effects based on frame rate:
|
||||
|
||||
```tsx
|
||||
import { useState, useCallback } from 'react';
|
||||
import { EffectComposer, Bloom, SSAO, Vignette } from '@react-three/postprocessing';
|
||||
import { usePerformanceMonitor } from '@suaveplan/r3f-performance';
|
||||
|
||||
type EffectQuality = 'high' | 'medium' | 'low' | 'off';
|
||||
|
||||
export function AdaptiveEffects() {
|
||||
const [quality, setQuality] = useState<EffectQuality>('high');
|
||||
|
||||
usePerformanceMonitor({
|
||||
onDecline: ({ fps }) => {
|
||||
if (fps < 20) setQuality('off');
|
||||
else if (fps < 30) setQuality('low');
|
||||
else if (fps < 45) setQuality('medium');
|
||||
},
|
||||
onIncline: ({ fps }) => {
|
||||
if (fps > 55) setQuality('high');
|
||||
else if (fps > 45) setQuality('medium');
|
||||
else if (fps > 30) setQuality('low');
|
||||
},
|
||||
});
|
||||
|
||||
if (quality === 'off') return null;
|
||||
|
||||
return (
|
||||
<EffectComposer multisampling={quality === 'high' ? 8 : quality === 'medium' ? 4 : 0}>
|
||||
<Bloom
|
||||
intensity={quality === 'high' ? 1 : 0.5}
|
||||
luminanceThreshold={0.8}
|
||||
mipmapBlur
|
||||
/>
|
||||
{quality !== 'low' && (
|
||||
<SSAO
|
||||
samples={quality === 'high' ? 30 : 15}
|
||||
radius={5}
|
||||
intensity={20}
|
||||
/>
|
||||
)}
|
||||
<Vignette darkness={0.5} />
|
||||
</EffectComposer>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Guidelines
|
||||
|
||||
### Effect Cost Hierarchy (Approximate)
|
||||
|
||||
| Effect | Cost | Notes |
|
||||
|---|---|---|
|
||||
| Vignette | Very Low | Single texture lookup |
|
||||
| Noise | Very Low | Math-only |
|
||||
| BrightnessContrast | Very Low | Per-pixel math |
|
||||
| HueSaturation | Low | Color space conversion |
|
||||
| ChromaticAberration | Low | 3 texture lookups |
|
||||
| Bloom (mipmap) | Medium | Multi-pass blur |
|
||||
| DOF | Medium-High | Multi-sample blur |
|
||||
| SSAO | High | Many depth samples |
|
||||
| N8AO (ultra) | Very High | Many samples + blur |
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. Merge effects into a single `<EffectComposer>` (auto-merged into fewer passes)
|
||||
2. Use `mipmapBlur` for Bloom (faster than kernel blur)
|
||||
3. Reduce `samples` count on SSAO for mobile
|
||||
4. Use `multisampling={0}` on mobile devices
|
||||
5. Disable effects entirely when FPS drops below threshold
|
||||
6. Use `HalfFloatType` framebuffer for HDR bloom
|
||||
|
||||
## Testing Post-Processing
|
||||
|
||||
### Vitest Tests
|
||||
|
||||
```tsx
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('Post-processing setup', () => {
|
||||
it('imports effects without errors', async () => {
|
||||
const { Bloom, Vignette, SSAO } = await import('@react-three/postprocessing');
|
||||
expect(Bloom).toBeDefined();
|
||||
expect(Vignette).toBeDefined();
|
||||
expect(SSAO).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Storybook Stories
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { OrbitControls, Environment } from '@react-three/drei';
|
||||
import { EffectComposer, Bloom, Vignette } from '@react-three/postprocessing';
|
||||
|
||||
function BloomDemo() {
|
||||
return (
|
||||
<Canvas style={{ height: 400 }}>
|
||||
<Environment preset="studio" />
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<meshStandardMaterial
|
||||
color="#ff0080"
|
||||
emissive="#ff0080"
|
||||
emissiveIntensity={3}
|
||||
toneMapped={false}
|
||||
/>
|
||||
</mesh>
|
||||
<EffectComposer>
|
||||
<Bloom luminanceThreshold={0.8} intensity={1.5} mipmapBlur />
|
||||
<Vignette darkness={0.5} />
|
||||
</EffectComposer>
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
|
||||
const meta: Meta = { title: 'R3F/PostProcessing/Bloom', component: BloomDemo };
|
||||
export default meta;
|
||||
export const Default: StoryObj = {};
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
When adding post-processing to R3F:
|
||||
|
||||
1. Use `@react-three/postprocessing` (effect merging) over Three.js pass-based
|
||||
2. Place `<EffectComposer>` as a sibling to scene content, not wrapping it
|
||||
3. Enable `depthBuffer` for effects that need depth (DOF, SSAO)
|
||||
4. Use `toneMapped={false}` on emissive materials for HDR bloom
|
||||
5. Set `luminanceThreshold` high to prevent everything from blooming
|
||||
6. Start with low SSAO samples and increase to taste
|
||||
7. Implement adaptive quality with `@suaveplan/r3f-performance`
|
||||
8. Profile with browser DevTools GPU panel to measure effect cost
|
||||
9. Disable all effects as a fallback for low-end devices
|
||||
10. Keep post-processing configuration under 250 lines per file
|
||||
@@ -0,0 +1,572 @@
|
||||
---
|
||||
name: threejs-shaders
|
||||
description: Three.js shader programming adapted for React Three Fiber and Suaveplan R3F packages. Covers shaderMaterial in JSX, the @suaveplan/r3f-shaders library, GLSL vertex and fragment shaders with React refs, uniform animation via useFrame, the extend() pattern for custom JSX elements, and onBeforeCompile for material patching.
|
||||
---
|
||||
|
||||
This skill covers custom shader development in Three.js within the React Three Fiber (R3F) context used by the Suaveplan monorepo. All code targets **React 19 + TypeScript + R3F** and follows Suaveplan CLAUDE.md conventions.
|
||||
|
||||
The user needs to write custom GLSL shaders for visual effects, procedural materials, particle systems, or compute-like operations in the GPU.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Three.js Shader Pipeline
|
||||
|
||||
Three.js shaders run on the GPU as GLSL programs:
|
||||
|
||||
- **Vertex Shader**: Transforms vertex positions (runs per vertex)
|
||||
- **Fragment Shader**: Determines pixel color (runs per fragment/pixel)
|
||||
- **Uniforms**: Values passed from JavaScript to GLSL (same for all vertices/fragments)
|
||||
- **Attributes**: Per-vertex data (position, normal, UV)
|
||||
- **Varyings**: Values interpolated from vertex to fragment shader
|
||||
|
||||
### R3F Shader Approach
|
||||
|
||||
| Three.js (Imperative) | R3F (Declarative) |
|
||||
|---|---|
|
||||
| `new ShaderMaterial({ uniforms, vertexShader, fragmentShader })` | `<shaderMaterial>` JSX or `extend()` |
|
||||
| `material.uniforms.time.value = t` | Ref + `useFrame` |
|
||||
| `new RawShaderMaterial(...)` | `<rawShaderMaterial>` |
|
||||
| Custom class extending Material | `extend({ CustomMaterial })` |
|
||||
|
||||
### Suaveplan R3F Packages
|
||||
|
||||
| Package | Purpose | Import |
|
||||
|---|---|---|
|
||||
| `@suaveplan/r3f-shaders` | Pre-built shader materials and utilities | `import { GlassMaterial, HologramMaterial, useShaderUniforms } from '@suaveplan/r3f-shaders'` |
|
||||
| `@suaveplan/r3f` | Core R3F utilities | `import { useR3F } from '@suaveplan/r3f'` |
|
||||
| `@suaveplan/r3f-performance` | Shader performance monitoring | `import { usePerformanceMonitor } from '@suaveplan/r3f-performance'` |
|
||||
|
||||
## ShaderMaterial in JSX
|
||||
|
||||
### Basic Custom Shader
|
||||
|
||||
```tsx
|
||||
import { useRef, useMemo } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { ShaderMaterial, Color } from 'three';
|
||||
|
||||
const vertexShader = /* glsl */ `
|
||||
varying vec2 vUv;
|
||||
varying vec3 vNormal;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
vNormal = normalize(normalMatrix * normal);
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const fragmentShader = /* glsl */ `
|
||||
uniform float uTime;
|
||||
uniform vec3 uColor;
|
||||
varying vec2 vUv;
|
||||
varying vec3 vNormal;
|
||||
|
||||
void main() {
|
||||
float stripe = sin(vUv.y * 20.0 + uTime * 2.0) * 0.5 + 0.5;
|
||||
vec3 color = mix(uColor, uColor * 0.3, step(0.5, stripe));
|
||||
float fresnel = pow(1.0 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 2.0);
|
||||
color += fresnel * 0.3;
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
export function StripedMaterial({ color = '#4ecdc4' }: { color?: string }) {
|
||||
const materialRef = useRef<ShaderMaterial>(null);
|
||||
|
||||
const uniforms = useMemo(
|
||||
() => ({
|
||||
uTime: { value: 0 },
|
||||
uColor: { value: new Color(color) },
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (materialRef.current) {
|
||||
materialRef.current.uniforms.uTime.value = clock.elapsedTime;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<shaderMaterial
|
||||
ref={materialRef}
|
||||
vertexShader={vertexShader}
|
||||
fragmentShader={fragmentShader}
|
||||
uniforms={uniforms}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Key Rule: useMemo for Uniforms
|
||||
|
||||
Always wrap uniforms in `useMemo`. Without it, R3F recreates the material every render:
|
||||
|
||||
```tsx
|
||||
// CORRECT: uniforms created once
|
||||
const uniforms = useMemo(() => ({
|
||||
uTime: { value: 0 },
|
||||
uResolution: { value: new Vector2(window.innerWidth, window.innerHeight) },
|
||||
}), []);
|
||||
|
||||
// WRONG: new uniforms object every render, causes material recreation
|
||||
const uniforms = {
|
||||
uTime: { value: 0 },
|
||||
};
|
||||
```
|
||||
|
||||
## extend(): Custom JSX Elements
|
||||
|
||||
For reusable shader materials, use `extend()` to register a custom Three.js class as a JSX element:
|
||||
|
||||
```tsx
|
||||
import { extend, useFrame } from '@react-three/fiber';
|
||||
import { shaderMaterial } from '@react-three/drei';
|
||||
import { Color, Vector2 } from 'three';
|
||||
import { useRef } from 'react';
|
||||
|
||||
// Create a typed shader material class
|
||||
const WaveMaterial = shaderMaterial(
|
||||
// Uniforms with defaults
|
||||
{
|
||||
uTime: 0,
|
||||
uColor: new Color('#ff6b6b'),
|
||||
uAmplitude: 0.3,
|
||||
uFrequency: 4.0,
|
||||
},
|
||||
// Vertex shader
|
||||
/* glsl */ `
|
||||
uniform float uTime;
|
||||
uniform float uAmplitude;
|
||||
uniform float uFrequency;
|
||||
varying vec2 vUv;
|
||||
varying float vElevation;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
vec3 pos = position;
|
||||
float elevation = sin(pos.x * uFrequency + uTime) * uAmplitude
|
||||
+ sin(pos.z * uFrequency * 0.7 + uTime * 0.8) * uAmplitude * 0.5;
|
||||
pos.y += elevation;
|
||||
vElevation = elevation;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
|
||||
}
|
||||
`,
|
||||
// Fragment shader
|
||||
/* glsl */ `
|
||||
uniform vec3 uColor;
|
||||
uniform float uAmplitude;
|
||||
varying vec2 vUv;
|
||||
varying float vElevation;
|
||||
|
||||
void main() {
|
||||
float brightness = (vElevation / uAmplitude + 1.0) * 0.5;
|
||||
vec3 color = mix(uColor * 0.3, uColor, brightness);
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
// Register as JSX element
|
||||
extend({ WaveMaterial });
|
||||
|
||||
// TypeScript declaration for JSX
|
||||
declare module '@react-three/fiber' {
|
||||
interface ThreeElements {
|
||||
waveMaterial: JSX.IntrinsicElements['shaderMaterial'] & {
|
||||
uTime?: number;
|
||||
uColor?: Color;
|
||||
uAmplitude?: number;
|
||||
uFrequency?: number;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Usage component
|
||||
export function WavePlane() {
|
||||
const materialRef = useRef<typeof WaveMaterial.prototype>(null);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (materialRef.current) {
|
||||
materialRef.current.uTime = clock.elapsedTime;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<planeGeometry args={[10, 10, 128, 128]} />
|
||||
<waveMaterial
|
||||
ref={materialRef}
|
||||
uColor={new Color('#4ecdc4')}
|
||||
uAmplitude={0.3}
|
||||
uFrequency={4}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## GLSL Built-in Variables
|
||||
|
||||
### Vertex Shader
|
||||
|
||||
| Variable | Type | Description |
|
||||
|---|---|---|
|
||||
| `position` | `vec3` | Vertex position (attribute) |
|
||||
| `normal` | `vec3` | Vertex normal (attribute) |
|
||||
| `uv` | `vec2` | Texture coordinate (attribute) |
|
||||
| `modelMatrix` | `mat4` | Object to world transform |
|
||||
| `viewMatrix` | `mat4` | World to camera transform |
|
||||
| `projectionMatrix` | `mat4` | Camera to clip space |
|
||||
| `modelViewMatrix` | `mat4` | `viewMatrix * modelMatrix` |
|
||||
| `normalMatrix` | `mat3` | Inverse transpose of modelView (for normals) |
|
||||
| `cameraPosition` | `vec3` | Camera world position |
|
||||
| `gl_Position` | `vec4` | **Output**: clip-space position |
|
||||
|
||||
### Fragment Shader
|
||||
|
||||
| Variable | Type | Description |
|
||||
|---|---|---|
|
||||
| `gl_FragCoord` | `vec4` | Pixel position in screen space |
|
||||
| `gl_FrontFacing` | `bool` | Whether fragment is on front face |
|
||||
| `gl_FragColor` | `vec4` | **Output**: pixel color (deprecated, use `out vec4`) |
|
||||
|
||||
## Common GLSL Patterns
|
||||
|
||||
### Fresnel Effect
|
||||
|
||||
```glsl
|
||||
// In vertex shader
|
||||
varying vec3 vViewDirection;
|
||||
varying vec3 vWorldNormal;
|
||||
|
||||
void main() {
|
||||
vec4 worldPos = modelMatrix * vec4(position, 1.0);
|
||||
vViewDirection = normalize(cameraPosition - worldPos.xyz);
|
||||
vWorldNormal = normalize((modelMatrix * vec4(normal, 0.0)).xyz);
|
||||
gl_Position = projectionMatrix * viewMatrix * worldPos;
|
||||
}
|
||||
|
||||
// In fragment shader
|
||||
void main() {
|
||||
float fresnel = pow(1.0 - dot(vViewDirection, vWorldNormal), 3.0);
|
||||
vec3 color = mix(vec3(0.1), vec3(0.0, 0.8, 1.0), fresnel);
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
### Noise Functions
|
||||
|
||||
```glsl
|
||||
// Simple 2D noise (include in fragment shader)
|
||||
float random(vec2 st) {
|
||||
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453);
|
||||
}
|
||||
|
||||
// Simplex-like value noise
|
||||
float noise(vec2 st) {
|
||||
vec2 i = floor(st);
|
||||
vec2 f = fract(st);
|
||||
float a = random(i);
|
||||
float b = random(i + vec2(1.0, 0.0));
|
||||
float c = random(i + vec2(0.0, 1.0));
|
||||
float d = random(i + vec2(1.0, 1.0));
|
||||
vec2 u = f * f * (3.0 - 2.0 * f); // smoothstep
|
||||
return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
|
||||
}
|
||||
|
||||
// FBM (Fractal Brownian Motion) for complex noise
|
||||
float fbm(vec2 st) {
|
||||
float value = 0.0;
|
||||
float amplitude = 0.5;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
value += amplitude * noise(st);
|
||||
st *= 2.0;
|
||||
amplitude *= 0.5;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
### SDF (Signed Distance Functions)
|
||||
|
||||
Common 2D SDFs: `sdCircle(p, r)` = `length(p) - r`, `sdBox(p, b)` = `length(max(abs(p) - b, 0.0))`. Use for procedural shapes, soft shadows, and UI in shaders. Combine with `smoothstep` for anti-aliased edges.
|
||||
|
||||
## @suaveplan/r3f-shaders Library
|
||||
|
||||
Pre-built shader materials from the Suaveplan shader library:
|
||||
|
||||
### Available Shaders
|
||||
|
||||
```tsx
|
||||
import {
|
||||
GlassMaterial,
|
||||
HologramMaterial,
|
||||
NoiseMaterial,
|
||||
GradientMaterial,
|
||||
FresnelMaterial,
|
||||
GridMaterial,
|
||||
DissolveEffect,
|
||||
} from '@suaveplan/r3f-shaders';
|
||||
|
||||
// Glass with custom refraction
|
||||
<GlassMaterial
|
||||
color="#88ccff"
|
||||
ior={1.45}
|
||||
thickness={0.3}
|
||||
distortion={0.1}
|
||||
chromaticAberration={0.02}
|
||||
/>
|
||||
|
||||
// Sci-fi hologram
|
||||
<HologramMaterial
|
||||
color="#00ff88"
|
||||
scanlineSpeed={2}
|
||||
fresnelIntensity={1.5}
|
||||
glitchIntensity={0.3}
|
||||
wireframe
|
||||
/>
|
||||
|
||||
// Procedural noise surface
|
||||
<NoiseMaterial
|
||||
color="#4a90d9"
|
||||
scale={3}
|
||||
speed={0.5}
|
||||
displacement={0.2}
|
||||
octaves={4}
|
||||
/>
|
||||
|
||||
// Two-color gradient
|
||||
<GradientMaterial
|
||||
colorA="#ff6b6b"
|
||||
colorB="#4ecdc4"
|
||||
direction="vertical"
|
||||
/>
|
||||
```
|
||||
|
||||
### Creating Custom Shaders for the Library
|
||||
|
||||
Follow the `@suaveplan/r3f-shaders` pattern:
|
||||
|
||||
```tsx
|
||||
// src/materials/my-effect/my-effect.ts
|
||||
import { shaderMaterial } from '@react-three/drei';
|
||||
import { Color } from 'three';
|
||||
|
||||
export const MyEffectMaterial = shaderMaterial(
|
||||
{ uTime: 0, uColor: new Color('#ffffff'), uIntensity: 1.0 },
|
||||
/* glsl */ `/* vertex shader */`,
|
||||
/* glsl */ `/* fragment shader */`
|
||||
);
|
||||
|
||||
// src/materials/my-effect/my-effect.types.ts
|
||||
import type { Color } from 'three';
|
||||
export interface MyEffectMaterialProps {
|
||||
uTime?: number;
|
||||
uColor?: Color | string;
|
||||
uIntensity?: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Uniform Animation Patterns
|
||||
|
||||
### Time-Based Animation
|
||||
|
||||
```tsx
|
||||
import { useRef } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import type { ShaderMaterial } from 'three';
|
||||
|
||||
export function AnimatedShader() {
|
||||
const matRef = useRef<ShaderMaterial>(null);
|
||||
|
||||
useFrame(({ clock, pointer }) => {
|
||||
if (!matRef.current) return;
|
||||
matRef.current.uniforms.uTime.value = clock.elapsedTime;
|
||||
matRef.current.uniforms.uMouse.value.set(pointer.x, pointer.y);
|
||||
});
|
||||
|
||||
// ... shader material with uTime and uMouse uniforms
|
||||
}
|
||||
```
|
||||
|
||||
### Reactive Uniforms (Props to Shader)
|
||||
|
||||
```tsx
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { Color } from 'three';
|
||||
import type { ShaderMaterial } from 'three';
|
||||
|
||||
interface ReactiveShaderProps {
|
||||
color: string;
|
||||
intensity: number;
|
||||
}
|
||||
|
||||
export function ReactiveShader({ color, intensity }: ReactiveShaderProps) {
|
||||
const matRef = useRef<ShaderMaterial>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!matRef.current) return;
|
||||
(matRef.current.uniforms.uColor.value as Color).set(color);
|
||||
matRef.current.uniforms.uIntensity.value = intensity;
|
||||
}, [color, intensity]);
|
||||
|
||||
// ... shader material
|
||||
}
|
||||
```
|
||||
|
||||
## onBeforeCompile: Patching Built-in Materials
|
||||
|
||||
Modify Three.js built-in materials by injecting GLSL:
|
||||
|
||||
```tsx
|
||||
import { useRef, useCallback } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import type { MeshStandardMaterial, Shader } from 'three';
|
||||
|
||||
export function PatchedMaterial() {
|
||||
const matRef = useRef<MeshStandardMaterial>(null);
|
||||
const shaderRef = useRef<Shader | null>(null);
|
||||
|
||||
const onBeforeCompile = useCallback((shader: Shader) => {
|
||||
shaderRef.current = shader;
|
||||
shader.uniforms.uTime = { value: 0 };
|
||||
|
||||
// Inject uniform declaration
|
||||
shader.vertexShader = shader.vertexShader.replace(
|
||||
'#include <common>',
|
||||
/* glsl */ `
|
||||
#include <common>
|
||||
uniform float uTime;
|
||||
`
|
||||
);
|
||||
|
||||
// Inject vertex displacement
|
||||
shader.vertexShader = shader.vertexShader.replace(
|
||||
'#include <begin_vertex>',
|
||||
/* glsl */ `
|
||||
#include <begin_vertex>
|
||||
transformed.y += sin(transformed.x * 4.0 + uTime * 2.0) * 0.2;
|
||||
`
|
||||
);
|
||||
}, []);
|
||||
|
||||
useFrame(({ clock }) => {
|
||||
if (shaderRef.current) {
|
||||
shaderRef.current.uniforms.uTime.value = clock.elapsedTime;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<planeGeometry args={[5, 5, 64, 64]} />
|
||||
<meshStandardMaterial
|
||||
ref={matRef}
|
||||
color="steelblue"
|
||||
onBeforeCompile={onBeforeCompile}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## RawShaderMaterial
|
||||
|
||||
For full control without Three.js built-in uniforms/attributes, use `<rawShaderMaterial>`. You must manually declare all attributes (`position`, `uv`), uniforms (`projectionMatrix`, `modelViewMatrix`), and set `precision`. Supports `glslVersion="300 es"` for GLSL 3.0.
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Minimize texture lookups**: Each `texture2D()` call has a cost. Cache results in variables.
|
||||
2. **Avoid branches**: GPU prefers math over `if/else`. Use `step()`, `smoothstep()`, `mix()` instead.
|
||||
3. **Limit loop iterations**: Unroll small loops. Keep `for` loops short and bounded.
|
||||
4. **Use mediump where possible**: `precision mediump float` saves bandwidth on mobile.
|
||||
5. **Pre-calculate on CPU**: Move invariant math to `useFrame` and pass as uniforms.
|
||||
|
||||
```glsl
|
||||
// SLOW: branching
|
||||
if (dist < radius) { color = vec3(1.0); }
|
||||
else { color = vec3(0.0); }
|
||||
|
||||
// FAST: step function
|
||||
color = vec3(step(dist, radius));
|
||||
|
||||
// SLOW: multiple conditions
|
||||
if (x > 0.0 && x < 1.0 && y > 0.0 && y < 1.0) { ... }
|
||||
|
||||
// FAST: step math
|
||||
float inside = step(0.0, x) * step(x, 1.0) * step(0.0, y) * step(y, 1.0);
|
||||
```
|
||||
|
||||
## Testing Shaders
|
||||
|
||||
### Vitest Tests
|
||||
|
||||
```tsx
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ShaderMaterial, Color } from 'three';
|
||||
|
||||
describe('Custom shader material', () => {
|
||||
it('creates material with correct uniforms', () => {
|
||||
const material = new ShaderMaterial({
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uColor: { value: new Color('#ff0000') },
|
||||
},
|
||||
vertexShader: 'void main() { gl_Position = vec4(0.0); }',
|
||||
fragmentShader: 'void main() { gl_FragColor = vec4(1.0); }',
|
||||
});
|
||||
expect(material.uniforms.uTime.value).toBe(0);
|
||||
expect(material.uniforms.uColor.value).toBeInstanceOf(Color);
|
||||
});
|
||||
|
||||
it('updates uniform values', () => {
|
||||
const uniforms = { uTime: { value: 0 } };
|
||||
uniforms.uTime.value = 1.5;
|
||||
expect(uniforms.uTime.value).toBe(1.5);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Storybook Stories
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { OrbitControls } from '@react-three/drei';
|
||||
import { WavePlane } from './wave-plane';
|
||||
|
||||
const meta: Meta<typeof WavePlane> = {
|
||||
title: 'R3F/Shaders/WavePlane',
|
||||
component: WavePlane,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Canvas camera={{ position: [5, 5, 5] }} style={{ height: 400 }}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<Story />
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
),
|
||||
],
|
||||
};
|
||||
export default meta;
|
||||
export const Default: StoryObj = {};
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
When writing shaders in R3F:
|
||||
|
||||
1. Wrap GLSL strings with `/* glsl */` template literal tag for IDE syntax highlighting
|
||||
2. Always `useMemo` for uniform objects to prevent material recreation
|
||||
3. Use `extend()` + `shaderMaterial` from drei for reusable shader elements
|
||||
4. Animate uniforms via `useFrame` and refs (not React state)
|
||||
5. Declare TypeScript JSX types when using `extend()`
|
||||
6. Use `@suaveplan/r3f-shaders` pre-built materials before writing custom GLSL
|
||||
7. Prefer `step`/`smoothstep`/`mix` over `if/else` in GLSL
|
||||
8. Pre-allocate all uniform objects (Color, Vector2, Vector3) in `useMemo`
|
||||
9. Use `onBeforeCompile` to patch built-in materials rather than rewriting from scratch
|
||||
10. Keep shader component files under 250 lines (split GLSL into separate strings or files)
|
||||
@@ -0,0 +1,487 @@
|
||||
---
|
||||
name: threejs-textures
|
||||
description: Three.js texture management adapted for React Three Fiber and Suaveplan R3F packages. Covers useTexture hook, texture configuration (wrapping, filtering, encoding), environment maps, HDR loading, render targets for dynamic textures, cubemaps, texture atlases, and @suaveplan/r3f texture utilities.
|
||||
---
|
||||
|
||||
This skill covers texture management in Three.js within the React Three Fiber (R3F) context used by the Suaveplan monorepo. All code targets **React 19 + TypeScript + R3F** and follows Suaveplan CLAUDE.md conventions.
|
||||
|
||||
The user needs to load, configure, or create textures for 3D materials: image textures, environment maps, render targets, procedural textures, or compressed texture formats.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Three.js Texture System
|
||||
|
||||
Textures in Three.js are GPU-side image data. Key classes:
|
||||
|
||||
- **Texture**: Base class for 2D textures from images
|
||||
- **CanvasTexture**: Texture from an HTML canvas element
|
||||
- **VideoTexture**: Texture from a video element
|
||||
- **CubeTexture**: 6-face cubemap for environment mapping
|
||||
- **DataTexture**: Texture from raw typed arrays
|
||||
- **WebGLRenderTarget**: Framebuffer for render-to-texture
|
||||
|
||||
### R3F Texture Approach
|
||||
|
||||
R3F provides hooks that integrate texture loading with React Suspense:
|
||||
|
||||
| Three.js (Imperative) | R3F (Declarative) |
|
||||
|---|---|
|
||||
| `new TextureLoader().load(url, cb)` | `useTexture(url)` |
|
||||
| `new CubeTextureLoader().load(urls, cb)` | `<Environment>` or `useCubeTexture` |
|
||||
| `new RGBELoader().load(url, cb)` | `useLoader(RGBELoader, url)` |
|
||||
| Manual texture.wrapS/wrapT | Configure after `useTexture` |
|
||||
| `renderer.setRenderTarget(rt)` | `useFBO()` or `createPortal()` |
|
||||
|
||||
### Suaveplan R3F Packages
|
||||
|
||||
| Package | Purpose | Import |
|
||||
|---|---|---|
|
||||
| `@suaveplan/r3f` | Core texture utilities, loading helpers | `import { useR3F } from '@suaveplan/r3f'` |
|
||||
| `@suaveplan/r3f-shaders` | Texture-based shader materials | `import { NoiseMaterial } from '@suaveplan/r3f-shaders'` |
|
||||
| `@suaveplan/r3f-performance` | Texture memory monitoring | `import { usePerformanceMonitor } from '@suaveplan/r3f-performance'` |
|
||||
|
||||
## useTexture: Loading Textures
|
||||
|
||||
The primary hook for loading image textures. Suspends the component until loaded.
|
||||
|
||||
### Single Texture
|
||||
|
||||
```tsx
|
||||
import { Suspense } from 'react';
|
||||
import { useTexture } from '@react-three/drei';
|
||||
|
||||
function TexturedBox() {
|
||||
const texture = useTexture('/textures/wood.jpg');
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial map={texture} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
|
||||
export function Scene() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<TexturedBox />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Textures (PBR Map Set)
|
||||
|
||||
```tsx
|
||||
import { useTexture } from '@react-three/drei';
|
||||
|
||||
function PBRSurface() {
|
||||
const textures = useTexture({
|
||||
map: '/textures/brick/color.jpg',
|
||||
normalMap: '/textures/brick/normal.jpg',
|
||||
roughnessMap: '/textures/brick/roughness.jpg',
|
||||
aoMap: '/textures/brick/ao.jpg',
|
||||
displacementMap: '/textures/brick/height.jpg',
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<planeGeometry args={[4, 4, 64, 64]} />
|
||||
<meshStandardMaterial
|
||||
{...textures}
|
||||
displacementScale={0.1}
|
||||
normalScale={[1, 1]}
|
||||
aoMapIntensity={1}
|
||||
/>
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Preloading Textures
|
||||
|
||||
```tsx
|
||||
import { useTexture } from '@react-three/drei';
|
||||
|
||||
// Preload at module level (starts loading immediately)
|
||||
useTexture.preload('/textures/wood.jpg');
|
||||
useTexture.preload({
|
||||
map: '/textures/brick/color.jpg',
|
||||
normalMap: '/textures/brick/normal.jpg',
|
||||
});
|
||||
```
|
||||
|
||||
## Texture Configuration
|
||||
|
||||
After loading, configure texture properties before use:
|
||||
|
||||
### Wrapping Modes
|
||||
|
||||
```tsx
|
||||
import { useTexture } from '@react-three/drei';
|
||||
import { RepeatWrapping, MirroredRepeatWrapping, ClampToEdgeWrapping } from 'three';
|
||||
|
||||
function TiledFloor() {
|
||||
const texture = useTexture('/textures/tile.jpg');
|
||||
|
||||
// Tile 4x4 across the surface
|
||||
texture.wrapS = texture.wrapT = RepeatWrapping;
|
||||
texture.repeat.set(4, 4);
|
||||
|
||||
return (
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<planeGeometry args={[10, 10]} />
|
||||
<meshStandardMaterial map={texture} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
| Wrapping Mode | Constant | Behavior |
|
||||
|---|---|---|
|
||||
| `ClampToEdgeWrapping` | Default | Edge pixels stretch |
|
||||
| `RepeatWrapping` | Tiling | Texture repeats |
|
||||
| `MirroredRepeatWrapping` | Mirrored tiling | Alternating mirror |
|
||||
|
||||
### Filtering Modes
|
||||
|
||||
```tsx
|
||||
import { NearestFilter, LinearFilter, LinearMipmapLinearFilter } from 'three';
|
||||
|
||||
// Pixel-art style (no blurring)
|
||||
texture.magFilter = NearestFilter;
|
||||
texture.minFilter = NearestFilter;
|
||||
texture.generateMipmaps = false;
|
||||
|
||||
// Smooth (default for photos/PBR)
|
||||
texture.magFilter = LinearFilter;
|
||||
texture.minFilter = LinearMipmapLinearFilter;
|
||||
|
||||
// Anisotropic filtering (reduces blur at oblique angles)
|
||||
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
||||
```
|
||||
|
||||
| Filter | Minification | Magnification | Use Case |
|
||||
|---|---|---|---|
|
||||
| `NearestFilter` | Pixelated | Pixelated | Pixel art, data textures |
|
||||
| `LinearFilter` | Smooth | Smooth | Photos, PBR maps |
|
||||
| `LinearMipmapLinearFilter` | Trilinear | N/A | Default min filter |
|
||||
|
||||
### Color Space
|
||||
|
||||
```tsx
|
||||
import { SRGBColorSpace, LinearSRGBColorSpace, NoColorSpace } from 'three';
|
||||
|
||||
// Color/albedo textures: sRGB (default in R3F)
|
||||
texture.colorSpace = SRGBColorSpace;
|
||||
|
||||
// Data textures (normal, roughness, metalness, AO): Linear
|
||||
normalMap.colorSpace = LinearSRGBColorSpace;
|
||||
roughnessMap.colorSpace = LinearSRGBColorSpace;
|
||||
|
||||
// Raw data (displacement, custom): No conversion
|
||||
dataTexture.colorSpace = NoColorSpace;
|
||||
```
|
||||
|
||||
### Texture Configuration Helper
|
||||
|
||||
```tsx
|
||||
import { useTexture } from '@react-three/drei';
|
||||
import { RepeatWrapping, SRGBColorSpace, LinearSRGBColorSpace } from 'three';
|
||||
import type { Texture } from 'three';
|
||||
|
||||
function configureTexture(texture: Texture, options: {
|
||||
repeat?: [number, number];
|
||||
colorSpace?: string;
|
||||
anisotropy?: number;
|
||||
}) {
|
||||
if (options.repeat) {
|
||||
texture.wrapS = texture.wrapT = RepeatWrapping;
|
||||
texture.repeat.set(...options.repeat);
|
||||
}
|
||||
if (options.colorSpace) {
|
||||
texture.colorSpace = options.colorSpace;
|
||||
}
|
||||
if (options.anisotropy) {
|
||||
texture.anisotropy = options.anisotropy;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
function ConfiguredSurface() {
|
||||
const textures = useTexture({
|
||||
map: '/textures/ground/color.jpg',
|
||||
normalMap: '/textures/ground/normal.jpg',
|
||||
});
|
||||
|
||||
configureTexture(textures.map, { repeat: [4, 4], colorSpace: SRGBColorSpace });
|
||||
configureTexture(textures.normalMap, { repeat: [4, 4], colorSpace: LinearSRGBColorSpace });
|
||||
|
||||
return (
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<planeGeometry args={[20, 20]} />
|
||||
<meshStandardMaterial {...textures} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Maps
|
||||
|
||||
### HDR Environment Maps
|
||||
|
||||
```tsx
|
||||
import { useLoader } from '@react-three/fiber';
|
||||
import { RGBELoader } from 'three-stdlib';
|
||||
import { EquirectangularReflectionMapping } from 'three';
|
||||
|
||||
function HDREnvironment({ url }: { url: string }) {
|
||||
const hdr = useLoader(RGBELoader, url);
|
||||
hdr.mapping = EquirectangularReflectionMapping;
|
||||
|
||||
return (
|
||||
<>
|
||||
<primitive object={hdr} attach="environment" />
|
||||
<primitive object={hdr} attach="background" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Drei Environment Component
|
||||
|
||||
The simplest way to add environment maps:
|
||||
|
||||
```tsx
|
||||
import { Environment } from '@react-three/drei';
|
||||
|
||||
// Preset HDR
|
||||
<Environment preset="sunset" background />
|
||||
|
||||
// Custom HDR file
|
||||
<Environment files="/textures/env.hdr" background />
|
||||
|
||||
// EXR file
|
||||
<Environment files="/textures/env.exr" background />
|
||||
|
||||
// Multiple face cubemap
|
||||
<Environment
|
||||
files={[
|
||||
'/cubemap/px.jpg', '/cubemap/nx.jpg',
|
||||
'/cubemap/py.jpg', '/cubemap/ny.jpg',
|
||||
'/cubemap/pz.jpg', '/cubemap/nz.jpg',
|
||||
]}
|
||||
/>
|
||||
|
||||
// Blurred background
|
||||
<Environment files="/textures/env.hdr" background blur={0.5} />
|
||||
```
|
||||
|
||||
### Lightformer Environments
|
||||
|
||||
Create procedural environment maps with `<Lightformer>` inside `<Environment>`. Use `form="rect"` or `form="ring"` with position, intensity, scale, and color props to simulate key, fill, and rim lights.
|
||||
|
||||
## Render Targets (Dynamic Textures)
|
||||
|
||||
### useFBO: Render to Texture
|
||||
|
||||
```tsx
|
||||
import { useRef } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { useFBO } from '@react-three/drei';
|
||||
import type { Mesh } from 'three';
|
||||
|
||||
export function MirrorSurface() {
|
||||
const meshRef = useRef<Mesh>(null);
|
||||
const renderTarget = useFBO(512, 512);
|
||||
|
||||
useFrame(({ gl, scene, camera }) => {
|
||||
// Hide mirror during render-to-texture
|
||||
if (meshRef.current) meshRef.current.visible = false;
|
||||
|
||||
gl.setRenderTarget(renderTarget);
|
||||
gl.render(scene, camera);
|
||||
gl.setRenderTarget(null);
|
||||
|
||||
if (meshRef.current) meshRef.current.visible = true;
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh ref={meshRef}>
|
||||
<planeGeometry args={[4, 3]} />
|
||||
<meshBasicMaterial map={renderTarget.texture} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Portal Rendering
|
||||
|
||||
Use `createPortal()` from R3F to render a separate `Scene` into a `useFBO()` render target. In `useFrame`, set the render target, render the portal scene with a dedicated camera, then reset to null.
|
||||
|
||||
## Canvas, Video, and Data Textures
|
||||
|
||||
- **CanvasTexture**: Create via `new CanvasTexture(canvas)` with a drawn 2D canvas. Wrap in `useMemo`.
|
||||
- **VideoTexture**: Use `useVideoTexture` from drei for video-mapped surfaces.
|
||||
- **DataTexture**: Create from `Float32Array` for noise, heightmaps, lookup tables. Set `needsUpdate = true`.
|
||||
|
||||
## Texture Atlas
|
||||
|
||||
Pack multiple images into a single texture and use UV offsets:
|
||||
|
||||
```tsx
|
||||
import { useTexture } from '@react-three/drei';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface AtlasFrame {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
function useTextureAtlas(url: string, frame: AtlasFrame, atlasSize: [number, number]) {
|
||||
const texture = useTexture(url);
|
||||
|
||||
return useMemo(() => {
|
||||
const clone = texture.clone();
|
||||
clone.repeat.set(frame.width / atlasSize[0], frame.height / atlasSize[1]);
|
||||
clone.offset.set(frame.x / atlasSize[0], 1 - (frame.y + frame.height) / atlasSize[1]);
|
||||
clone.needsUpdate = true;
|
||||
return clone;
|
||||
}, [texture, frame, atlasSize]);
|
||||
}
|
||||
|
||||
// Usage: select a 64x64 region from a 512x512 atlas
|
||||
function AtlasSprite() {
|
||||
const tex = useTextureAtlas(
|
||||
'/textures/sprites.png',
|
||||
{ x: 128, y: 0, width: 64, height: 64 },
|
||||
[512, 512]
|
||||
);
|
||||
|
||||
return (
|
||||
<sprite scale={[1, 1, 1]}>
|
||||
<spriteMaterial map={tex} />
|
||||
</sprite>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Texture Memory Management
|
||||
|
||||
### GPU Memory Estimation
|
||||
|
||||
| Dimensions | Format | Mipmaps | VRAM |
|
||||
|---|---|---|---|
|
||||
| 1024x1024 | RGBA8 | Yes | ~5.3 MB |
|
||||
| 2048x2048 | RGBA8 | Yes | ~21.3 MB |
|
||||
| 4096x4096 | RGBA8 | Yes | ~85.3 MB |
|
||||
| 1024x1024 | KTX2 (BC7) | Yes | ~1.3 MB |
|
||||
| 2048x2048 | KTX2 (BC7) | Yes | ~5.3 MB |
|
||||
|
||||
### Optimization Guidelines
|
||||
|
||||
1. **Use power-of-two dimensions**: 256, 512, 1024, 2048, 4096 (required for mipmaps)
|
||||
2. **Use KTX2 compression**: 4x-8x VRAM savings over raw RGBA
|
||||
3. **Reduce resolution**: Most roughness/AO maps work fine at 512x512
|
||||
4. **Share textures**: Use `texture.clone()` for UV variations on the same image
|
||||
5. **Generate mipmaps**: Set `texture.generateMipmaps = true` (default) for filtered rendering
|
||||
6. **Dispose unused textures**: `texture.dispose()` frees GPU memory
|
||||
|
||||
```tsx
|
||||
import { usePerformanceMonitor } from '@suaveplan/r3f-performance';
|
||||
|
||||
function useAdaptiveTextureResolution() {
|
||||
const [resolution, setResolution] = useState<'high' | 'low'>('high');
|
||||
|
||||
usePerformanceMonitor({
|
||||
onDecline: () => setResolution('low'),
|
||||
onIncline: () => setResolution('high'),
|
||||
});
|
||||
|
||||
return resolution === 'high' ? '2k' : '1k';
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Textures
|
||||
|
||||
### Vitest Tests
|
||||
|
||||
```tsx
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
DataTexture,
|
||||
FloatType,
|
||||
RedFormat,
|
||||
RepeatWrapping,
|
||||
CanvasTexture,
|
||||
} from 'three';
|
||||
|
||||
describe('Texture utilities', () => {
|
||||
it('creates data texture with correct dimensions', () => {
|
||||
const size = 64;
|
||||
const data = new Float32Array(size * size);
|
||||
const tex = new DataTexture(data, size, size, RedFormat, FloatType);
|
||||
expect(tex.image.width).toBe(size);
|
||||
expect(tex.image.height).toBe(size);
|
||||
});
|
||||
|
||||
it('configures repeat wrapping', () => {
|
||||
const data = new Float32Array(16);
|
||||
const tex = new DataTexture(data, 4, 4, RedFormat, FloatType);
|
||||
tex.wrapS = tex.wrapT = RepeatWrapping;
|
||||
tex.repeat.set(4, 4);
|
||||
expect(tex.wrapS).toBe(RepeatWrapping);
|
||||
expect(tex.repeat.x).toBe(4);
|
||||
});
|
||||
|
||||
it('creates canvas texture', () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 256;
|
||||
canvas.height = 256;
|
||||
const tex = new CanvasTexture(canvas);
|
||||
expect(tex.image).toBe(canvas);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Storybook Stories
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Suspense } from 'react';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import { OrbitControls, Environment } from '@react-three/drei';
|
||||
|
||||
function TextureDemo() {
|
||||
return (
|
||||
<Canvas camera={{ position: [3, 3, 3] }} style={{ height: 400 }}>
|
||||
<Environment preset="studio" />
|
||||
<Suspense fallback={null}>
|
||||
{/* Textured mesh component */}
|
||||
</Suspense>
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
|
||||
const meta: Meta = { title: 'R3F/Textures/PBRSurface', component: TextureDemo };
|
||||
export default meta;
|
||||
export const Default: StoryObj = {};
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
When working with textures in R3F:
|
||||
|
||||
1. Use `useTexture` from drei for all image texture loading
|
||||
2. Wrap textured components in `<Suspense>` for async loading
|
||||
3. Set `colorSpace = SRGBColorSpace` for color/albedo maps only
|
||||
4. Set `colorSpace = LinearSRGBColorSpace` for normal, roughness, metalness, AO maps
|
||||
5. Use `RepeatWrapping` and `texture.repeat.set()` for tiled surfaces
|
||||
6. Preload textures with `useTexture.preload()` at module level
|
||||
7. Use KTX2 compression for production (4-8x VRAM savings)
|
||||
8. Use power-of-two dimensions for mipmap generation
|
||||
9. Dispose textures when no longer needed (`texture.dispose()`)
|
||||
10. Keep texture-related component files under 250 lines
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-web-artifacts",
|
||||
"version": "1.0.0",
|
||||
"description": "Build production-grade React artifacts using Suaveplan component library"
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
---
|
||||
name: web-artifacts-builder
|
||||
description: Build production-ready React 19 + TypeScript + Tailwind CSS components and pages using Suaveplan's UI library, Vite bundling, and shadcn/ui primitives
|
||||
---
|
||||
|
||||
# Web Artifacts Builder for Suaveplan
|
||||
|
||||
Build React 19 components, pages, and interactive artifacts within the Suaveplan monorepo. All artifacts use TypeScript, Tailwind CSS, shadcn/ui from `@suaveplan/react-ui`, and Vite for bundling. No external UI libraries when Suaveplan equivalents exist.
|
||||
|
||||
## Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Framework | React 19 (Server Components, Actions, useActionState) |
|
||||
| Language | TypeScript 5.x (strict mode) |
|
||||
| Styling | Tailwind CSS with `cn()` from `@suaveplan/react-ui` |
|
||||
| UI Primitives | shadcn/ui via `@suaveplan/react-ui` |
|
||||
| Bundler | Vite (NOT Parcel, NOT webpack) |
|
||||
| Testing | Vitest (unit) + Playwright (E2E) |
|
||||
| Linting | Biome (NOT ESLint) |
|
||||
| Stories | Storybook 8 |
|
||||
| Package Manager | Bun with workspace protocol |
|
||||
|
||||
## Available Suaveplan Packages
|
||||
|
||||
Always use these instead of external alternatives:
|
||||
|
||||
| Need | Suaveplan Package | Do NOT Use |
|
||||
|------|------------------|------------|
|
||||
| Buttons, Cards, Dialogs, Inputs | `@suaveplan/react-ui` | Material UI, Chakra UI, Ant Design |
|
||||
| Forms with validation | `@suaveplan/react-forms` | React Hook Form (standalone) |
|
||||
| Data grids/tables | `@suaveplan/react-data-table` | AG Grid, TanStack Table (standalone) |
|
||||
| Notifications/toasts | `@suaveplan/react-notifications` | react-hot-toast, sonner |
|
||||
| Timeline/Gantt views | `@suaveplan/react-timeline` | vis-timeline |
|
||||
| Authentication UI | `@suaveplan/react-auth` | custom auth forms |
|
||||
| Media players | `@suaveplan/react-media` | react-player |
|
||||
| Internationalization | `@suaveplan/react-translate` | react-i18next (standalone) |
|
||||
| CSS class merging | `cn()` from `@suaveplan/react-ui` | clsx, classnames (standalone) |
|
||||
| Charts/visualization | `@suaveplan/react-charts` | recharts (standalone) |
|
||||
| Drive/file sharing UI | `@suaveplan/drive-react-sharing` | custom file dialogs |
|
||||
| Virtualized lists | `@suaveplan/react-virtual` | react-window, react-virtuoso |
|
||||
|
||||
## Component File Structure
|
||||
|
||||
Every component follows this five-file pattern inside its directory:
|
||||
|
||||
```text
|
||||
src/components/<component-name>/
|
||||
<component-name>.tsx # Implementation (max 250 lines)
|
||||
<component-name>.test.tsx # Unit tests (max 500 lines, 95%+ coverage)
|
||||
<component-name>.stories.tsx # Storybook stories
|
||||
<component-name>.types.ts # TypeScript interfaces and types
|
||||
<component-name>.md # Documentation (200+ words, simple + advanced examples)
|
||||
```
|
||||
|
||||
### Implementation File Pattern
|
||||
|
||||
```typescript
|
||||
// src/components/status-badge/status-badge.tsx
|
||||
import * as React from 'react';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { cn } from '@suaveplan/react-ui';
|
||||
import type { StatusBadgeProps } from './status-badge.types';
|
||||
|
||||
const statusBadgeVariants = cva(
|
||||
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors',
|
||||
{
|
||||
variants: {
|
||||
status: {
|
||||
active: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||
pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||||
inactive: 'bg-muted text-muted-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: { status: 'active' },
|
||||
}
|
||||
);
|
||||
|
||||
export const StatusBadge = React.forwardRef<HTMLSpanElement, StatusBadgeProps>(
|
||||
({ className, status, label, ...props }, ref) => {
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
data-testid="sp-status-badge"
|
||||
role="status"
|
||||
aria-label={`Status: ${label ?? status}`}
|
||||
className={cn(statusBadgeVariants({ status }), className)}
|
||||
{...props}
|
||||
>
|
||||
{label ?? status}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
StatusBadge.displayName = 'StatusBadge';
|
||||
```
|
||||
|
||||
### Types File Pattern
|
||||
|
||||
```typescript
|
||||
// src/components/status-badge/status-badge.types.ts
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import type { statusBadgeVariants } from './status-badge';
|
||||
|
||||
export interface StatusBadgeProps
|
||||
extends React.HTMLAttributes<HTMLSpanElement>,
|
||||
VariantProps<typeof statusBadgeVariants> {
|
||||
/** Display label (defaults to status name) */
|
||||
label?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Stories File Pattern
|
||||
|
||||
```typescript
|
||||
// src/components/status-badge/status-badge.stories.tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { StatusBadge } from './status-badge';
|
||||
|
||||
const meta: Meta<typeof StatusBadge> = {
|
||||
title: 'Components/StatusBadge',
|
||||
component: StatusBadge,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
status: {
|
||||
control: 'select',
|
||||
options: ['active', 'pending', 'inactive'],
|
||||
},
|
||||
},
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof StatusBadge>;
|
||||
|
||||
export const Active: Story = { args: { status: 'active' } };
|
||||
export const Pending: Story = { args: { status: 'pending', label: 'Awaiting Review' } };
|
||||
export const Inactive: Story = { args: { status: 'inactive' } };
|
||||
|
||||
export const AllStatuses: Story = {
|
||||
render: () => (
|
||||
<div className="flex gap-2">
|
||||
<StatusBadge status="active" />
|
||||
<StatusBadge status="pending" />
|
||||
<StatusBadge status="inactive" />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
```
|
||||
|
||||
### Test File Pattern
|
||||
|
||||
```typescript
|
||||
// src/components/status-badge/status-badge.test.tsx
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { StatusBadge } from './status-badge';
|
||||
|
||||
describe('StatusBadge', () => {
|
||||
it('renders with active status', () => {
|
||||
render(<StatusBadge status="active" />);
|
||||
expect(screen.getByTestId('sp-status-badge')).toBeInTheDocument();
|
||||
expect(screen.getByRole('status')).toHaveTextContent('active');
|
||||
});
|
||||
|
||||
it('uses custom label when provided', () => {
|
||||
render(<StatusBadge status="pending" label="In Review" />);
|
||||
expect(screen.getByRole('status')).toHaveTextContent('In Review');
|
||||
});
|
||||
|
||||
it('applies custom className via cn()', () => {
|
||||
render(<StatusBadge status="active" className="ml-4" />);
|
||||
expect(screen.getByTestId('sp-status-badge')).toHaveClass('ml-4');
|
||||
});
|
||||
|
||||
it('forwards ref', () => {
|
||||
const ref = { current: null as HTMLSpanElement | null };
|
||||
render(<StatusBadge ref={ref} status="active" />);
|
||||
expect(ref.current).toBeInstanceOf(HTMLSpanElement);
|
||||
});
|
||||
|
||||
it('has accessible aria-label', () => {
|
||||
render(<StatusBadge status="active" />);
|
||||
expect(screen.getByRole('status')).toHaveAttribute('aria-label', 'Status: active');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Documentation File Pattern
|
||||
|
||||
Follow `.claude/templates/module-docs.md` template with these requirements:
|
||||
|
||||
- Minimum 200 words
|
||||
- Include a "Basic Example" section (simple import + render)
|
||||
- Include an "Advanced Example" section (composition, custom styling, controlled state)
|
||||
- Document all props in an API Reference section
|
||||
- Explain design decisions
|
||||
|
||||
```markdown
|
||||
# StatusBadge
|
||||
|
||||
## Purpose
|
||||
A visual indicator component that displays the current status of an entity...
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Example
|
||||
\`\`\`typescript
|
||||
import { StatusBadge } from '@suaveplan/react-<package>';
|
||||
<StatusBadge status="active" />
|
||||
\`\`\`
|
||||
|
||||
### Advanced Example
|
||||
\`\`\`typescript
|
||||
import { StatusBadge } from '@suaveplan/react-<package>';
|
||||
import { cn } from '@suaveplan/react-ui';
|
||||
// Composed with conditional logic and custom styling...
|
||||
\`\`\`
|
||||
|
||||
## API Reference
|
||||
...
|
||||
```
|
||||
|
||||
## Styling Rules
|
||||
|
||||
### Tailwind CSS + cn() Utility
|
||||
|
||||
Always use `cn()` from `@suaveplan/react-ui` for conditional class merging:
|
||||
|
||||
```typescript
|
||||
import { cn } from '@suaveplan/react-ui';
|
||||
|
||||
// Correct: cn() for merging
|
||||
<div className={cn('flex items-center', isActive && 'bg-primary', className)} />
|
||||
|
||||
// Wrong: template literals for conditional classes
|
||||
<div className={`flex items-center ${isActive ? 'bg-primary' : ''}`} />
|
||||
```
|
||||
|
||||
### Dark Mode
|
||||
|
||||
Use Tailwind's `dark:` variant. Shadcn/ui CSS variables automatically adapt:
|
||||
|
||||
```typescript
|
||||
<div className="bg-background text-foreground dark:bg-background dark:text-foreground">
|
||||
<p className="text-muted-foreground">Uses CSS variables, works in both modes</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Shadcn/UI CSS Variables
|
||||
|
||||
Reference these semantic tokens instead of raw Tailwind colors:
|
||||
|
||||
- `background` / `foreground` -- page background and default text
|
||||
- `primary` / `primary-foreground` -- primary actions
|
||||
- `secondary` / `secondary-foreground` -- secondary actions
|
||||
- `muted` / `muted-foreground` -- subdued UI elements
|
||||
- `accent` / `accent-foreground` -- highlights and hover states
|
||||
- `destructive` / `destructive-foreground` -- errors and deletions
|
||||
- `border` -- default border color
|
||||
- `ring` -- focus ring color
|
||||
- `card` / `card-foreground` -- card surfaces
|
||||
|
||||
## Build Configuration
|
||||
|
||||
Suaveplan browser packages use Vite for building:
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import dts from 'vite-plugin-dts';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), dts({ rollupTypes: true })],
|
||||
build: {
|
||||
lib: {
|
||||
entry: 'src/index.ts',
|
||||
formats: ['es'],
|
||||
fileName: 'index',
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['react', 'react-dom', 'react/jsx-runtime'],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Package.json Requirements
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@suaveplan/react-<name>",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@suaveplan/react-ui": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Max 250 lines** per implementation file -- split into sub-components if larger
|
||||
- **Max 500 lines** per test file -- split by feature area
|
||||
- **95% minimum test coverage** -- verified via `bun run test:coverage`
|
||||
- **No external UI libraries** when a Suaveplan package covers the need
|
||||
- **No Parcel** -- Vite is the bundler for all browser packages
|
||||
- **No tsc compilation** -- Vite handles transforms, TypeScript is for type-checking only
|
||||
- **No mocks** -- test with real implementations
|
||||
- **Biome linting** -- not ESLint
|
||||
- **React 19** -- use modern patterns (useActionState, useOptimistic, Server Components where applicable)
|
||||
- **Accessibility** -- ARIA labels, keyboard navigation, focus management on all interactive elements
|
||||
- **`data-testid`** attributes must use `sp-` prefix on all Suaveplan components
|
||||
- **`forwardRef`** -- all leaf components must forward refs
|
||||
- **Documentation** -- every `.tsx` implementation file must have a matching `.md` file
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-webapp-testing",
|
||||
"version": "1.0.0",
|
||||
"description": "Playwright E2E testing toolkit for Suaveplan browser packages"
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
---
|
||||
name: webapp-testing
|
||||
description: End-to-end and visual regression testing for Suaveplan React 19 + TypeScript + Tailwind CSS web applications using Playwright and Storybook
|
||||
---
|
||||
|
||||
# Webapp Testing for Suaveplan
|
||||
|
||||
Write and maintain Playwright E2E tests and Storybook visual regression tests for Suaveplan browser packages. All tests are TypeScript `.spec.ts` files executed natively through Playwright -- no Python scripts, no wrapper layers.
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Runtime**: Bun + Turborepo monorepo
|
||||
- **E2E Framework**: Playwright (TypeScript) with `.spec.ts` files in `e2e/` directories
|
||||
- **Unit/Integration**: Vitest with `.test.ts` / `.test.tsx` files co-located with source
|
||||
- **Visual Testing**: Storybook stories serve as E2E test fixtures via `playwright.config.ts` webServer
|
||||
- **UI Library**: `@suaveplan/react-ui` (shadcn/ui-based) with `cn()` utility for Tailwind class merging
|
||||
- **CSS Prefix**: All Suaveplan components use `sp-` prefix in `data-testid` attributes
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run E2E tests for a specific package
|
||||
bun run e2e --filter=@suaveplan/<package-name>
|
||||
|
||||
# Run unit tests
|
||||
bun run test --filter=@suaveplan/<package-name>
|
||||
|
||||
# Run all E2E tests across monorepo
|
||||
bunx turbo e2e
|
||||
|
||||
# Run with coverage (95% minimum required)
|
||||
bun run test:coverage --filter=@suaveplan/<package-name>
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
Every testable browser package follows this layout:
|
||||
|
||||
```text
|
||||
packages/browser/<package-name>/
|
||||
playwright.config.ts # Playwright configuration
|
||||
e2e/
|
||||
<feature>.spec.ts # E2E test files
|
||||
accessibility.spec.ts # Dedicated a11y E2E tests
|
||||
.storybook/
|
||||
main.ts # Storybook configuration
|
||||
preview.ts # Global decorators, Tailwind CSS import
|
||||
src/
|
||||
components/
|
||||
<component>/
|
||||
<component>.tsx # Implementation
|
||||
<component>.test.tsx # Unit tests (Vitest)
|
||||
<component>.stories.tsx # Storybook stories (E2E fixtures)
|
||||
<component>.types.ts # TypeScript types
|
||||
<component>.md # Documentation
|
||||
```
|
||||
|
||||
## Playwright Configuration Pattern
|
||||
|
||||
Each package has its own `playwright.config.ts` that launches Storybook as the web server:
|
||||
|
||||
```typescript
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
testMatch: '**/*.spec.ts',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: [['html'], ['json', { outputFile: 'test-results.json' }]],
|
||||
use: {
|
||||
baseURL: 'http://localhost:6030',
|
||||
viewport: { width: 1280, height: 720 },
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
||||
webServer: {
|
||||
command: 'bun run storybook',
|
||||
port: 6030,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 300 * 1000,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## E2E Test Patterns
|
||||
|
||||
### Navigating to Storybook Stories
|
||||
|
||||
All E2E tests use Storybook iframe URLs as their entry point:
|
||||
|
||||
```typescript
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
const storyUrl = (story: string) =>
|
||||
`/iframe.html?viewMode=story&id=components-<component>--${story}`;
|
||||
|
||||
test.describe('<Component>: Default story', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(storyUrl('default'));
|
||||
await page.waitForSelector('[data-testid="sp-<component>-container"]');
|
||||
});
|
||||
|
||||
test('renders the component', async ({ page }) => {
|
||||
const container = page.getByTestId('sp-<component>-container');
|
||||
await expect(container).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Selector Strategy (Priority Order)
|
||||
|
||||
1. **`data-testid` with `sp-` prefix** -- Primary selector for Suaveplan components
|
||||
2. **ARIA roles and labels** -- For accessible elements
|
||||
3. **Text content** -- For user-visible labels
|
||||
4. **CSS class selectors** -- Last resort only, prefix with `sp-`
|
||||
|
||||
```typescript
|
||||
// Preferred: data-testid with sp- prefix
|
||||
page.getByTestId('sp-timeline-container');
|
||||
page.getByTestId('sp-event-bar');
|
||||
page.getByTestId('sp-data-table-row');
|
||||
|
||||
// Good: ARIA roles
|
||||
page.getByRole('button', { name: 'Submit' });
|
||||
page.getByRole('dialog', { name: 'Confirm deletion' });
|
||||
page.getByRole('application', { name: 'Timeline' });
|
||||
|
||||
// Acceptable: text content
|
||||
page.getByText('No results found');
|
||||
|
||||
// Avoid unless necessary: CSS classes
|
||||
page.locator('.sp-card-header');
|
||||
```
|
||||
|
||||
### Testing shadcn/ui Components
|
||||
|
||||
shadcn/ui components from `@suaveplan/react-ui` use Radix UI primitives. Test their behavior through ARIA roles:
|
||||
|
||||
```typescript
|
||||
// Dialog (from @suaveplan/react-ui)
|
||||
test('opens dialog on trigger click', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Open dialog' }).click();
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog).toHaveAttribute('aria-modal', 'true');
|
||||
});
|
||||
|
||||
// Select (Radix-based)
|
||||
test('selects an option from dropdown', async ({ page }) => {
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Option A' }).click();
|
||||
await expect(page.getByRole('combobox')).toHaveText('Option A');
|
||||
});
|
||||
|
||||
// Toast notifications (@suaveplan/react-notifications)
|
||||
test('shows success toast after save', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
const toast = page.getByTestId('sp-toast');
|
||||
await expect(toast).toBeVisible();
|
||||
await expect(toast).toContainText('Saved successfully');
|
||||
});
|
||||
|
||||
// Data table (@suaveplan/react-data-table)
|
||||
test('sorts table by column header', async ({ page }) => {
|
||||
await page.getByRole('columnheader', { name: 'Name' }).click();
|
||||
const firstRow = page.getByTestId('sp-data-table-row').first();
|
||||
await expect(firstRow).toContainText('Alpha');
|
||||
});
|
||||
```
|
||||
|
||||
### Accessibility Testing Pattern
|
||||
|
||||
Every package should have a dedicated `accessibility.spec.ts`:
|
||||
|
||||
```typescript
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
const storyUrl = (story: string) =>
|
||||
`/iframe.html?viewMode=story&id=components-<component>--${story}`;
|
||||
|
||||
test.describe('Accessibility', () => {
|
||||
test('all interactive elements are keyboard-focusable', async ({ page }) => {
|
||||
await page.goto(storyUrl('default'));
|
||||
await page.waitForSelector('[data-testid="sp-<component>-container"]');
|
||||
|
||||
// Tab through interactive elements
|
||||
await page.keyboard.press('Tab');
|
||||
const focused = page.locator(':focus');
|
||||
await expect(focused).toHaveAttribute('tabindex', '0');
|
||||
});
|
||||
|
||||
test('ARIA labels are present on all semantic elements', async ({ page }) => {
|
||||
await page.goto(storyUrl('default'));
|
||||
const container = page.getByTestId('sp-<component>-container');
|
||||
await expect(container).toHaveAttribute('aria-label');
|
||||
});
|
||||
|
||||
test('focus is trapped inside modal dialogs', async ({ page }) => {
|
||||
await page.goto(storyUrl('with-dialog'));
|
||||
await page.getByRole('button', { name: 'Open' }).click();
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog).toBeFocused();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Visual Regression Pattern
|
||||
|
||||
Use Playwright screenshot comparison against Storybook stories:
|
||||
|
||||
```typescript
|
||||
test('matches visual snapshot', async ({ page }) => {
|
||||
await page.goto(storyUrl('default'));
|
||||
await page.waitForSelector('[data-testid="sp-<component>-container"]');
|
||||
// Wait for animations to settle
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page).toHaveScreenshot('<component>-default.png', {
|
||||
maxDiffPixelRatio: 0.01,
|
||||
});
|
||||
});
|
||||
|
||||
test('matches dark mode snapshot', async ({ page }) => {
|
||||
await page.goto(storyUrl('dark-mode'));
|
||||
await page.waitForSelector('[data-testid="sp-<component>-container"]');
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page).toHaveScreenshot('<component>-dark.png', {
|
||||
maxDiffPixelRatio: 0.01,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Form Testing Pattern
|
||||
|
||||
For `@suaveplan/react-forms` components:
|
||||
|
||||
```typescript
|
||||
test('validates required fields on submit', async ({ page }) => {
|
||||
await page.goto(storyUrl('with-validation'));
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// Check validation messages appear
|
||||
await expect(page.getByText('Name is required')).toBeVisible();
|
||||
await expect(page.getByText('Email is required')).toBeVisible();
|
||||
});
|
||||
|
||||
test('submits form with valid data', async ({ page }) => {
|
||||
await page.goto(storyUrl('with-validation'));
|
||||
await page.getByLabel('Name').fill('Jane Doe');
|
||||
await page.getByLabel('Email').fill('jane@example.com');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// Verify success state
|
||||
await expect(page.getByTestId('sp-form-success')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Max 500 lines per test file** -- split large test suites into multiple `.spec.ts` files by feature
|
||||
- **Max 250 lines per implementation file** -- components exceeding this need refactoring
|
||||
- **95% minimum test coverage** -- enforced across unit + E2E combined
|
||||
- **No mocks** -- all tests use real implementations and real Storybook stories
|
||||
- **TypeScript only** -- no `.js` or `.mjs` test files
|
||||
- **Biome linting** -- all test files must pass `bun run lint`
|
||||
- Every component must have `data-testid` attributes with `sp-` prefix
|
||||
- Every `.spec.ts` file must have a corresponding `.stories.tsx` fixture
|
||||
- Accessibility tests are mandatory for interactive components
|
||||
|
||||
## Packages with E2E Tests
|
||||
|
||||
The following packages have Playwright configurations and should always maintain E2E coverage:
|
||||
|
||||
| Package | Storybook Port | Test Directory |
|
||||
|---------|---------------|----------------|
|
||||
| `@suaveplan/react-ui` | 6006 | `e2e/` |
|
||||
| `@suaveplan/react-forms` | 6007 | `e2e/` |
|
||||
| `@suaveplan/react-data-table` | 6030 | `e2e/` |
|
||||
| `@suaveplan/react-timeline` | 6030 | `e2e/` |
|
||||
| `@suaveplan/react-notifications` | 6030 | `e2e/` |
|
||||
| `@suaveplan/react-charts` | 6030 | `e2e/` |
|
||||
| `@suaveplan/drive-react-sharing` | 6030 | `e2e/` |
|
||||
| `@suaveplan/react-virtual` | 6030 | `e2e/` |
|
||||
| `@suaveplan/react-auth` | 6030 | `e2e/` |
|
||||
| `@suaveplan/react-media` | 6030 | `e2e/` |
|
||||
|
||||
## Checklist Before Completing Tests
|
||||
|
||||
1. All `.spec.ts` files pass: `bun run e2e --filter=@suaveplan/<package>`
|
||||
2. All `.test.tsx` files pass: `bun run test --filter=@suaveplan/<package>`
|
||||
3. Coverage at 95%+: `bun run test:coverage --filter=@suaveplan/<package>`
|
||||
4. Biome lint clean: `bun run lint --filter=@suaveplan/<package>`
|
||||
5. TypeScript types pass: `bun run typecheck --filter=@suaveplan/<package>`
|
||||
6. Accessibility spec exists for interactive components
|
||||
7. No `vi.mock()` or `jest.mock()` calls
|
||||
8. All `data-testid` attributes use `sp-` prefix
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-webgpu",
|
||||
"version": "1.0.0",
|
||||
"description": "WebGPU rendering with Three.js TSL in React Three Fiber"
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
# TSL API Reference -- Cheatsheet for R3F
|
||||
|
||||
Complete reference for Three Shading Language nodes, operators, and patterns.
|
||||
All examples assume a React Three Fiber context with WebGPU or WebGL backend.
|
||||
|
||||
## Imports
|
||||
|
||||
```ts
|
||||
// Node materials
|
||||
import {
|
||||
MeshStandardNodeMaterial,
|
||||
MeshPhysicalNodeMaterial,
|
||||
MeshBasicNodeMaterial,
|
||||
SpriteNodeMaterial,
|
||||
LineBasicNodeMaterial,
|
||||
PointsNodeMaterial,
|
||||
} from 'three/webgpu';
|
||||
|
||||
// TSL core
|
||||
import {
|
||||
// Types & constructors
|
||||
float, int, uint, bool, vec2, vec3, vec4, mat2, mat3, mat4, color,
|
||||
// Uniforms & attributes
|
||||
uniform, attribute, varying, varyingProperty,
|
||||
// Math
|
||||
sin, cos, tan, asin, acos, atan, atan2,
|
||||
abs, sign, floor, ceil, fract, round, trunc,
|
||||
pow, exp, exp2, log, log2, sqrt, inverseSqrt,
|
||||
min, max, clamp, mix, step, smoothstep, saturate,
|
||||
length, distance, dot, cross, normalize, faceForward, reflect, refract,
|
||||
// Geometry
|
||||
positionLocal, positionWorld, positionView, positionClip,
|
||||
normalLocal, normalWorld, normalView, normalGeometry,
|
||||
tangentLocal, tangentWorld, tangentView,
|
||||
uv, // uv() for channel 0
|
||||
// Camera
|
||||
cameraPosition, cameraNear, cameraFar, cameraProjectionMatrix,
|
||||
cameraViewMatrix, cameraWorldMatrix,
|
||||
// Time
|
||||
timerLocal, timerGlobal, timerDelta, frameId,
|
||||
// Textures
|
||||
texture, cubeTexture, triplanarTexture,
|
||||
// Display / output
|
||||
screenSize, screenCoordinate, viewportSize, viewportCoordinate,
|
||||
// Control flow
|
||||
If, Loop, select, Fn,
|
||||
// Storage / compute
|
||||
instancedArray, storage, storageObject, compute,
|
||||
instanceIndex, vertexIndex, drawIndex,
|
||||
// Noise
|
||||
mx_noise_float, mx_noise_vec3, mx_fractal_noise_float,
|
||||
mx_cell_noise_float, mx_worley_noise_float,
|
||||
// Color / tone
|
||||
luminance, saturation, vibrance, hue,
|
||||
toneMappingExposure, colorSpaceToWorking,
|
||||
// SDF
|
||||
sdfCircle, sdfBox, sdfRoundedBox,
|
||||
// Miscellaneous
|
||||
hash, range, vertexColor, frontFacing,
|
||||
} from 'three/tsl';
|
||||
```
|
||||
|
||||
## TSL Types and Constructors
|
||||
|
||||
| Constructor | WGSL Type | Example |
|
||||
|---|---|---|
|
||||
| `float(x)` | `f32` | `float(1.0)` |
|
||||
| `int(x)` | `i32` | `int(5)` |
|
||||
| `uint(x)` | `u32` | `uint(10)` |
|
||||
| `bool(x)` | `bool` | `bool(true)` |
|
||||
| `vec2(x, y)` | `vec2f` | `vec2(1, 2)` |
|
||||
| `vec3(x, y, z)` | `vec3f` | `vec3(1, 0, 0)` |
|
||||
| `vec4(x, y, z, w)` | `vec4f` | `vec4(1, 0, 0, 1)` |
|
||||
| `mat2(...)` | `mat2x2f` | `mat2(1,0,0,1)` |
|
||||
| `mat3(...)` | `mat3x3f` | `mat3(...)` |
|
||||
| `mat4(...)` | `mat4x4f` | `mat4(...)` |
|
||||
| `color(hex)` | `vec3f` | `color(0x3b82f6)` |
|
||||
| `uniform(val)` | varies | `uniform(0.5)` |
|
||||
|
||||
## Operators (Method Chaining)
|
||||
|
||||
TSL overloads methods, not JS operators. Use method calls:
|
||||
|
||||
```ts
|
||||
const a = float(1.0);
|
||||
const b = float(2.0);
|
||||
|
||||
a.add(b) // a + b
|
||||
a.sub(b) // a - b
|
||||
a.mul(b) // a * b
|
||||
a.div(b) // a / b
|
||||
a.mod(b) // a % b
|
||||
a.equal(b) // a == b
|
||||
a.notEqual(b) // a != b
|
||||
a.lessThan(b) // a < b
|
||||
a.greaterThan(b) // a > b
|
||||
a.lessThanEqual(b)
|
||||
a.greaterThanEqual(b)
|
||||
a.and(b) // a && b
|
||||
a.or(b) // a || b
|
||||
a.not() // !a
|
||||
a.negate() // -a
|
||||
a.abs() // abs(a)
|
||||
|
||||
// Ternary
|
||||
a.greaterThan(b).select(a, b) // a > b ? a : b
|
||||
```
|
||||
|
||||
## Swizzling
|
||||
|
||||
```ts
|
||||
const v = vec4(1, 2, 3, 4);
|
||||
v.x // float(1)
|
||||
v.xy // vec2(1, 2)
|
||||
v.xyz // vec3(1, 2, 3)
|
||||
v.zyx // vec3(3, 2, 1) -- reorder
|
||||
v.xxx // vec3(1, 1, 1) -- repeat
|
||||
```
|
||||
|
||||
## Uniforms (R3F Pattern)
|
||||
|
||||
```tsx
|
||||
function MyMesh() {
|
||||
const uTime = useMemo(() => uniform(0), []);
|
||||
const uColor = useMemo(() => uniform(new THREE.Color(0xff0000)), []);
|
||||
const uIntensity = useMemo(() => uniform(1.0), []);
|
||||
|
||||
useFrame((state) => {
|
||||
uTime.value = state.clock.elapsedTime;
|
||||
});
|
||||
|
||||
const material = useMemo(() => {
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.colorNode = uColor.mul(uIntensity);
|
||||
mat.emissiveNode = uColor.mul(sin(uTime).mul(0.5).add(0.5));
|
||||
return mat;
|
||||
}, [uTime, uColor, uIntensity]);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<boxGeometry />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Geometry Nodes
|
||||
|
||||
```ts
|
||||
positionLocal // Object-space vertex position
|
||||
positionWorld // World-space vertex position
|
||||
positionView // View-space (camera) position
|
||||
positionClip // Clip-space position
|
||||
|
||||
normalLocal // Object-space normal
|
||||
normalWorld // World-space normal
|
||||
normalView // View-space normal
|
||||
normalGeometry // Raw geometry normal (before skinning)
|
||||
|
||||
tangentLocal // Object-space tangent
|
||||
tangentWorld // World-space tangent
|
||||
|
||||
uv() // UV channel 0
|
||||
uv(1) // UV channel 1
|
||||
```
|
||||
|
||||
## Camera Nodes
|
||||
|
||||
```ts
|
||||
cameraPosition // Camera world position (vec3)
|
||||
cameraNear // Near plane (float)
|
||||
cameraFar // Far plane (float)
|
||||
cameraProjectionMatrix // Projection matrix (mat4)
|
||||
cameraViewMatrix // View matrix (mat4)
|
||||
cameraWorldMatrix // Camera world matrix (mat4)
|
||||
```
|
||||
|
||||
## Time Nodes
|
||||
|
||||
```ts
|
||||
timerLocal() // Time since material creation (seconds, float)
|
||||
timerGlobal() // Time since renderer init (seconds, float)
|
||||
timerDelta() // Delta time per frame (seconds, float)
|
||||
frameId // Integer frame counter
|
||||
```
|
||||
|
||||
## Noise Functions (MaterialX)
|
||||
|
||||
```ts
|
||||
mx_noise_float(pos) // Perlin noise, returns float
|
||||
mx_noise_vec3(pos) // Perlin noise, returns vec3
|
||||
mx_fractal_noise_float(pos, octaves, lacunarity, diminish)
|
||||
mx_cell_noise_float(pos) // Cell/Voronoi noise
|
||||
mx_worley_noise_float(pos) // Worley noise
|
||||
hash(value) // Deterministic hash
|
||||
```
|
||||
|
||||
## Material Slots (Node Overrides)
|
||||
|
||||
```ts
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
|
||||
// Color
|
||||
mat.colorNode // Base color (replaces map)
|
||||
mat.opacityNode // Alpha (replaces alphaMap)
|
||||
mat.emissiveNode // Emissive color
|
||||
mat.metalnessNode // Metalness (float)
|
||||
mat.roughnessNode // Roughness (float)
|
||||
|
||||
// Normals
|
||||
mat.normalNode // Normal map override
|
||||
|
||||
// Position
|
||||
mat.positionNode // Vertex displacement
|
||||
|
||||
// Fragment output
|
||||
mat.fragmentNode // Full fragment override (unlit)
|
||||
mat.outputNode // Final output override (after lighting)
|
||||
|
||||
// Other
|
||||
mat.shadowNode // Custom shadow
|
||||
mat.aoNode // Ambient occlusion
|
||||
mat.clearcoatNode // Clearcoat intensity
|
||||
mat.clearcoatRoughnessNode
|
||||
mat.sheenNode // Sheen color
|
||||
mat.sheenRoughnessNode
|
||||
mat.iorNode // Index of refraction
|
||||
mat.transmissionNode // Transmission
|
||||
mat.thicknessNode // Volume thickness
|
||||
```
|
||||
|
||||
## Control Flow
|
||||
|
||||
```ts
|
||||
// If statement (GPU branching)
|
||||
If(condition, () => {
|
||||
// true branch
|
||||
}).ElseIf(otherCondition, () => {
|
||||
// else-if branch
|
||||
}).Else(() => {
|
||||
// false branch
|
||||
});
|
||||
|
||||
// Select (ternary)
|
||||
select(condition, trueValue, falseValue)
|
||||
|
||||
// Loop
|
||||
Loop(count, ({ i }) => {
|
||||
// loop body, i is the index
|
||||
});
|
||||
|
||||
Loop({ start: 0, end: 10, type: 'int' }, ({ i }) => {
|
||||
// explicit range loop
|
||||
});
|
||||
```
|
||||
|
||||
## Custom TSL Functions
|
||||
|
||||
```ts
|
||||
const myFunc = Fn(([param1, param2]) => {
|
||||
return param1.mul(param2).add(float(1.0));
|
||||
});
|
||||
|
||||
// Usage in material
|
||||
mat.colorNode = myFunc(uv(), float(2.0));
|
||||
```
|
||||
|
||||
## Compute Shaders
|
||||
|
||||
```ts
|
||||
import { instancedArray, storage, compute, instanceIndex } from 'three/tsl';
|
||||
|
||||
// Create storage buffer
|
||||
const posBuffer = instancedArray(count, 'vec3');
|
||||
const velBuffer = instancedArray(count, 'vec3');
|
||||
|
||||
// Define compute kernel
|
||||
const updateParticles = Fn(() => {
|
||||
const pos = posBuffer.element(instanceIndex);
|
||||
const vel = velBuffer.element(instanceIndex);
|
||||
pos.addAssign(vel.mul(timerDelta()));
|
||||
});
|
||||
|
||||
// Create compute node
|
||||
const computeNode = updateParticles().compute(count);
|
||||
|
||||
// Dispatch in useFrame
|
||||
useFrame(({ gl }) => {
|
||||
(gl as WebGPURenderer).computeAsync(computeNode);
|
||||
});
|
||||
```
|
||||
|
||||
## Post-Processing Nodes
|
||||
|
||||
```ts
|
||||
import { PostProcessing } from 'three/addons/renderers/common/PostProcessing.js';
|
||||
import { pass, bloom, dotScreen, film, fxaa } from 'three/tsl';
|
||||
|
||||
const scenePass = pass(scene, camera);
|
||||
const beautyPass = scenePass.getTextureNode();
|
||||
|
||||
// Chain effects
|
||||
const postProcessing = new PostProcessing(renderer);
|
||||
postProcessing.outputNode = beautyPass
|
||||
.pipe(bloom({ strength: 0.5, radius: 0.4, threshold: 0.85 }))
|
||||
.pipe(fxaa())
|
||||
.pipe(film({ intensity: 0.1 }));
|
||||
```
|
||||
|
||||
## Textures
|
||||
|
||||
```ts
|
||||
import { texture, cubeTexture, triplanarTexture } from 'three/tsl';
|
||||
|
||||
const tex = texture(myTexture); // Sample texture
|
||||
const texUV = texture(myTexture, uv()); // Explicit UV
|
||||
tex.uv(customUV) // Custom UV coordinates
|
||||
tex.level(mipLevel) // Specific mip level
|
||||
tex.bias(lodBias) // LOD bias
|
||||
|
||||
const cube = cubeTexture(myCubeMap);
|
||||
const triplanar = triplanarTexture(tex, null, null, normalWorld);
|
||||
```
|
||||
|
||||
## SDF (Signed Distance Fields)
|
||||
|
||||
```ts
|
||||
sdfCircle(uv().sub(0.5), float(0.3)) // Circle SDF
|
||||
sdfBox(uv().sub(0.5), vec2(0.3, 0.2)) // Box SDF
|
||||
sdfRoundedBox(uv().sub(0.5), vec2(0.3, 0.2), vec4(0.05))
|
||||
```
|
||||
|
||||
## Color Operations
|
||||
|
||||
```ts
|
||||
luminance(colorNode) // Perceived brightness
|
||||
saturation(colorNode, amount) // Adjust saturation
|
||||
vibrance(colorNode, amount) // Adjust vibrance
|
||||
hue(colorNode, amount) // Rotate hue
|
||||
```
|
||||
|
||||
## Screen/Viewport
|
||||
|
||||
```ts
|
||||
screenSize // vec2 -- screen dimensions in pixels
|
||||
screenCoordinate // vec2 -- fragment position in pixels
|
||||
viewportSize // vec2 -- viewport dimensions
|
||||
viewportCoordinate // vec2 -- normalized viewport coords (0..1)
|
||||
```
|
||||
|
||||
## Common R3F + TSL Patterns
|
||||
|
||||
### Extend for JSX (optional, for frequently reused materials)
|
||||
|
||||
```tsx
|
||||
import { extend } from '@react-three/fiber';
|
||||
import { MeshStandardNodeMaterial } from 'three/webgpu';
|
||||
|
||||
extend({ MeshStandardNodeMaterial });
|
||||
|
||||
// Now usable as JSX (but cannot set node properties declaratively)
|
||||
<mesh>
|
||||
<boxGeometry />
|
||||
<meshStandardNodeMaterial color="hotpink" />
|
||||
</mesh>
|
||||
```
|
||||
|
||||
### Ref-Based Node Updates
|
||||
|
||||
```tsx
|
||||
function DynamicShader() {
|
||||
const matRef = useRef<MeshStandardNodeMaterial>(null!);
|
||||
const uHover = useMemo(() => uniform(0), []);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
uHover.value = THREE.MathUtils.lerp(uHover.value, hovered ? 1 : 0, delta * 5);
|
||||
});
|
||||
|
||||
const material = useMemo(() => {
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.colorNode = mix(color(0x1e293b), color(0x3b82f6), uHover);
|
||||
return mat;
|
||||
}, [uHover]);
|
||||
|
||||
return (
|
||||
<mesh onPointerOver={() => setHovered(true)} onPointerOut={() => setHovered(false)}>
|
||||
<sphereGeometry args={[1, 32, 32]} />
|
||||
<primitive object={material} attach="material" ref={matRef} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Dispose on Unmount
|
||||
|
||||
TSL materials must be disposed like any Three.js material. When using `useMemo` +
|
||||
`<primitive>`, R3F auto-disposes on unmount. For manual management:
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
return () => material.dispose();
|
||||
}, [material]);
|
||||
```
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
name: webgpu-threejs-tsl
|
||||
description: WebGPU rendering with Three.js TSL (Three Shading Language) in React Three Fiber. Use when building GPU-accelerated visuals, compute shaders, node materials, or post-processing effects in the Suaveplan R3F ecosystem.
|
||||
---
|
||||
|
||||
# WebGPU + TSL in React Three Fiber
|
||||
|
||||
Three.js r170+ ships a full **WebGPU renderer** and **TSL (Three Shading Language)** -- a
|
||||
JavaScript-authored shading language that compiles to WGSL (WebGPU) or GLSL (WebGL fallback).
|
||||
This skill covers how to use both inside Suaveplan's React Three Fiber packages.
|
||||
|
||||
## Suaveplan R3F Packages
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/r3f` | Core R3F utilities and components |
|
||||
| `@suaveplan/r3f-shaders` | Custom shader library (GLSL + TSL) |
|
||||
| `@suaveplan/r3f-performance` | Performance monitoring with plugin system |
|
||||
| `@suaveplan/r3f-physics-core` | Physics abstraction layer |
|
||||
|
||||
## Quick Start -- WebGPU Canvas in R3F
|
||||
|
||||
R3F does not yet export a first-class `<Canvas>` prop for WebGPU. You create the
|
||||
renderer yourself and pass it via the `gl` prop.
|
||||
|
||||
```tsx
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
|
||||
|
||||
async function createWebGPURenderer(canvas: HTMLCanvasElement) {
|
||||
const renderer = new WebGPURenderer({ canvas, antialias: true });
|
||||
await renderer.init();
|
||||
return renderer;
|
||||
}
|
||||
|
||||
export function WebGPUCanvas({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Canvas
|
||||
gl={(canvas) => createWebGPURenderer(canvas)}
|
||||
// frameloop="demand" if you drive frames manually
|
||||
>
|
||||
{children}
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Important: `gl` callback must return the renderer
|
||||
|
||||
R3F calls the `gl` function with the underlying `<canvas>` element. The function must
|
||||
return a renderer instance. Because `WebGPURenderer.init()` is async, R3F >= 8.17
|
||||
supports returning a Promise from the `gl` callback.
|
||||
|
||||
## Quick Start -- TSL Node Material
|
||||
|
||||
TSL materials replace GLSL strings with composable JavaScript nodes.
|
||||
|
||||
```tsx
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import { useRef, useMemo } from 'react';
|
||||
import { MeshStandardNodeMaterial } from 'three/webgpu';
|
||||
import { color, uniform, sin, timerLocal } from 'three/tsl';
|
||||
|
||||
function PulsingSphere() {
|
||||
const matRef = useRef<MeshStandardNodeMaterial>(null!);
|
||||
|
||||
const material = useMemo(() => {
|
||||
const time = timerLocal();
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.colorNode = color(0x3b82f6).mul(sin(time).mul(0.5).add(0.5));
|
||||
return mat;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<primitive object={material} attach="material" ref={matRef} />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Key Pattern: `useMemo` + `primitive`
|
||||
|
||||
1. Create the node material inside `useMemo` so it is constructed once.
|
||||
2. Attach it with `<primitive object={mat} attach="material" />`.
|
||||
3. TSL `timerLocal()` auto-updates each frame -- no `useFrame` needed for basic time.
|
||||
4. For uniforms you want to drive from React state, use `uniform()` and update in `useFrame`.
|
||||
|
||||
## TSL Uniform Updates with useFrame
|
||||
|
||||
```tsx
|
||||
import { uniform } from 'three/tsl';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
|
||||
function AnimatedMesh() {
|
||||
const uProgress = useMemo(() => uniform(0), []);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
uProgress.value = Math.min(uProgress.value + delta * 0.5, 1);
|
||||
});
|
||||
|
||||
const material = useMemo(() => {
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.colorNode = mix(color(0xff0000), color(0x00ff00), uProgress);
|
||||
return mat;
|
||||
}, [uProgress]);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<boxGeometry />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## WebGL Fallback
|
||||
|
||||
TSL compiles to GLSL when WebGPU is unavailable. The same node material code works
|
||||
with the standard `<Canvas>` (WebGL2 renderer). Only compute shaders and storage
|
||||
buffers require the WebGPU backend.
|
||||
|
||||
```tsx
|
||||
import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
|
||||
|
||||
async function createRenderer(canvas: HTMLCanvasElement) {
|
||||
if (!navigator.gpu) {
|
||||
// Falls back automatically -- TSL materials still work on WebGL
|
||||
return new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||
}
|
||||
const renderer = new WebGPURenderer({ canvas, antialias: true });
|
||||
await renderer.init();
|
||||
return renderer;
|
||||
}
|
||||
```
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
| Document | Contents |
|
||||
|---|---|
|
||||
| [REFERENCE.md](./REFERENCE.md) | Full TSL API cheatsheet |
|
||||
| [core-concepts.md](./docs/core-concepts.md) | Types, operators, uniforms, control flow |
|
||||
| [materials.md](./docs/materials.md) | Node materials in R3F |
|
||||
| [compute-shaders.md](./docs/compute-shaders.md) | GPU compute, particles, storage |
|
||||
| [post-processing.md](./docs/post-processing.md) | Bloom, blur, SSAO, custom effects |
|
||||
| [wgsl-integration.md](./docs/wgsl-integration.md) | Raw WGSL via wgslFn() |
|
||||
| [device-loss.md](./docs/device-loss.md) | GPU device loss recovery |
|
||||
|
||||
## Integration with @suaveplan/r3f-shaders
|
||||
|
||||
The existing `@suaveplan/r3f-shaders` package uses GLSL string shaders. TSL node
|
||||
materials can coexist in the same scene. When adding new shader effects, prefer TSL
|
||||
for cross-backend compatibility. Existing GLSL shaders continue to work on the WebGL
|
||||
backend without modification.
|
||||
|
||||
### Naming Convention
|
||||
|
||||
- GLSL shaders: `src/shaders/<category>/<name>.ts` (exports string constants)
|
||||
- TSL materials: `src/tsl/<category>/<name>.ts` (exports node material factories)
|
||||
|
||||
## Suaveplan Code Standards
|
||||
|
||||
- React 19, TypeScript strict mode
|
||||
- Max 250 lines per file
|
||||
- Vitest for testing, 95% coverage minimum
|
||||
- No mocks -- test with real WebGPU/WebGL contexts where possible
|
||||
- Tree-shakeable exports with `sideEffects: false`
|
||||
@@ -0,0 +1,327 @@
|
||||
# Compute Shaders in React Three Fiber
|
||||
|
||||
Compute shaders run arbitrary GPU programs outside the render pipeline. They are
|
||||
used for particle simulation, physics, image processing, and any massively parallel
|
||||
workload. **Compute shaders require the WebGPU backend** -- they do not fall back
|
||||
to WebGL.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```tsx
|
||||
import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
|
||||
|
||||
// Canvas MUST use WebGPURenderer for compute
|
||||
<Canvas gl={(canvas) => {
|
||||
const renderer = new WebGPURenderer({ canvas, antialias: true });
|
||||
await renderer.init();
|
||||
return renderer;
|
||||
}}>
|
||||
```
|
||||
|
||||
## Core API
|
||||
|
||||
### Storage Buffers
|
||||
|
||||
Storage buffers hold GPU-accessible data arrays:
|
||||
|
||||
```ts
|
||||
import { instancedArray, storage, instanceIndex } from 'three/tsl';
|
||||
|
||||
// instancedArray(count, type) -- creates a typed storage buffer
|
||||
const positions = instancedArray(1024, 'vec3'); // 1024 vec3 values
|
||||
const velocities = instancedArray(1024, 'vec3');
|
||||
const lifetimes = instancedArray(1024, 'float');
|
||||
const colors = instancedArray(1024, 'vec4');
|
||||
```
|
||||
|
||||
### Element Access
|
||||
|
||||
```ts
|
||||
// Read/write individual elements by index
|
||||
const pos = positions.element(instanceIndex); // current instance
|
||||
const vel = velocities.element(instanceIndex);
|
||||
|
||||
// Read another element (e.g., neighbor)
|
||||
const neighbor = positions.element(instanceIndex.add(1));
|
||||
```
|
||||
|
||||
### Compute Functions
|
||||
|
||||
```ts
|
||||
import { Fn, compute } from 'three/tsl';
|
||||
|
||||
// Define the kernel
|
||||
const updateKernel = Fn(() => {
|
||||
const pos = positions.element(instanceIndex);
|
||||
const vel = velocities.element(instanceIndex);
|
||||
|
||||
// Update position by velocity * dt
|
||||
pos.addAssign(vel.mul(timerDelta()));
|
||||
|
||||
// Bounce off floor
|
||||
If(pos.y.lessThan(0), () => {
|
||||
pos.y.assign(0);
|
||||
vel.y.assign(vel.y.negate().mul(0.8));
|
||||
});
|
||||
});
|
||||
|
||||
// Create compute node with workgroup count
|
||||
const computeNode = updateKernel().compute(1024);
|
||||
```
|
||||
|
||||
### Dispatching from useFrame
|
||||
|
||||
```tsx
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import type WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
|
||||
|
||||
function ParticleSystem() {
|
||||
const computeRef = useRef(computeNode);
|
||||
|
||||
useFrame(({ gl }) => {
|
||||
const renderer = gl as unknown as WebGPURenderer;
|
||||
renderer.computeAsync(computeRef.current);
|
||||
});
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Particle System Example
|
||||
|
||||
```tsx
|
||||
import { useRef, useMemo } from 'react';
|
||||
import { useFrame, Canvas } from '@react-three/fiber';
|
||||
import * as THREE from 'three';
|
||||
import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
|
||||
import { PointsNodeMaterial } from 'three/webgpu';
|
||||
import {
|
||||
instancedArray, instanceIndex, Fn, compute,
|
||||
float, vec3, timerDelta, timerLocal, hash,
|
||||
storage, If, sin, cos, color, mix,
|
||||
} from 'three/tsl';
|
||||
|
||||
const PARTICLE_COUNT = 10_000;
|
||||
|
||||
function GPUParticles() {
|
||||
const { computeNode, material, geometry } = useMemo(() => {
|
||||
// Storage buffers
|
||||
const positions = instancedArray(PARTICLE_COUNT, 'vec3');
|
||||
const velocities = instancedArray(PARTICLE_COUNT, 'vec3');
|
||||
const ages = instancedArray(PARTICLE_COUNT, 'float');
|
||||
|
||||
// Init kernel -- runs once to seed particles
|
||||
const initKernel = Fn(() => {
|
||||
const i = float(instanceIndex);
|
||||
const h = hash(i);
|
||||
const angle = h.mul(Math.PI * 2);
|
||||
const radius = hash(i.add(1)).mul(2);
|
||||
|
||||
positions.element(instanceIndex).assign(
|
||||
vec3(cos(angle).mul(radius), float(0), sin(angle).mul(radius))
|
||||
);
|
||||
velocities.element(instanceIndex).assign(
|
||||
vec3(cos(angle).mul(0.5), hash(i.add(2)).mul(3).add(1), sin(angle).mul(0.5))
|
||||
);
|
||||
ages.element(instanceIndex).assign(hash(i.add(3)).mul(5));
|
||||
});
|
||||
|
||||
// Update kernel -- runs each frame
|
||||
const updateKernel = Fn(() => {
|
||||
const pos = positions.element(instanceIndex);
|
||||
const vel = velocities.element(instanceIndex);
|
||||
const age = ages.element(instanceIndex);
|
||||
const dt = timerDelta();
|
||||
|
||||
// Gravity
|
||||
vel.y.subAssign(float(9.8).mul(dt));
|
||||
|
||||
// Move
|
||||
pos.addAssign(vel.mul(dt));
|
||||
|
||||
// Age
|
||||
age.addAssign(dt);
|
||||
|
||||
// Respawn when aged out
|
||||
If(age.greaterThan(5.0), () => {
|
||||
pos.assign(vec3(0, 0, 0));
|
||||
vel.y.assign(hash(float(instanceIndex).add(age)).mul(3).add(2));
|
||||
age.assign(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Compute nodes
|
||||
const initNode = initKernel().compute(PARTICLE_COUNT);
|
||||
const updateNode = updateKernel().compute(PARTICLE_COUNT);
|
||||
|
||||
// Material reads from positions buffer
|
||||
const mat = new PointsNodeMaterial();
|
||||
mat.sizeNode = float(3.0);
|
||||
mat.colorNode = mix(
|
||||
color(0xff6600),
|
||||
color(0xffcc00),
|
||||
ages.element(instanceIndex).div(5).saturate()
|
||||
);
|
||||
|
||||
// Geometry uses storage buffer as position attribute
|
||||
const geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute('position',
|
||||
new THREE.BufferAttribute(new Float32Array(PARTICLE_COUNT * 3), 3)
|
||||
);
|
||||
geo.drawRange.count = PARTICLE_COUNT;
|
||||
|
||||
return {
|
||||
initNode,
|
||||
computeNode: updateNode,
|
||||
material: mat,
|
||||
geometry: geo,
|
||||
positions,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const initialized = useRef(false);
|
||||
|
||||
useFrame(({ gl }) => {
|
||||
const renderer = gl as unknown as WebGPURenderer;
|
||||
|
||||
if (!initialized.current) {
|
||||
renderer.computeAsync(initNode);
|
||||
initialized.current = true;
|
||||
}
|
||||
|
||||
renderer.computeAsync(computeNode);
|
||||
});
|
||||
|
||||
return (
|
||||
<points geometry={geometry}>
|
||||
<primitive object={material} attach="material" />
|
||||
</points>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Storage Buffer Types
|
||||
|
||||
| Type String | WGSL Type | Byte Size |
|
||||
|---|---|---|
|
||||
| `'float'` | `f32` | 4 |
|
||||
| `'vec2'` | `vec2f` | 8 |
|
||||
| `'vec3'` | `vec3f` | 12 (padded to 16) |
|
||||
| `'vec4'` | `vec4f` | 16 |
|
||||
| `'int'` | `i32` | 4 |
|
||||
| `'uint'` | `u32` | 4 |
|
||||
| `'mat3'` | `mat3x3f` | 48 |
|
||||
| `'mat4'` | `mat4x4f` | 64 |
|
||||
|
||||
## Reading Back Storage Data
|
||||
|
||||
To read GPU buffer data back to the CPU (for debugging, saving state):
|
||||
|
||||
```ts
|
||||
const renderer = gl as unknown as WebGPURenderer;
|
||||
|
||||
// Read buffer back asynchronously
|
||||
const data = await renderer.getArrayBufferAsync(positions.value);
|
||||
const floats = new Float32Array(data);
|
||||
```
|
||||
|
||||
**Warning**: GPU readback is slow. Avoid doing it every frame. Use only for
|
||||
debugging, snapshots, or saving state.
|
||||
|
||||
## Workgroup Size
|
||||
|
||||
The `.compute(count)` argument defines the total dispatch size. Three.js
|
||||
handles workgroup subdivision internally (typically 64 invocations per workgroup).
|
||||
|
||||
```ts
|
||||
// 1024 total invocations
|
||||
const computeNode = myKernel().compute(1024);
|
||||
|
||||
// For 2D dispatches (e.g., image processing)
|
||||
const computeNode2D = myKernel().compute(width * height);
|
||||
```
|
||||
|
||||
## Atomic Operations
|
||||
|
||||
For concurrent writes (histograms, counters):
|
||||
|
||||
```ts
|
||||
import { atomicAdd, atomicStore, atomicLoad } from 'three/tsl';
|
||||
|
||||
const counter = instancedArray(1, 'uint');
|
||||
|
||||
const countKernel = Fn(() => {
|
||||
If(someCondition, () => {
|
||||
atomicAdd(counter.element(uint(0)), uint(1));
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Double Buffering
|
||||
|
||||
For simulations where reading and writing the same buffer causes race conditions:
|
||||
|
||||
```ts
|
||||
const posA = instancedArray(count, 'vec3');
|
||||
const posB = instancedArray(count, 'vec3');
|
||||
|
||||
// Frame N: read A, write B
|
||||
const stepA = Fn(() => {
|
||||
const src = posA.element(instanceIndex);
|
||||
const dst = posB.element(instanceIndex);
|
||||
dst.assign(src.add(vel.mul(timerDelta())));
|
||||
}).compute(count);
|
||||
|
||||
// Frame N+1: read B, write A
|
||||
const stepB = Fn(() => {
|
||||
const src = posB.element(instanceIndex);
|
||||
const dst = posA.element(instanceIndex);
|
||||
dst.assign(src.add(vel.mul(timerDelta())));
|
||||
}).compute(count);
|
||||
|
||||
// Toggle each frame
|
||||
let frame = 0;
|
||||
useFrame(({ gl }) => {
|
||||
const renderer = gl as unknown as WebGPURenderer;
|
||||
renderer.computeAsync(frame % 2 === 0 ? stepA : stepB);
|
||||
frame++;
|
||||
});
|
||||
```
|
||||
|
||||
## Integration with @suaveplan/r3f-physics-core
|
||||
|
||||
Compute shaders can implement physics simulations that integrate with the
|
||||
`@suaveplan/r3f-physics-core` abstraction layer. The physics core defines
|
||||
an interface for simulation steps:
|
||||
|
||||
```ts
|
||||
// Implement the physics step with a compute shader
|
||||
interface ComputePhysicsProvider {
|
||||
init(count: number): void;
|
||||
step(renderer: WebGPURenderer, dt: number): void;
|
||||
getPositions(): StorageBuffer;
|
||||
getVelocities(): StorageBuffer;
|
||||
}
|
||||
```
|
||||
|
||||
Use the `useFrame` priority parameter to ensure compute dispatch runs
|
||||
before the render pass:
|
||||
|
||||
```tsx
|
||||
useFrame(({ gl }) => {
|
||||
const renderer = gl as unknown as WebGPURenderer;
|
||||
renderer.computeAsync(physicsComputeNode);
|
||||
}, -1); // Priority -1 runs before default (0) render
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Minimize readbacks**: `getArrayBufferAsync` stalls the pipeline.
|
||||
2. **Batch dispatches**: Multiple compute calls per frame are fine but each
|
||||
adds dispatch overhead.
|
||||
3. **Storage alignment**: vec3 is padded to 16 bytes in WGSL. Use vec4 if
|
||||
you need the extra channel anyway.
|
||||
4. **Workgroup locality**: Access patterns that read neighboring elements
|
||||
benefit from workgroup shared memory (not yet exposed in TSL).
|
||||
5. **Async compute**: `computeAsync` returns a Promise. In R3F useFrame,
|
||||
you typically fire-and-forget (do not await).
|
||||
@@ -0,0 +1,398 @@
|
||||
# TSL Core Concepts
|
||||
|
||||
Three Shading Language (TSL) is a graph-based shading system where you compose
|
||||
JavaScript objects ("nodes") into shader programs. The Three.js compiler then emits
|
||||
WGSL (WebGPU) or GLSL (WebGL) depending on the active backend.
|
||||
|
||||
## Node Graph Model
|
||||
|
||||
Every TSL expression produces a **node**. Nodes are connected by method calls to
|
||||
form an acyclic graph. The compiler walks the graph to generate shader source.
|
||||
|
||||
```ts
|
||||
import { float, sin, timerLocal, color, mix } from 'three/tsl';
|
||||
|
||||
// Each call returns a node; chaining builds the graph
|
||||
const time = timerLocal();
|
||||
const wave = sin(time.mul(2.0)).mul(0.5).add(0.5);
|
||||
const result = mix(color(0x000000), color(0x3b82f6), wave);
|
||||
```
|
||||
|
||||
This graph compiles to roughly:
|
||||
|
||||
```wgsl
|
||||
let wave = sin(time * 2.0) * 0.5 + 0.5;
|
||||
let result = mix(vec3(0.0), vec3(0.231, 0.510, 0.965), wave);
|
||||
```
|
||||
|
||||
## Primitive Types
|
||||
|
||||
### Scalars
|
||||
|
||||
```ts
|
||||
float(1.0) // f32
|
||||
int(5) // i32
|
||||
uint(10) // u32
|
||||
bool(true) // bool
|
||||
```
|
||||
|
||||
### Vectors
|
||||
|
||||
```ts
|
||||
vec2(1.0, 2.0) // vec2f
|
||||
vec3(1.0, 0.0, 0.0) // vec3f
|
||||
vec4(1.0, 0.0, 0.0, 1.0) // vec4f
|
||||
color(0xff6600) // vec3f from hex
|
||||
color(new THREE.Color('red')) // vec3f from Color
|
||||
```
|
||||
|
||||
### Matrices
|
||||
|
||||
```ts
|
||||
mat2(1, 0, 0, 1) // 2x2 identity
|
||||
mat3(...) // 3x3
|
||||
mat4(...) // 4x4
|
||||
```
|
||||
|
||||
### Type Coercion
|
||||
|
||||
TSL auto-promotes scalars in arithmetic:
|
||||
|
||||
```ts
|
||||
vec3(1, 0, 0).mul(0.5) // float(0.5) auto-wrapped
|
||||
float(1).add(2) // int 2 auto-wrapped to float
|
||||
```
|
||||
|
||||
## Swizzling
|
||||
|
||||
All vector nodes support GLSL-style swizzle access:
|
||||
|
||||
```ts
|
||||
const v = vec4(1, 2, 3, 4);
|
||||
v.x // float -- component access
|
||||
v.xy // vec2
|
||||
v.xyz // vec3
|
||||
v.zyx // vec3 -- reversed
|
||||
v.rrr // vec3 -- RGBA aliases work too
|
||||
v.stpq // vec4 -- texture coordinate aliases
|
||||
```
|
||||
|
||||
Assignment swizzles are not supported in TSL. To set individual components,
|
||||
construct a new vector:
|
||||
|
||||
```ts
|
||||
// Instead of v.x = 5, do:
|
||||
const modified = vec4(float(5), v.y, v.z, v.w);
|
||||
```
|
||||
|
||||
## Operators
|
||||
|
||||
TSL uses method calls rather than JavaScript operators because JS does not support
|
||||
operator overloading.
|
||||
|
||||
### Arithmetic
|
||||
|
||||
```ts
|
||||
a.add(b) // +
|
||||
a.sub(b) // -
|
||||
a.mul(b) // *
|
||||
a.div(b) // /
|
||||
a.mod(b) // %
|
||||
a.negate() // unary -
|
||||
```
|
||||
|
||||
### Comparison
|
||||
|
||||
```ts
|
||||
a.equal(b) // ==
|
||||
a.notEqual(b) // !=
|
||||
a.lessThan(b) // <
|
||||
a.greaterThan(b) // >
|
||||
a.lessThanEqual(b) // <=
|
||||
a.greaterThanEqual(b) // >=
|
||||
```
|
||||
|
||||
### Logical
|
||||
|
||||
```ts
|
||||
a.and(b) // &&
|
||||
a.or(b) // ||
|
||||
a.not() // !
|
||||
```
|
||||
|
||||
### Assignment (Compute Only)
|
||||
|
||||
In compute shaders, storage elements support in-place assignment:
|
||||
|
||||
```ts
|
||||
element.assign(newValue) // =
|
||||
element.addAssign(delta) // +=
|
||||
element.subAssign(delta) // -=
|
||||
element.mulAssign(factor) // *=
|
||||
```
|
||||
|
||||
## Uniforms
|
||||
|
||||
Uniforms are the bridge between JavaScript (React state, useFrame) and GPU code.
|
||||
|
||||
### Creating Uniforms
|
||||
|
||||
```ts
|
||||
import { uniform } from 'three/tsl';
|
||||
|
||||
const uFloat = uniform(0.0); // float uniform
|
||||
const uVec3 = uniform(new THREE.Vector3()); // vec3 uniform
|
||||
const uColor = uniform(new THREE.Color()); // vec3 uniform
|
||||
const uMat4 = uniform(new THREE.Matrix4()); // mat4 uniform
|
||||
const uInt = uniform(int(0)); // explicit int uniform
|
||||
```
|
||||
|
||||
### Updating Uniforms in R3F
|
||||
|
||||
```tsx
|
||||
function AnimatedMesh() {
|
||||
const uProgress = useMemo(() => uniform(0.0), []);
|
||||
const uMouse = useMemo(() => uniform(new THREE.Vector2()), []);
|
||||
|
||||
useFrame((state) => {
|
||||
uProgress.value = state.clock.elapsedTime;
|
||||
uMouse.value.set(
|
||||
state.pointer.x * 0.5 + 0.5,
|
||||
state.pointer.y * 0.5 + 0.5
|
||||
);
|
||||
});
|
||||
|
||||
const material = useMemo(() => {
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.colorNode = mix(color(0x000000), color(0xffffff), uProgress.fract());
|
||||
return mat;
|
||||
}, [uProgress]);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<boxGeometry />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Uniform Value Types
|
||||
|
||||
| JS Value | TSL Type | Notes |
|
||||
|---|---|---|
|
||||
| `number` | `float` | Default for bare numbers |
|
||||
| `THREE.Vector2` | `vec2` | |
|
||||
| `THREE.Vector3` | `vec3` | |
|
||||
| `THREE.Vector4` | `vec4` | |
|
||||
| `THREE.Color` | `vec3` | RGB, no alpha |
|
||||
| `THREE.Matrix3` | `mat3` | |
|
||||
| `THREE.Matrix4` | `mat4` | |
|
||||
| `int(n)` | `int` | Explicit int type |
|
||||
| `uint(n)` | `uint` | Explicit uint type |
|
||||
|
||||
## Time and Animation
|
||||
|
||||
TSL has built-in time nodes that auto-increment each frame:
|
||||
|
||||
```ts
|
||||
timerLocal() // Seconds since this material was first rendered
|
||||
timerGlobal() // Seconds since renderer was created
|
||||
timerDelta() // Seconds elapsed since previous frame
|
||||
frameId // Integer frame counter (uint)
|
||||
```
|
||||
|
||||
### R3F Integration -- timerLocal vs useFrame
|
||||
|
||||
For pure-GPU animation, `timerLocal()` is simpler and avoids JS-to-GPU
|
||||
round trips:
|
||||
|
||||
```ts
|
||||
// Pure GPU -- no useFrame needed
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.colorNode = color(0x3b82f6).mul(sin(timerLocal().mul(3.0)).mul(0.5).add(0.5));
|
||||
```
|
||||
|
||||
Use `uniform()` + `useFrame()` only when animation depends on React state,
|
||||
pointer position, scroll, or other JS-side data.
|
||||
|
||||
## Attributes and Varyings
|
||||
|
||||
### Built-in Attributes
|
||||
|
||||
```ts
|
||||
positionLocal // Vertex position in object space
|
||||
normalLocal // Vertex normal in object space
|
||||
uv() // UV channel 0
|
||||
uv(1) // UV channel 1
|
||||
vertexColor // Vertex color attribute
|
||||
tangentLocal // Tangent vector
|
||||
```
|
||||
|
||||
### Custom Attributes
|
||||
|
||||
```ts
|
||||
import { attribute } from 'three/tsl';
|
||||
|
||||
// Reads from geometry.setAttribute('aCustom', ...)
|
||||
const custom = attribute('aCustom', 'vec3');
|
||||
```
|
||||
|
||||
### Varyings
|
||||
|
||||
Varyings pass data from vertex to fragment shader:
|
||||
|
||||
```ts
|
||||
import { varying, varyingProperty } from 'three/tsl';
|
||||
|
||||
// Declare a varying
|
||||
const vWorldPos = varying(positionWorld, 'v_worldPos');
|
||||
|
||||
// Or use varyingProperty for named access
|
||||
varyingProperty('vec3', 'v_myData').assign(someNode);
|
||||
```
|
||||
|
||||
## Control Flow
|
||||
|
||||
### Branching (If/Else)
|
||||
|
||||
```ts
|
||||
import { If } from 'three/tsl';
|
||||
|
||||
If(condition, () => {
|
||||
// true branch
|
||||
}).ElseIf(otherCondition, () => {
|
||||
// else-if
|
||||
}).Else(() => {
|
||||
// false branch
|
||||
});
|
||||
```
|
||||
|
||||
### Select (Ternary)
|
||||
|
||||
```ts
|
||||
import { select } from 'three/tsl';
|
||||
|
||||
// select(condition, ifTrue, ifFalse)
|
||||
const result = select(uv().x.greaterThan(0.5), color(0xff0000), color(0x0000ff));
|
||||
```
|
||||
|
||||
### Loops
|
||||
|
||||
```ts
|
||||
import { Loop } from 'three/tsl';
|
||||
|
||||
// Simple counted loop
|
||||
Loop(10, ({ i }) => {
|
||||
// i is an int node, 0..9
|
||||
});
|
||||
|
||||
// Explicit range
|
||||
Loop({ start: int(0), end: int(100), type: 'int' }, ({ i }) => {
|
||||
// body
|
||||
});
|
||||
|
||||
// Break out early
|
||||
Loop(100, ({ i, Break }) => {
|
||||
If(someCondition, () => {
|
||||
Break();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Custom Functions (Fn)
|
||||
|
||||
`Fn` creates reusable TSL functions. They compile to WGSL/GLSL functions.
|
||||
|
||||
```ts
|
||||
import { Fn } from 'three/tsl';
|
||||
|
||||
const fbm = Fn(([pos, octaves]) => {
|
||||
const value = float(0.0).toVar();
|
||||
const amplitude = float(0.5).toVar();
|
||||
const frequency = float(1.0).toVar();
|
||||
|
||||
Loop(octaves, () => {
|
||||
value.addAssign(mx_noise_float(pos.mul(frequency)).mul(amplitude));
|
||||
amplitude.mulAssign(0.5);
|
||||
frequency.mulAssign(2.0);
|
||||
});
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
// Use in material
|
||||
mat.colorNode = vec3(fbm(positionWorld, int(6)));
|
||||
```
|
||||
|
||||
### Important: `.toVar()`
|
||||
|
||||
Inside `Fn`, mutable local variables require `.toVar()`:
|
||||
|
||||
```ts
|
||||
const myValue = float(0.0).toVar(); // mutable local
|
||||
myValue.addAssign(1.0); // modify in place
|
||||
```
|
||||
|
||||
Without `.toVar()`, the node is an immutable expression and `addAssign` will fail.
|
||||
|
||||
## Math Functions
|
||||
|
||||
All standard math is available as both standalone functions and methods:
|
||||
|
||||
```ts
|
||||
// Standalone
|
||||
sin(x), cos(x), tan(x)
|
||||
asin(x), acos(x), atan(x), atan2(y, x)
|
||||
pow(x, y), exp(x), log(x), sqrt(x)
|
||||
abs(x), sign(x), floor(x), ceil(x), fract(x), round(x)
|
||||
min(a, b), max(a, b), clamp(x, lo, hi)
|
||||
mix(a, b, t), step(edge, x), smoothstep(lo, hi, x)
|
||||
length(v), distance(a, b), dot(a, b), cross(a, b), normalize(v)
|
||||
reflect(v, n), refract(v, n, eta)
|
||||
|
||||
// As methods
|
||||
x.sin(), x.cos(), x.abs(), x.floor()
|
||||
v.length(), v.normalize(), v.dot(other)
|
||||
a.mix(b, t), x.clamp(0, 1), x.smoothstep(0, 1)
|
||||
x.saturate() // clamp(x, 0, 1)
|
||||
```
|
||||
|
||||
## Textures
|
||||
|
||||
```ts
|
||||
import { texture } from 'three/tsl';
|
||||
|
||||
const tex = texture(myTexture); // Sample at default UV
|
||||
const texCustom = texture(myTexture, customUV); // Custom UV
|
||||
tex.uv(customUV) // Alternative UV syntax
|
||||
tex.level(float(2)) // Specific mip level
|
||||
tex.bias(float(-1)) // LOD bias
|
||||
|
||||
// Texture properties
|
||||
tex.r, tex.g, tex.b, tex.a // Channel access
|
||||
```
|
||||
|
||||
### R3F Texture Loading
|
||||
|
||||
```tsx
|
||||
import { useTexture } from '@react-three/drei';
|
||||
import { texture } from 'three/tsl';
|
||||
|
||||
function TexturedMesh() {
|
||||
const map = useTexture('/textures/brick.jpg');
|
||||
|
||||
const material = useMemo(() => {
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.colorNode = texture(map);
|
||||
return mat;
|
||||
}, [map]);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<boxGeometry />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,422 @@
|
||||
# GPU Device Loss and Recovery
|
||||
|
||||
GPU device loss occurs when the browser's WebGPU device becomes unavailable.
|
||||
Common causes: driver crash, GPU hang, system sleep/wake, GPU reset by OS,
|
||||
resource exhaustion, or the user switching GPUs. Your application must handle
|
||||
this gracefully.
|
||||
|
||||
## How Device Loss Manifests
|
||||
|
||||
When the GPU device is lost:
|
||||
|
||||
1. All GPU resources (buffers, textures, pipelines) become invalid.
|
||||
2. Subsequent GPU operations silently fail or throw.
|
||||
3. The canvas may go black or freeze on the last rendered frame.
|
||||
4. `GPUDevice.lost` promise resolves with a `GPUDeviceLostInfo` object.
|
||||
|
||||
## Detection
|
||||
|
||||
### WebGPU Native API
|
||||
|
||||
```ts
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
const device = await adapter.requestDevice();
|
||||
|
||||
device.lost.then((info) => {
|
||||
console.error(`GPU device lost: ${info.reason}`, info.message);
|
||||
if (info.reason === 'destroyed') {
|
||||
// Intentional -- app called device.destroy()
|
||||
} else {
|
||||
// Unexpected loss -- attempt recovery
|
||||
recoverDevice();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Three.js WebGPURenderer
|
||||
|
||||
Three.js wraps the device internally. You can detect loss by listening on
|
||||
the renderer's underlying device:
|
||||
|
||||
```ts
|
||||
import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
|
||||
|
||||
async function setupRenderer(canvas: HTMLCanvasElement) {
|
||||
const renderer = new WebGPURenderer({ canvas, antialias: true });
|
||||
await renderer.init();
|
||||
|
||||
// Access the underlying GPUDevice
|
||||
const backend = renderer.backend;
|
||||
const device = backend.device;
|
||||
|
||||
device.lost.then((info) => {
|
||||
console.error('WebGPU device lost:', info.reason, info.message);
|
||||
handleDeviceLoss(renderer);
|
||||
});
|
||||
|
||||
return renderer;
|
||||
}
|
||||
```
|
||||
|
||||
## React Error Boundary for Device Loss
|
||||
|
||||
Wrap your WebGPU canvas in an error boundary that catches device-loss errors
|
||||
and offers recovery.
|
||||
|
||||
### DeviceLossErrorBoundary Component
|
||||
|
||||
```tsx
|
||||
import { Component, type ReactNode } from 'react';
|
||||
|
||||
interface DeviceLossState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
recovering: boolean;
|
||||
}
|
||||
|
||||
interface DeviceLossProps {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
onDeviceLost?: (error: Error) => void;
|
||||
maxRetries?: number;
|
||||
}
|
||||
|
||||
export class DeviceLossErrorBoundary extends Component<
|
||||
DeviceLossProps,
|
||||
DeviceLossState
|
||||
> {
|
||||
private retryCount = 0;
|
||||
|
||||
constructor(props: DeviceLossProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null, recovering: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): Partial<DeviceLossState> {
|
||||
// Check if the error is GPU-related
|
||||
const isGPUError =
|
||||
error.message.includes('device') ||
|
||||
error.message.includes('GPU') ||
|
||||
error.message.includes('context lost') ||
|
||||
error.message.includes('destroyed') ||
|
||||
error.name === 'GPUDeviceLostError';
|
||||
|
||||
if (isGPUError) {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
// Re-throw non-GPU errors
|
||||
throw error;
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error) {
|
||||
this.props.onDeviceLost?.(error);
|
||||
}
|
||||
|
||||
handleRetry = () => {
|
||||
const maxRetries = this.props.maxRetries ?? 3;
|
||||
|
||||
if (this.retryCount >= maxRetries) {
|
||||
return; // Give up after max retries
|
||||
}
|
||||
|
||||
this.retryCount++;
|
||||
this.setState({ hasError: false, error: null, recovering: true });
|
||||
|
||||
// Small delay to let GPU recover
|
||||
setTimeout(() => {
|
||||
this.setState({ recovering: false });
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20, textAlign: 'center' }}>
|
||||
<h3>GPU Connection Lost</h3>
|
||||
<p>The graphics device became unavailable.</p>
|
||||
<button onClick={this.handleRetry}>
|
||||
Retry ({this.retryCount}/{this.props.maxRetries ?? 3})
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.recovering) {
|
||||
return <div style={{ padding: 20 }}>Reconnecting to GPU...</div>;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage in R3F
|
||||
|
||||
```tsx
|
||||
function App() {
|
||||
return (
|
||||
<DeviceLossErrorBoundary
|
||||
maxRetries={3}
|
||||
onDeviceLost={(err) => {
|
||||
analytics.track('gpu_device_lost', { message: err.message });
|
||||
}}
|
||||
fallback={
|
||||
<div>
|
||||
<p>3D rendering is unavailable.</p>
|
||||
<p>Try refreshing the page or switching to a different browser.</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<WebGPUCanvas>
|
||||
<Scene />
|
||||
</WebGPUCanvas>
|
||||
</DeviceLossErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Recovery Strategies
|
||||
|
||||
### Strategy 1: Full Remount (Simple, Reliable)
|
||||
|
||||
Unmount and remount the entire Canvas. This destroys all Three.js state
|
||||
and recreates it from scratch.
|
||||
|
||||
```tsx
|
||||
function RecoverableScene() {
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
const handleDeviceLoss = useCallback(() => {
|
||||
// Increment key to force full remount
|
||||
setTimeout(() => setKey((k) => k + 1), 1000);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DeviceLossErrorBoundary
|
||||
key={key}
|
||||
onDeviceLost={handleDeviceLoss}
|
||||
>
|
||||
<WebGPUCanvas>
|
||||
<Scene />
|
||||
</WebGPUCanvas>
|
||||
</DeviceLossErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Strategy 2: Renderer Recreation (Preserves Scene Graph)
|
||||
|
||||
More sophisticated -- recreate only the renderer while keeping the
|
||||
scene graph intact. This requires accessing Three.js internals.
|
||||
|
||||
```tsx
|
||||
function useDeviceLossRecovery() {
|
||||
const { gl, set } = useThree();
|
||||
|
||||
useEffect(() => {
|
||||
const renderer = gl as unknown as WebGPURenderer;
|
||||
const backend = renderer.backend;
|
||||
|
||||
if (!backend?.device) return;
|
||||
|
||||
backend.device.lost.then(async (info) => {
|
||||
console.warn('GPU device lost, attempting recovery...');
|
||||
|
||||
// Create a new renderer on the same canvas
|
||||
const canvas = renderer.domElement;
|
||||
const newRenderer = new WebGPURenderer({
|
||||
canvas,
|
||||
antialias: true,
|
||||
});
|
||||
await newRenderer.init();
|
||||
|
||||
// Replace the renderer in R3F state
|
||||
set({ gl: newRenderer as unknown as THREE.WebGLRenderer });
|
||||
});
|
||||
}, [gl, set]);
|
||||
}
|
||||
```
|
||||
|
||||
### Strategy 3: Graceful Degradation to WebGL
|
||||
|
||||
Fall back to WebGL when WebGPU fails:
|
||||
|
||||
```tsx
|
||||
async function createRendererWithFallback(canvas: HTMLCanvasElement) {
|
||||
// Try WebGPU first
|
||||
if (navigator.gpu) {
|
||||
try {
|
||||
const renderer = new WebGPURenderer({ canvas, antialias: true });
|
||||
await renderer.init();
|
||||
return { renderer, backend: 'webgpu' as const };
|
||||
} catch {
|
||||
console.warn('WebGPU init failed, falling back to WebGL');
|
||||
}
|
||||
}
|
||||
|
||||
// WebGL fallback
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
|
||||
return { renderer, backend: 'webgl' as const };
|
||||
}
|
||||
```
|
||||
|
||||
## State Preservation
|
||||
|
||||
When recovering from device loss, you may want to preserve application state
|
||||
(camera position, animation progress, user selections). Store critical state
|
||||
outside the Three.js scene graph.
|
||||
|
||||
```tsx
|
||||
interface SceneState {
|
||||
cameraPosition: [number, number, number];
|
||||
cameraTarget: [number, number, number];
|
||||
animationProgress: number;
|
||||
selectedObjectId: string | null;
|
||||
}
|
||||
|
||||
function useSceneStateBackup() {
|
||||
const stateRef = useRef<SceneState>({
|
||||
cameraPosition: [0, 5, 10],
|
||||
cameraTarget: [0, 0, 0],
|
||||
animationProgress: 0,
|
||||
selectedObjectId: null,
|
||||
});
|
||||
|
||||
// Periodically snapshot camera state
|
||||
useFrame(({ camera }) => {
|
||||
stateRef.current.cameraPosition = camera.position.toArray();
|
||||
});
|
||||
|
||||
return stateRef;
|
||||
}
|
||||
```
|
||||
|
||||
After recovery, restore from the backup:
|
||||
|
||||
```tsx
|
||||
function Scene({ initialState }: { initialState?: SceneState }) {
|
||||
const cameraRef = useRef<THREE.PerspectiveCamera>(null!);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialState) {
|
||||
cameraRef.current.position.fromArray(initialState.cameraPosition);
|
||||
}
|
||||
}, [initialState]);
|
||||
|
||||
return (
|
||||
<perspectiveCamera ref={cameraRef} />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Compute Shader Buffer Recovery
|
||||
|
||||
Storage buffers for compute shaders are lost along with the device. If the
|
||||
simulation state is important, periodically snapshot it:
|
||||
|
||||
```tsx
|
||||
function useComputeStateBackup(
|
||||
renderer: WebGPURenderer,
|
||||
storageBuffer: StorageBuffer,
|
||||
intervalMs = 5000
|
||||
) {
|
||||
const backupRef = useRef<Float32Array | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const data = await renderer.getArrayBufferAsync(storageBuffer.value);
|
||||
backupRef.current = new Float32Array(data);
|
||||
} catch {
|
||||
// Device may already be lost
|
||||
}
|
||||
}, intervalMs);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [renderer, storageBuffer, intervalMs]);
|
||||
|
||||
return backupRef;
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Device Loss
|
||||
|
||||
### Simulating Device Loss in Tests
|
||||
|
||||
```ts
|
||||
// In a Vitest test
|
||||
describe('device loss recovery', () => {
|
||||
it('should remount canvas on device loss', async () => {
|
||||
const onDeviceLost = vi.fn();
|
||||
|
||||
const { rerender } = render(
|
||||
<DeviceLossErrorBoundary onDeviceLost={onDeviceLost}>
|
||||
<WebGPUCanvas>
|
||||
<Scene />
|
||||
</WebGPUCanvas>
|
||||
</DeviceLossErrorBoundary>
|
||||
);
|
||||
|
||||
// Simulate device loss by triggering error boundary
|
||||
// In a real scenario, the GPU device.lost promise resolves
|
||||
// For testing, throw a simulated GPU error
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Chrome DevTools
|
||||
|
||||
1. Open Chrome DevTools.
|
||||
2. Go to `chrome://gpu` to see GPU status.
|
||||
3. Use `chrome://flags/#force-gpu-rasterization` for GPU stress testing.
|
||||
4. The WebGPU spec provides `device.destroy()` for testing intentional loss.
|
||||
|
||||
### Programmatic Device Destruction
|
||||
|
||||
```ts
|
||||
// Intentionally destroy device for testing recovery
|
||||
function testDeviceLoss(renderer: WebGPURenderer) {
|
||||
const device = renderer.backend.device;
|
||||
device.destroy(); // Triggers device.lost promise
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with @suaveplan/r3f-performance
|
||||
|
||||
The `@suaveplan/r3f-performance` package monitors frame timing and GPU
|
||||
utilization. On device loss, the performance monitor should:
|
||||
|
||||
1. Detect the anomaly (sudden drop to 0 FPS or error events).
|
||||
2. Emit a `'device-lost'` event via the performance event bus.
|
||||
3. Pause metric collection during recovery.
|
||||
4. Resume collection after the new device is initialized.
|
||||
5. Log a recovery marker in the performance timeline.
|
||||
|
||||
```ts
|
||||
// Performance monitor integration
|
||||
performanceMonitor.on('device-lost', () => {
|
||||
performanceMonitor.pause();
|
||||
});
|
||||
|
||||
performanceMonitor.on('device-recovered', () => {
|
||||
performanceMonitor.resume();
|
||||
performanceMonitor.addMarker('gpu-recovery');
|
||||
});
|
||||
```
|
||||
|
||||
## Checklist
|
||||
|
||||
When implementing WebGPU in a Suaveplan R3F component:
|
||||
|
||||
- [ ] Wrap the Canvas in `DeviceLossErrorBoundary`
|
||||
- [ ] Provide meaningful fallback UI
|
||||
- [ ] Preserve important scene state outside the GPU
|
||||
- [ ] Test with `device.destroy()` during development
|
||||
- [ ] Log device loss events to analytics
|
||||
- [ ] Consider WebGL fallback for critical features
|
||||
- [ ] Handle async renderer init errors
|
||||
- [ ] Clean up GPU resources in React effect cleanup functions
|
||||
@@ -0,0 +1,370 @@
|
||||
# TSL Node Materials in React Three Fiber
|
||||
|
||||
Node materials are the primary way to use TSL. Each material type extends a
|
||||
standard Three.js material with node slots that accept TSL expressions.
|
||||
|
||||
## Material Types
|
||||
|
||||
| Material | Use Case |
|
||||
|---|---|
|
||||
| `MeshStandardNodeMaterial` | PBR with metalness/roughness workflow |
|
||||
| `MeshPhysicalNodeMaterial` | Extended PBR (clearcoat, transmission, sheen) |
|
||||
| `MeshBasicNodeMaterial` | Unlit, flat shading |
|
||||
| `MeshLambertNodeMaterial` | Non-PBR diffuse lighting |
|
||||
| `MeshPhongNodeMaterial` | Non-PBR specular lighting |
|
||||
| `SpriteNodeMaterial` | Billboard sprites |
|
||||
| `LineBasicNodeMaterial` | Lines |
|
||||
| `PointsNodeMaterial` | Point clouds |
|
||||
| `MeshSSSNodeMaterial` | Subsurface scattering |
|
||||
| `VolumeNodeMaterial` | Volumetric rendering |
|
||||
|
||||
All are imported from `'three/webgpu'`.
|
||||
|
||||
## Node Slots
|
||||
|
||||
### MeshStandardNodeMaterial Slots
|
||||
|
||||
```ts
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
|
||||
// Color & appearance
|
||||
mat.colorNode // vec3 -- base color (replaces .color + .map)
|
||||
mat.opacityNode // float -- alpha (replaces .opacity + .alphaMap)
|
||||
mat.emissiveNode // vec3 -- emissive light
|
||||
mat.metalnessNode // float -- 0 = dielectric, 1 = metal
|
||||
mat.roughnessNode // float -- 0 = mirror, 1 = diffuse
|
||||
mat.aoNode // float -- ambient occlusion
|
||||
|
||||
// Normals
|
||||
mat.normalNode // vec3 -- replaces normalMap
|
||||
|
||||
// Vertex
|
||||
mat.positionNode // vec3 -- vertex displacement (object space)
|
||||
|
||||
// Output (advanced)
|
||||
mat.fragmentNode // vec4 -- replaces entire fragment (no lighting)
|
||||
mat.outputNode // vec4 -- replaces final output (after lighting)
|
||||
mat.shadowNode // vec4 -- custom shadow contribution
|
||||
```
|
||||
|
||||
### MeshPhysicalNodeMaterial Additional Slots
|
||||
|
||||
```ts
|
||||
const mat = new MeshPhysicalNodeMaterial();
|
||||
|
||||
// All MeshStandardNodeMaterial slots plus:
|
||||
mat.clearcoatNode // float -- clearcoat intensity
|
||||
mat.clearcoatRoughnessNode // float -- clearcoat roughness
|
||||
mat.clearcoatNormalNode // vec3 -- clearcoat normal
|
||||
mat.sheenNode // vec3 -- sheen color
|
||||
mat.sheenRoughnessNode // float -- sheen roughness
|
||||
mat.iorNode // float -- index of refraction
|
||||
mat.transmissionNode // float -- transmission (glass)
|
||||
mat.thicknessNode // float -- volume thickness
|
||||
mat.attenuationColorNode // vec3 -- volume color
|
||||
mat.attenuationDistanceNode // float -- volume distance
|
||||
mat.specularColorNode // vec3 -- specular tint
|
||||
mat.specularIntensityNode // float -- specular intensity
|
||||
mat.iridescenceNode // float -- iridescence intensity
|
||||
mat.iridescenceIORNode // float -- iridescence IOR
|
||||
mat.anisotropyNode // float -- anisotropy strength
|
||||
```
|
||||
|
||||
## R3F Declarative Patterns
|
||||
|
||||
### Pattern 1: useMemo + primitive (Recommended)
|
||||
|
||||
The standard approach. Create the material in `useMemo`, attach via `<primitive>`.
|
||||
|
||||
```tsx
|
||||
import { useMemo } from 'react';
|
||||
import { MeshStandardNodeMaterial } from 'three/webgpu';
|
||||
import { color, sin, timerLocal, mix } from 'three/tsl';
|
||||
|
||||
function GlowingSphere() {
|
||||
const material = useMemo(() => {
|
||||
const time = timerLocal();
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.colorNode = color(0x3b82f6);
|
||||
mat.emissiveNode = color(0x60a5fa).mul(sin(time.mul(2)).mul(0.5).add(0.5));
|
||||
mat.roughnessNode = float(0.3);
|
||||
mat.metalnessNode = float(0.8);
|
||||
return mat;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: extend() for JSX Syntax
|
||||
|
||||
Register the material so R3F recognizes it as a JSX element. Note that node
|
||||
properties (colorNode, etc.) cannot be set as JSX props -- only standard
|
||||
Three.js properties (color, roughness, etc.) work declaratively.
|
||||
|
||||
```tsx
|
||||
import { extend } from '@react-three/fiber';
|
||||
import { MeshStandardNodeMaterial } from 'three/webgpu';
|
||||
|
||||
extend({ MeshStandardNodeMaterial });
|
||||
|
||||
// JSX usage (standard props only -- no node props)
|
||||
<meshStandardNodeMaterial color="hotpink" roughness={0.5} />
|
||||
```
|
||||
|
||||
This is useful for simple materials where you do not need TSL nodes.
|
||||
|
||||
### Pattern 3: Custom Material Hook
|
||||
|
||||
For materials used across multiple components, create a hook:
|
||||
|
||||
```tsx
|
||||
function useHologramMaterial(baseColor: number = 0x00ff88) {
|
||||
return useMemo(() => {
|
||||
const time = timerLocal();
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.transparent = true;
|
||||
mat.side = THREE.DoubleSide;
|
||||
|
||||
const scanline = sin(positionLocal.y.mul(50).add(time.mul(5)))
|
||||
.mul(0.5).add(0.5);
|
||||
const fresnel = float(1).sub(normalView.z.abs()).pow(2);
|
||||
|
||||
mat.colorNode = color(baseColor);
|
||||
mat.opacityNode = scanline.mul(0.5).add(fresnel.mul(0.5));
|
||||
mat.emissiveNode = color(baseColor).mul(fresnel);
|
||||
|
||||
return mat;
|
||||
}, [baseColor]);
|
||||
}
|
||||
|
||||
function HologramMesh() {
|
||||
const material = useHologramMaterial(0x00ff88);
|
||||
return (
|
||||
<mesh>
|
||||
<torusKnotGeometry args={[1, 0.3, 128, 32]} />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Vertex Displacement
|
||||
|
||||
Use `positionNode` to displace vertices on the GPU:
|
||||
|
||||
```tsx
|
||||
function DisplacedPlane() {
|
||||
const uAmplitude = useMemo(() => uniform(0.5), []);
|
||||
|
||||
const material = useMemo(() => {
|
||||
const time = timerLocal();
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
|
||||
const displacement = mx_noise_float(
|
||||
positionLocal.xz.mul(2.0).add(time.mul(0.5))
|
||||
).mul(uAmplitude);
|
||||
|
||||
mat.positionNode = positionLocal.add(
|
||||
normalLocal.mul(displacement)
|
||||
);
|
||||
mat.colorNode = mix(
|
||||
color(0x1e3a5f),
|
||||
color(0x7dd3fc),
|
||||
displacement.div(uAmplitude).mul(0.5).add(0.5)
|
||||
);
|
||||
|
||||
return mat;
|
||||
}, [uAmplitude]);
|
||||
|
||||
return (
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
||||
<planeGeometry args={[10, 10, 256, 256]} />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Glass / Transmission Material
|
||||
|
||||
```tsx
|
||||
function GlassSphere() {
|
||||
const material = useMemo(() => {
|
||||
const mat = new MeshPhysicalNodeMaterial();
|
||||
mat.colorNode = color(0xffffff);
|
||||
mat.roughnessNode = float(0.05);
|
||||
mat.metalnessNode = float(0.0);
|
||||
mat.transmissionNode = float(0.95);
|
||||
mat.thicknessNode = float(0.5);
|
||||
mat.iorNode = float(1.5);
|
||||
return mat;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Texture-Based Materials
|
||||
|
||||
Combine texture sampling with procedural effects:
|
||||
|
||||
```tsx
|
||||
import { useTexture } from '@react-three/drei';
|
||||
import { texture, mix, mx_noise_float } from 'three/tsl';
|
||||
|
||||
function WeatheredWall() {
|
||||
const [diffuse, normal, roughnessMap] = useTexture([
|
||||
'/textures/brick_diffuse.jpg',
|
||||
'/textures/brick_normal.jpg',
|
||||
'/textures/brick_roughness.jpg',
|
||||
]);
|
||||
|
||||
const material = useMemo(() => {
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
const time = timerLocal();
|
||||
|
||||
const baseTex = texture(diffuse);
|
||||
const noise = mx_noise_float(uv().mul(10).add(time.mul(0.1)));
|
||||
|
||||
// Blend texture with procedural dirt
|
||||
mat.colorNode = mix(baseTex, color(0x3d2b1f), noise.mul(0.3));
|
||||
mat.normalNode = texture(normal);
|
||||
mat.roughnessNode = texture(roughnessMap).r.add(noise.mul(0.1));
|
||||
|
||||
return mat;
|
||||
}, [diffuse, normal, roughnessMap]);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<planeGeometry args={[5, 5]} />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Sprite and Points Materials
|
||||
|
||||
### SpriteNodeMaterial
|
||||
|
||||
```tsx
|
||||
function CustomSprite() {
|
||||
const material = useMemo(() => {
|
||||
const mat = new SpriteNodeMaterial();
|
||||
mat.colorNode = color(0xffaa00);
|
||||
mat.opacityNode = float(1).sub(length(uv().sub(0.5)).mul(2)).saturate();
|
||||
return mat;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<sprite>
|
||||
<primitive object={material} attach="material" />
|
||||
</sprite>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### PointsNodeMaterial
|
||||
|
||||
```tsx
|
||||
function ParticleField() {
|
||||
const material = useMemo(() => {
|
||||
const mat = new PointsNodeMaterial();
|
||||
mat.sizeNode = float(5.0);
|
||||
mat.colorNode = mix(color(0xff0000), color(0x0000ff), uv().x);
|
||||
return mat;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<points>
|
||||
<bufferGeometry>
|
||||
{/* attach position attribute */}
|
||||
</bufferGeometry>
|
||||
<primitive object={material} attach="material" />
|
||||
</points>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with @suaveplan/r3f-shaders
|
||||
|
||||
The existing package at `packages/browser/r3f-shaders` stores GLSL shaders as
|
||||
string exports. TSL materials should be added under a new `src/tsl/` directory:
|
||||
|
||||
```text
|
||||
packages/browser/r3f-shaders/src/
|
||||
shaders/ # Existing GLSL string shaders
|
||||
background/
|
||||
parallax/
|
||||
tsl/ # New TSL node material factories
|
||||
hologram/
|
||||
hologram.ts
|
||||
hologram.test.ts
|
||||
hologram.types.ts
|
||||
hologram.md
|
||||
glass/
|
||||
glass.ts
|
||||
glass.test.ts
|
||||
glass.types.ts
|
||||
glass.md
|
||||
```
|
||||
|
||||
### TSL Material Factory Convention
|
||||
|
||||
Export factory functions that return configured node materials:
|
||||
|
||||
```ts
|
||||
// src/tsl/hologram/hologram.ts
|
||||
import { MeshStandardNodeMaterial } from 'three/webgpu';
|
||||
import { color, sin, timerLocal, float } from 'three/tsl';
|
||||
import type { HologramOptions } from './hologram.types.js';
|
||||
|
||||
export function createHologramMaterial(options: HologramOptions = {}) {
|
||||
const { baseColor = 0x00ff88, scanlineFreq = 50, speed = 5 } = options;
|
||||
const time = timerLocal();
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.transparent = true;
|
||||
|
||||
const scanline = sin(positionLocal.y.mul(scanlineFreq).add(time.mul(speed)))
|
||||
.mul(0.5).add(0.5);
|
||||
|
||||
mat.colorNode = color(baseColor);
|
||||
mat.opacityNode = scanline;
|
||||
mat.emissiveNode = color(baseColor);
|
||||
|
||||
return mat;
|
||||
}
|
||||
```
|
||||
|
||||
## Material Disposal
|
||||
|
||||
R3F automatically disposes materials attached via `<primitive>` when the
|
||||
component unmounts. For materials stored in refs or external state:
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
material.dispose();
|
||||
};
|
||||
}, [material]);
|
||||
```
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- Node materials compile shaders on first render. Expect a one-frame stall.
|
||||
- Reuse materials across meshes when possible (same instance, different meshes).
|
||||
- `timerLocal()` is cheaper than uniform + useFrame for time-based animation.
|
||||
- Vertex displacement via `positionNode` is entirely GPU-side -- no JS overhead.
|
||||
- For `@suaveplan/r3f-performance`, node materials work with the existing
|
||||
LOD and quality tier plugins without modification.
|
||||
@@ -0,0 +1,400 @@
|
||||
# Post-Processing with TSL in React Three Fiber
|
||||
|
||||
Three.js provides a node-based post-processing pipeline for the WebGPU backend.
|
||||
Effects are composed as TSL node chains, replacing the older EffectComposer/Pass
|
||||
pattern.
|
||||
|
||||
## PostProcessing Class
|
||||
|
||||
The core class that manages the effect chain:
|
||||
|
||||
```ts
|
||||
import { PostProcessing } from 'three/addons/renderers/common/PostProcessing.js';
|
||||
```
|
||||
|
||||
This class takes a renderer and an `outputNode` -- a TSL node graph that
|
||||
processes the rendered scene into the final framebuffer.
|
||||
|
||||
## R3F Integration Pattern
|
||||
|
||||
Since R3F manages the render loop, you need to hook into it at the right level.
|
||||
The PostProcessing class replaces R3F's default render call.
|
||||
|
||||
### Pattern 1: Manual Render with useFrame
|
||||
|
||||
```tsx
|
||||
import { useRef, useMemo, useEffect } from 'react';
|
||||
import { useFrame, useThree } from '@react-three/fiber';
|
||||
import { PostProcessing } from 'three/addons/renderers/common/PostProcessing.js';
|
||||
import { pass, bloom, fxaa } from 'three/tsl';
|
||||
|
||||
function WebGPUPostProcessing() {
|
||||
const { gl, scene, camera } = useThree();
|
||||
const postRef = useRef<PostProcessing | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const postProcessing = new PostProcessing(gl);
|
||||
const scenePass = pass(scene, camera);
|
||||
|
||||
postProcessing.outputNode = scenePass
|
||||
.pipe(bloom({ strength: 0.5, radius: 0.4, threshold: 0.85 }))
|
||||
.pipe(fxaa());
|
||||
|
||||
postRef.current = postProcessing;
|
||||
|
||||
return () => {
|
||||
// Cleanup if needed
|
||||
postRef.current = null;
|
||||
};
|
||||
}, [gl, scene, camera]);
|
||||
|
||||
useFrame(() => {
|
||||
if (postRef.current) {
|
||||
postRef.current.render();
|
||||
}
|
||||
}, 1); // Priority 1 to run after default scene updates
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Disable R3F Auto-Render
|
||||
|
||||
For full control over rendering:
|
||||
|
||||
```tsx
|
||||
<Canvas
|
||||
gl={(canvas) => createWebGPURenderer(canvas)}
|
||||
frameloop="never" // Disable auto-render
|
||||
>
|
||||
<Scene />
|
||||
<ManualRenderLoop />
|
||||
</Canvas>
|
||||
|
||||
function ManualRenderLoop() {
|
||||
const { gl, scene, camera, invalidate } = useThree();
|
||||
const postRef = useRef<PostProcessing | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const post = new PostProcessing(gl);
|
||||
const scenePass = pass(scene, camera);
|
||||
post.outputNode = scenePass.pipe(bloom({ strength: 1.0 }));
|
||||
postRef.current = post;
|
||||
}, [gl, scene, camera]);
|
||||
|
||||
useFrame(() => {
|
||||
postRef.current?.render();
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## Built-in Effects
|
||||
|
||||
### pass() -- Scene Render Pass
|
||||
|
||||
The entry point for all post-processing chains:
|
||||
|
||||
```ts
|
||||
import { pass } from 'three/tsl';
|
||||
|
||||
const scenePass = pass(scene, camera);
|
||||
|
||||
// Access rendered textures
|
||||
const beautyTexture = scenePass.getTextureNode(); // Color
|
||||
const depthTexture = scenePass.getDepthNode(); // Depth
|
||||
const normalTexture = scenePass.getNormalNode(); // View-space normals (if enabled)
|
||||
```
|
||||
|
||||
### bloom()
|
||||
|
||||
HDR bloom effect:
|
||||
|
||||
```ts
|
||||
import { bloom } from 'three/tsl';
|
||||
|
||||
scenePass.pipe(bloom({
|
||||
strength: 0.5, // Bloom intensity (0..2+)
|
||||
radius: 0.4, // Blur radius (0..1)
|
||||
threshold: 0.85, // Luminance threshold (0..1)
|
||||
}));
|
||||
```
|
||||
|
||||
### fxaa()
|
||||
|
||||
Fast approximate anti-aliasing:
|
||||
|
||||
```ts
|
||||
import { fxaa } from 'three/tsl';
|
||||
|
||||
scenePass.pipe(fxaa());
|
||||
```
|
||||
|
||||
### gaussianBlur()
|
||||
|
||||
Gaussian blur with configurable kernel:
|
||||
|
||||
```ts
|
||||
import { gaussianBlur } from 'three/tsl';
|
||||
|
||||
scenePass.pipe(gaussianBlur({
|
||||
sigma: 2.0, // Blur sigma
|
||||
direction: vec2(1, 0), // Horizontal pass
|
||||
}));
|
||||
```
|
||||
|
||||
### dotScreen()
|
||||
|
||||
Halftone dot pattern:
|
||||
|
||||
```ts
|
||||
import { dotScreen } from 'three/tsl';
|
||||
|
||||
scenePass.pipe(dotScreen({
|
||||
scale: 4.0,
|
||||
angle: 0.5,
|
||||
}));
|
||||
```
|
||||
|
||||
### film()
|
||||
|
||||
Film grain effect:
|
||||
|
||||
```ts
|
||||
import { film } from 'three/tsl';
|
||||
|
||||
scenePass.pipe(film({
|
||||
intensity: 0.35,
|
||||
grayscale: false,
|
||||
}));
|
||||
```
|
||||
|
||||
### sobel()
|
||||
|
||||
Edge detection:
|
||||
|
||||
```ts
|
||||
import { sobel } from 'three/tsl';
|
||||
|
||||
scenePass.pipe(sobel());
|
||||
```
|
||||
|
||||
## Depth-Based Effects
|
||||
|
||||
### SSAO (Screen-Space Ambient Occlusion)
|
||||
|
||||
```ts
|
||||
import { ao } from 'three/tsl';
|
||||
|
||||
const scenePass = pass(scene, camera);
|
||||
const depthNode = scenePass.getDepthNode();
|
||||
const normalNode = scenePass.getNormalNode();
|
||||
|
||||
const ssao = ao({
|
||||
depthNode,
|
||||
normalNode,
|
||||
radius: 0.5,
|
||||
intensity: 1.0,
|
||||
samples: 16,
|
||||
});
|
||||
|
||||
// Multiply AO into the beauty pass
|
||||
postProcessing.outputNode = scenePass.getTextureNode().mul(ssao);
|
||||
```
|
||||
|
||||
### Depth of Field
|
||||
|
||||
```ts
|
||||
import { dof } from 'three/tsl';
|
||||
|
||||
const scenePass = pass(scene, camera);
|
||||
|
||||
postProcessing.outputNode = scenePass.pipe(dof({
|
||||
focus: 5.0, // Focus distance
|
||||
aperture: 0.025, // Aperture size
|
||||
maxblur: 0.01, // Maximum blur
|
||||
}));
|
||||
```
|
||||
|
||||
## Custom Post-Processing Effects
|
||||
|
||||
### Simple Full-Screen Effect
|
||||
|
||||
```ts
|
||||
import { Fn, screenCoordinate, screenSize, texture } from 'three/tsl';
|
||||
|
||||
const vignetteEffect = Fn(([inputTexture]) => {
|
||||
const uv = screenCoordinate.div(screenSize);
|
||||
const center = vec2(0.5, 0.5);
|
||||
const dist = distance(uv, center);
|
||||
const vignette = smoothstep(float(0.8), float(0.4), dist);
|
||||
|
||||
return inputTexture.mul(vignette);
|
||||
});
|
||||
|
||||
const scenePass = pass(scene, camera);
|
||||
postProcessing.outputNode = vignetteEffect(scenePass.getTextureNode());
|
||||
```
|
||||
|
||||
### Color Grading
|
||||
|
||||
```ts
|
||||
const colorGrade = Fn(([inputTexture]) => {
|
||||
const col = inputTexture.toVar();
|
||||
|
||||
// Lift / Gamma / Gain
|
||||
const lift = vec3(0.0, 0.0, 0.05); // Shadow tint
|
||||
const gamma = vec3(1.0, 0.95, 0.9); // Midtone tint
|
||||
const gain = vec3(1.1, 1.05, 1.0); // Highlight tint
|
||||
|
||||
col.assign(col.mul(gain).add(lift));
|
||||
col.assign(pow(col, vec3(1.0).div(gamma)));
|
||||
|
||||
// Contrast
|
||||
col.assign(col.sub(0.5).mul(1.2).add(0.5));
|
||||
|
||||
// Saturation
|
||||
const lum = dot(col, vec3(0.2126, 0.7152, 0.0722));
|
||||
col.assign(mix(vec3(lum), col, float(1.3)));
|
||||
|
||||
return vec4(col, float(1.0));
|
||||
});
|
||||
```
|
||||
|
||||
### Chromatic Aberration
|
||||
|
||||
```ts
|
||||
const chromaticAberration = Fn(([inputTexture]) => {
|
||||
const uv = screenCoordinate.div(screenSize);
|
||||
const center = vec2(0.5, 0.5);
|
||||
const dir = uv.sub(center);
|
||||
const offset = float(0.003);
|
||||
|
||||
const r = texture(inputTexture, uv.add(dir.mul(offset))).r;
|
||||
const g = texture(inputTexture, uv).g;
|
||||
const b = texture(inputTexture, uv.sub(dir.mul(offset))).b;
|
||||
|
||||
return vec4(r, g, b, float(1.0));
|
||||
});
|
||||
```
|
||||
|
||||
## Effect Chaining
|
||||
|
||||
Effects can be chained with `.pipe()`:
|
||||
|
||||
```ts
|
||||
postProcessing.outputNode = scenePass
|
||||
.pipe(bloom({ strength: 0.5, threshold: 0.8 }))
|
||||
.pipe(fxaa())
|
||||
.pipe(film({ intensity: 0.15 }));
|
||||
```
|
||||
|
||||
Or composed manually:
|
||||
|
||||
```ts
|
||||
const beauty = scenePass.getTextureNode();
|
||||
const bloomed = bloom({ strength: 0.5 })(beauty);
|
||||
const graded = colorGrade(bloomed);
|
||||
const final = vignetteEffect(graded);
|
||||
|
||||
postProcessing.outputNode = final;
|
||||
```
|
||||
|
||||
## Uniform-Driven Effects (React State)
|
||||
|
||||
```tsx
|
||||
function DynamicPostFX({ bloomStrength = 0.5 }) {
|
||||
const { gl, scene, camera } = useThree();
|
||||
const uBloom = useMemo(() => uniform(bloomStrength), []);
|
||||
const postRef = useRef<PostProcessing | null>(null);
|
||||
|
||||
// Sync React prop to GPU uniform
|
||||
useEffect(() => {
|
||||
uBloom.value = bloomStrength;
|
||||
}, [bloomStrength, uBloom]);
|
||||
|
||||
useEffect(() => {
|
||||
const post = new PostProcessing(gl);
|
||||
const scenePass = pass(scene, camera);
|
||||
|
||||
post.outputNode = scenePass.pipe(bloom({
|
||||
strength: uBloom,
|
||||
radius: 0.4,
|
||||
threshold: 0.85,
|
||||
}));
|
||||
|
||||
postRef.current = post;
|
||||
}, [gl, scene, camera, uBloom]);
|
||||
|
||||
useFrame(() => {
|
||||
postRef.current?.render();
|
||||
}, 1);
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## R3F Post-Processing Component Pattern
|
||||
|
||||
Encapsulate post-processing into a reusable component:
|
||||
|
||||
```tsx
|
||||
interface PostFXProps {
|
||||
bloomStrength?: number;
|
||||
bloomThreshold?: number;
|
||||
enableFXAA?: boolean;
|
||||
enableFilm?: boolean;
|
||||
filmIntensity?: number;
|
||||
}
|
||||
|
||||
function PostFX({
|
||||
bloomStrength = 0.5,
|
||||
bloomThreshold = 0.85,
|
||||
enableFXAA = true,
|
||||
enableFilm = false,
|
||||
filmIntensity = 0.1,
|
||||
}: PostFXProps) {
|
||||
const { gl, scene, camera } = useThree();
|
||||
const postRef = useRef<PostProcessing | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const post = new PostProcessing(gl);
|
||||
const scenePass = pass(scene, camera);
|
||||
|
||||
let output = scenePass.pipe(bloom({
|
||||
strength: bloomStrength,
|
||||
threshold: bloomThreshold,
|
||||
}));
|
||||
|
||||
if (enableFXAA) output = output.pipe(fxaa());
|
||||
if (enableFilm) output = output.pipe(film({ intensity: filmIntensity }));
|
||||
|
||||
post.outputNode = output;
|
||||
postRef.current = post;
|
||||
}, [gl, scene, camera, bloomStrength, bloomThreshold, enableFXAA, enableFilm, filmIntensity]);
|
||||
|
||||
useFrame(() => {
|
||||
postRef.current?.render();
|
||||
}, 1);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Usage in scene
|
||||
<Canvas gl={(canvas) => createWebGPURenderer(canvas)}>
|
||||
<Scene />
|
||||
<PostFX bloomStrength={0.8} enableFilm />
|
||||
</Canvas>
|
||||
```
|
||||
|
||||
## Performance Notes
|
||||
|
||||
1. Each `.pipe()` adds a full-screen pass. Minimize the chain length.
|
||||
2. SSAO and DOF are the most expensive built-in effects.
|
||||
3. `fxaa()` is cheap -- prefer it over MSAA for WebGPU post-processing.
|
||||
4. Custom effects that only read `screenCoordinate` / `screenSize` are
|
||||
essentially free (single texture fetch per pixel).
|
||||
5. For `@suaveplan/r3f-performance`, post-processing quality can be
|
||||
adjusted dynamically by updating uniforms based on the current
|
||||
performance tier (e.g., reduce bloom samples on low-end devices).
|
||||
@@ -0,0 +1,328 @@
|
||||
# WGSL Integration with TSL
|
||||
|
||||
When TSL's declarative node API is insufficient -- raw GPU intrinsics, shared
|
||||
workgroup memory, complex control flow -- you can drop to raw WGSL using
|
||||
`wgslFn()` and integrate the result back into the TSL graph.
|
||||
|
||||
## wgslFn() Basics
|
||||
|
||||
```ts
|
||||
import { wgslFn, float, vec2, vec3, vec4 } from 'three/tsl';
|
||||
|
||||
const myShaderFn = wgslFn(`
|
||||
fn myFunction(uv: vec2f, time: f32) -> vec4f {
|
||||
let col = vec3f(uv, sin(time) * 0.5 + 0.5);
|
||||
return vec4f(col, 1.0);
|
||||
}
|
||||
`);
|
||||
|
||||
// Call it like a TSL function
|
||||
const result = myShaderFn({ uv: uv(), time: timerLocal() });
|
||||
```
|
||||
|
||||
### Parameter Passing
|
||||
|
||||
Parameters are passed as a named object. The names must match the WGSL
|
||||
function parameter names:
|
||||
|
||||
```ts
|
||||
const sdfShape = wgslFn(`
|
||||
fn sdfShape(pos: vec3f, radius: f32, smoothness: f32) -> f32 {
|
||||
let d = length(pos) - radius;
|
||||
return smoothstep(-smoothness, smoothness, -d);
|
||||
}
|
||||
`);
|
||||
|
||||
// TSL call site
|
||||
const dist = sdfShape({
|
||||
pos: positionLocal,
|
||||
radius: float(1.0),
|
||||
smoothness: float(0.02),
|
||||
});
|
||||
```
|
||||
|
||||
## WGSL Type Reference
|
||||
|
||||
### Scalar Types
|
||||
|
||||
| WGSL | Description | TSL Equivalent |
|
||||
|---|---|---|
|
||||
| `f32` | 32-bit float | `float()` |
|
||||
| `i32` | 32-bit signed int | `int()` |
|
||||
| `u32` | 32-bit unsigned int | `uint()` |
|
||||
| `bool` | Boolean | `bool()` |
|
||||
| `f16` | 16-bit float (requires extension) | -- |
|
||||
|
||||
### Vector Types
|
||||
|
||||
| WGSL | Description | TSL Equivalent |
|
||||
|---|---|---|
|
||||
| `vec2f` | 2-component f32 vector | `vec2()` |
|
||||
| `vec3f` | 3-component f32 vector | `vec3()` |
|
||||
| `vec4f` | 4-component f32 vector | `vec4()` |
|
||||
| `vec2i` | 2-component i32 vector | `ivec2()` |
|
||||
| `vec3i` | 3-component i32 vector | `ivec3()` |
|
||||
| `vec4i` | 4-component i32 vector | `ivec4()` |
|
||||
| `vec2u` | 2-component u32 vector | `uvec2()` |
|
||||
| `vec3u` | 3-component u32 vector | `uvec3()` |
|
||||
| `vec4u` | 4-component u32 vector | `uvec4()` |
|
||||
|
||||
### Matrix Types
|
||||
|
||||
| WGSL | Description | TSL Equivalent |
|
||||
|---|---|---|
|
||||
| `mat2x2f` | 2x2 float matrix | `mat2()` |
|
||||
| `mat3x3f` | 3x3 float matrix | `mat3()` |
|
||||
| `mat4x4f` | 4x4 float matrix | `mat4()` |
|
||||
|
||||
### Array Types
|
||||
|
||||
```wgsl
|
||||
array<f32, 16> // Fixed-size array of 16 floats
|
||||
array<vec4f, 256> // Fixed-size array of 256 vec4
|
||||
```
|
||||
|
||||
### Struct Types
|
||||
|
||||
```wgsl
|
||||
struct Particle {
|
||||
position: vec3f,
|
||||
velocity: vec3f,
|
||||
life: f32,
|
||||
pad: f32, // Padding for 16-byte alignment
|
||||
}
|
||||
```
|
||||
|
||||
## Hybrid TSL + WGSL Patterns
|
||||
|
||||
### Pattern 1: WGSL Function in TSL Material
|
||||
|
||||
```tsx
|
||||
import { wgslFn, uv, timerLocal, float } from 'three/tsl';
|
||||
import { MeshStandardNodeMaterial } from 'three/webgpu';
|
||||
|
||||
const voronoiNoise = wgslFn(`
|
||||
fn hash2(p: vec2f) -> vec2f {
|
||||
let k = vec2f(0.3183099, 0.3678794);
|
||||
let x = p * k + k.yx;
|
||||
return fract(16.0 * k * fract(x.x * x.y * (x.x + x.y))) * 2.0 - 1.0;
|
||||
}
|
||||
|
||||
fn voronoi(uv: vec2f, time: f32) -> f32 {
|
||||
let cell = floor(uv);
|
||||
let frac = fract(uv);
|
||||
var minDist: f32 = 1.0;
|
||||
|
||||
for (var y: i32 = -1; y <= 1; y++) {
|
||||
for (var x: i32 = -1; x <= 1; x++) {
|
||||
let neighbor = vec2f(f32(x), f32(y));
|
||||
let point = hash2(cell + neighbor);
|
||||
let animPoint = 0.5 + 0.5 * sin(time + 6.2831 * point);
|
||||
let diff = neighbor + animPoint - frac;
|
||||
let dist = length(diff);
|
||||
minDist = min(minDist, dist);
|
||||
}
|
||||
}
|
||||
|
||||
return minDist;
|
||||
}
|
||||
`);
|
||||
|
||||
function VoronoiMesh() {
|
||||
const material = useMemo(() => {
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
const noise = voronoiNoise({
|
||||
uv: uv().mul(5),
|
||||
time: timerLocal(),
|
||||
});
|
||||
mat.colorNode = mix(color(0x1e293b), color(0x38bdf8), noise);
|
||||
mat.roughnessNode = noise.oneMinus().mul(0.8).add(0.1);
|
||||
return mat;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<mesh>
|
||||
<sphereGeometry args={[1, 64, 64]} />
|
||||
<primitive object={material} attach="material" />
|
||||
</mesh>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: WGSL Compute with TSL Dispatch
|
||||
|
||||
```ts
|
||||
const nbodyCompute = wgslFn(`
|
||||
fn nbodyStep(
|
||||
idx: u32,
|
||||
count: u32,
|
||||
positions: ptr<storage, array<vec4f>, read_write>,
|
||||
velocities: ptr<storage, array<vec4f>, read_write>,
|
||||
dt: f32
|
||||
) {
|
||||
let pos = (*positions)[idx].xyz;
|
||||
var acc = vec3f(0.0);
|
||||
|
||||
for (var i: u32 = 0u; i < count; i++) {
|
||||
if (i == idx) { continue; }
|
||||
let other = (*positions)[i].xyz;
|
||||
let diff = other - pos;
|
||||
let dist2 = dot(diff, diff) + 0.01;
|
||||
acc += normalize(diff) * ((*positions)[i].w / dist2);
|
||||
}
|
||||
|
||||
(*velocities)[idx] = vec4f((*velocities)[idx].xyz + acc * dt, 0.0);
|
||||
(*positions)[idx] = vec4f(pos + (*velocities)[idx].xyz * dt, (*positions)[idx].w);
|
||||
}
|
||||
`);
|
||||
```
|
||||
|
||||
### Pattern 3: WGSL Utility Functions Mixed with TSL
|
||||
|
||||
```ts
|
||||
// Define a WGSL helper
|
||||
const rotateY = wgslFn(`
|
||||
fn rotateY(v: vec3f, angle: f32) -> vec3f {
|
||||
let c = cos(angle);
|
||||
let s = sin(angle);
|
||||
return vec3f(
|
||||
v.x * c + v.z * s,
|
||||
v.y,
|
||||
-v.x * s + v.z * c
|
||||
);
|
||||
}
|
||||
`);
|
||||
|
||||
// Use in TSL material
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
mat.positionNode = rotateY({
|
||||
v: positionLocal,
|
||||
angle: timerLocal(),
|
||||
});
|
||||
```
|
||||
|
||||
## WGSL Built-in Functions
|
||||
|
||||
### Math Functions
|
||||
|
||||
```wgsl
|
||||
abs(x), acos(x), asin(x), atan(x), atan2(y, x)
|
||||
ceil(x), clamp(x, lo, hi), cos(x), cosh(x)
|
||||
cross(a, b), degrees(x), determinant(m)
|
||||
distance(a, b), dot(a, b), exp(x), exp2(x)
|
||||
floor(x), fma(a, b, c), fract(x)
|
||||
inverseSqrt(x), length(v), log(x), log2(x)
|
||||
max(a, b), min(a, b), mix(a, b, t)
|
||||
normalize(v), pow(x, y), radians(x)
|
||||
reflect(v, n), refract(v, n, eta), round(x)
|
||||
saturate(x), sign(x), sin(x), sinh(x)
|
||||
smoothstep(lo, hi, x), sqrt(x), step(edge, x)
|
||||
tan(x), tanh(x), transpose(m), trunc(x)
|
||||
```
|
||||
|
||||
### Texture Functions
|
||||
|
||||
```wgsl
|
||||
textureSample(tex, sampler, uv)
|
||||
textureSampleLevel(tex, sampler, uv, level)
|
||||
textureSampleGrad(tex, sampler, uv, ddx, ddy)
|
||||
textureLoad(tex, coords, level)
|
||||
textureStore(tex, coords, value)
|
||||
textureDimensions(tex)
|
||||
```
|
||||
|
||||
### Atomic Functions (Compute)
|
||||
|
||||
```wgsl
|
||||
atomicLoad(ptr)
|
||||
atomicStore(ptr, value)
|
||||
atomicAdd(ptr, value)
|
||||
atomicSub(ptr, value)
|
||||
atomicMax(ptr, value)
|
||||
atomicMin(ptr, value)
|
||||
atomicAnd(ptr, value)
|
||||
atomicOr(ptr, value)
|
||||
atomicXor(ptr, value)
|
||||
atomicCompareExchangeWeak(ptr, compare, value)
|
||||
```
|
||||
|
||||
### Synchronization (Compute)
|
||||
|
||||
```wgsl
|
||||
workgroupBarrier() // Sync all invocations in workgroup
|
||||
storageBarrier() // Sync storage memory access
|
||||
textureBarrier() // Sync texture memory access
|
||||
```
|
||||
|
||||
## Memory Layout and Alignment
|
||||
|
||||
WGSL has strict alignment rules. Key points:
|
||||
|
||||
| Type | Size | Alignment |
|
||||
|---|---|---|
|
||||
| `f32` | 4 | 4 |
|
||||
| `vec2f` | 8 | 8 |
|
||||
| `vec3f` | 12 | 16 |
|
||||
| `vec4f` | 16 | 16 |
|
||||
| `mat4x4f` | 64 | 16 |
|
||||
| `array<T, N>` | N * stride(T) | align(T) |
|
||||
|
||||
**Important**: `vec3f` has alignment 16 (not 12). Add padding or use `vec4f`
|
||||
in storage buffers to avoid alignment issues.
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Prefer TSL when possible**: The compiler optimizes better with full
|
||||
graph visibility. Use `wgslFn` only when TSL lacks the feature.
|
||||
|
||||
2. **Minimize branching**: GPU wavefronts execute both branches. Use
|
||||
`select()` / `mix()` for simple conditionals.
|
||||
|
||||
3. **Shared memory for compute**: WGSL `var<workgroup>` gives fast
|
||||
workgroup-local memory. This is the main reason to drop to WGSL
|
||||
for compute kernels.
|
||||
|
||||
```wgsl
|
||||
var<workgroup> shared_data: array<vec4f, 64>;
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn main(@builtin(local_invocation_id) lid: vec3u) {
|
||||
shared_data[lid.x] = /* load from global */;
|
||||
workgroupBarrier();
|
||||
// Now all threads can read shared_data
|
||||
}
|
||||
```
|
||||
|
||||
<!-- markdownlint-disable MD029 -->
|
||||
|
||||
4. **Avoid register pressure**: Keep live variables minimal. WGSL
|
||||
compilers spill to memory when registers are exhausted.
|
||||
|
||||
5. **Loop unrolling**: Small fixed loops are often unrolled by the
|
||||
compiler. Avoid dynamic loop bounds when the range is known.
|
||||
|
||||
<!-- markdownlint-enable MD029 -->
|
||||
|
||||
## R3F Material Integration
|
||||
|
||||
To use a `wgslFn` result in an R3F component:
|
||||
|
||||
```tsx
|
||||
function WGSLMaterial() {
|
||||
const material = useMemo(() => {
|
||||
const mat = new MeshStandardNodeMaterial();
|
||||
const wgslResult = myWgslFunction({
|
||||
pos: positionLocal,
|
||||
time: timerLocal(),
|
||||
});
|
||||
mat.colorNode = wgslResult.xyz;
|
||||
return mat;
|
||||
}, []);
|
||||
|
||||
return <primitive object={material} attach="material" />;
|
||||
}
|
||||
```
|
||||
|
||||
The `wgslFn` return type matches the WGSL function return type. If the
|
||||
function returns `vec4f`, the TSL node is a `vec4` that supports
|
||||
`.xyz`, `.x`, `.r`, etc.
|
||||
7
.claude/settings.json
Normal file
7
.claude/settings.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"enabledPlugins": {
|
||||
},
|
||||
"env": {
|
||||
"ECC_DISABLED_HOOKS": "pre:bash:gateguard-fact-force,pre:edit-write:gateguard-fact-force"
|
||||
}
|
||||
}
|
||||
10
.claude/settings.local.json
Normal file
10
.claude/settings.local.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(rm /mnt/s/web/genesis/.git/index.lock)",
|
||||
"Bash(git rebase *)",
|
||||
"Bash(rm *)",
|
||||
"Bash(ln -s *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
158
.claude/skills/enterprise-package-migration/SKILL.md
Normal file
158
.claude/skills/enterprise-package-migration/SKILL.md
Normal file
@@ -0,0 +1,158 @@
|
||||
---
|
||||
name: enterprise-package-migration
|
||||
description: Use when migrating a package from `../suaveplan/` into the genesis monorepo, or when adding a new genesis-original package. Mandates reading `../suaveplan/enterprise/<pkg>.md` first, authoring `**Verified by:**` lines at draft time, completing the 4-gate triple before declaring done. Triggers on path patterns like `packages/<plugin>/<pkg>` for new packages, or when the user mentions migration from main, bringing across, or porting from `suaveplan`.
|
||||
---
|
||||
|
||||
# Enterprise Package Migration
|
||||
|
||||
This skill enforces the workflow that prevents the 17 retroactive-ticking incidents that occurred during the first migration wave. Read it end-to-end before doing anything.
|
||||
|
||||
## When this applies
|
||||
|
||||
You are in this skill if:
|
||||
|
||||
1. The user is creating a new package under `packages/<plugin>/<pkg>/` in `/mnt/s/web/genesis`.
|
||||
2. The user mentions migrating, porting, or bringing across a package from `../suaveplan/`.
|
||||
3. You are authoring an `openspec/changes/<id>/` for a package that has an `enterprise/<pkg>.md` source.
|
||||
|
||||
If none of those apply, this skill does not apply — exit.
|
||||
|
||||
## The non-negotiable workflow
|
||||
|
||||
### Step 0 — Read the source
|
||||
|
||||
If `../suaveplan/enterprise/<pkg>.md` exists, **read it end-to-end before opening any other file**. It is the canonical requirements source. Do not author a genesis spec from memory of what the package does — you will lose requirements that way. The 17 retroactive-ticking incidents all traced to specs written from memory rather than from a side-by-side mapping with this file.
|
||||
|
||||
### Step 1 — Read the workflow rules
|
||||
|
||||
In this order:
|
||||
|
||||
1. `/mnt/s/web/genesis/CLAUDE.md` — full read.
|
||||
2. `/mnt/s/web/genesis/openspec/AGENTS.md` — every section, especially 13 (spec-conformance audit) and 14 (source mapping for migrations).
|
||||
3. `/mnt/s/web/genesis/openspec/templates/README.md` — when and how to use the templates.
|
||||
|
||||
### Step 2 — Author the change folder from templates
|
||||
|
||||
```bash
|
||||
cp -r /mnt/s/web/genesis/openspec/templates/proposal.md /mnt/s/web/genesis/openspec/changes/<change-id>/proposal.md
|
||||
cp -r /mnt/s/web/genesis/openspec/templates/tasks.md /mnt/s/web/genesis/openspec/changes/<change-id>/tasks.md
|
||||
mkdir -p /mnt/s/web/genesis/openspec/changes/<change-id>/specs/<cap>/
|
||||
cp -r /mnt/s/web/genesis/openspec/templates/spec-delta.md /mnt/s/web/genesis/openspec/changes/<change-id>/specs/<cap>/spec.md
|
||||
```
|
||||
|
||||
Do not author from scratch. Every template field exists to prevent a specific failure mode.
|
||||
|
||||
### Step 3 — Fill `## Source mapping` in proposal.md
|
||||
|
||||
Every requirement in the spec delta MUST cite a line range in `../suaveplan/enterprise/<pkg>.md`. Format:
|
||||
|
||||
```markdown
|
||||
| Requirement | Source lines | Notes |
|
||||
|---|---|---|
|
||||
| Zero workspace deps | L12-L34 | verbatim |
|
||||
| OTel adapter | L140-L155 | verbatim |
|
||||
```
|
||||
|
||||
Requirements with no source line go in the second table with a justification:
|
||||
|
||||
```markdown
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| 100% coverage threshold | Genesis-wide policy, not package-specific |
|
||||
```
|
||||
|
||||
"Forgot to map" is not a justification.
|
||||
|
||||
### Step 4 — Fill `**Verified by:**` lines in spec-delta.md
|
||||
|
||||
Every requirement gets a line listing the concrete `it(...)` titles that pin every scenario:
|
||||
|
||||
```markdown
|
||||
**Verified by:** `src/feature/feature.test.ts::"<exact title>"` AND `src/feature/feature.spec-conformance.test.ts::"<exact title>"`
|
||||
```
|
||||
|
||||
This is mandatory at authoring time, not after the gates pass. If you don't know what to put, write the test name first, then write the test. Don't ship the spec without the line filled in.
|
||||
|
||||
### Step 5 — Implement, test, document
|
||||
|
||||
Standard CLAUDE.md rules apply:
|
||||
|
||||
- Five files per feature module (`feature.ts`, `feature.test.ts`, `feature.types.ts`, `feature.md`, `index.ts`).
|
||||
- 100% test coverage from day one (`provider: 'istanbul'`, never v8).
|
||||
- Sibling `.md` for every implementation file (≥200 words, basic + advanced examples, full API reference).
|
||||
- `{ timeout: 5000 }` on every `it()`/`test()`.
|
||||
- NEVER use mocks — use injectable `Clock`/`Rng`/`Logger`/etc from `@suaveplan/types`.
|
||||
- Check the `@suaveplan/error` consumers rule: every package extends `SuaveplanError` EXCEPT the three testing packages.
|
||||
|
||||
### Step 6 — Pass all 5 gates
|
||||
|
||||
Run in order. Stop on the first red:
|
||||
|
||||
```bash
|
||||
cd /mnt/s/web/genesis
|
||||
bun run check # gate 1 — Biome 0/0/0 across monorepo
|
||||
bunx turbo lint typecheck test --filter=@suaveplan/<pkg> # gate 2
|
||||
bunx turbo test:coverage --filter=@suaveplan/<pkg> # gate 3 — 100/100/100/100
|
||||
bun scripts/openspec-conformance.ts @suaveplan/<pkg> # gate 4 — 0 errors
|
||||
bunx turbo build --filter=@suaveplan/<pkg> # gate 5
|
||||
```
|
||||
|
||||
Never add a `biome-ignore` directive on faith. Workflow:
|
||||
|
||||
1. Run `bun run check` to see what Biome actually flags.
|
||||
2. Copy the exact rule path from Biome's output (e.g. `lint/suspicious/noExplicitAny`). Guessing produces parse errors — `lint/suspicious/noThrowLiterals` does not exist in Biome 2.x.
|
||||
3. Add the suppression, re-run `bun run check`, confirm it's now used.
|
||||
4. If you remove the underlying flag later, also remove the suppression — unused suppressions are errors.
|
||||
|
||||
If gate 3 reports drift, FIX IT INLINE. Do not punt to a follow-up change. The acceptable patterns:
|
||||
|
||||
- Implementation drifts from spec → align impl to spec, OR align spec to impl if the impl is the intended design.
|
||||
- Error code registered but unused → wire it into a real production code path.
|
||||
- OTel instrument declared but not emitted → wire it into the production hot path; assert with a real in-memory `MeterProvider`, not `vi.mock`.
|
||||
- Public type returns `: unknown` instead of a generic → thread `<T = unknown>` through the public surface.
|
||||
|
||||
### Step 7 — Pre-archive 8-row checklist
|
||||
|
||||
Before folding the spec delta:
|
||||
|
||||
| # | Check |
|
||||
|---|---|
|
||||
| 1 | Generics threaded; `expectTypeOf` assertions on every literal-type claim |
|
||||
| 2 | Zod schemas wired at every declared boundary |
|
||||
| 3 | Every error code in `error-codes.ts` thrown by ≥1 production code path |
|
||||
| 4 | Strict-mode behaviors tested both on and off |
|
||||
| 5 | OTel instruments emitted from production paths and asserted with real `MeterProvider` |
|
||||
| 6 | Lifecycle dedup tested where claimed (in-flight Promise, idempotent set/load) |
|
||||
| 7 | No retroactive tick — every `tasks.md` checkbox reflects work actually done in this change |
|
||||
| 8 | **No `export *` in any feature-module barrel** (§6a). Run `grep -rn "^\s*export\s\+\(type\s\+\)\?\*\s\+from" packages/<plugin>/<pkg>/src/ --include="index.ts" \| grep -v "/src/index\.ts:" \| grep -v "\.select/" \| grep -v "\.native/index\.ts:" \| grep -v "\.noble/index\.ts:" \| grep -v "export \* as"`. Zero output expected. If the upstream library used `export *` barrels, **convert them in this migration** — every `<feature>/index.ts` must list its symbols by name. |
|
||||
| 9 | **No direct `vitest` / `bun:test` imports in test files** (CLAUDE.md rule 11 / §7 / §13 step 4b). Run `grep -rnE "from ['\"](vitest\|bun:test)['\"]" packages/<plugin>/<pkg>/src/ --include="*.ts" --include="*.tsx"`. Zero output expected for any non-exempt package. The exempt allowlist is exactly five testing-infra packages (`@suaveplan/testing`, `testing-dom`, `testing-e2e`, `testing-integration`, `testing-integration-step-ca`). If the upstream library imported runner primitives directly from `vitest`, **rewrite every line to `from "@suaveplan/testing/runner"` in this migration** — not as a follow-up. The repo-wide CI gate `bun scripts/lint/check-test-runner-imports.ts` catches regressions but the migration commit must already be clean. |
|
||||
|
||||
### Step 8 — Archive
|
||||
|
||||
```bash
|
||||
# fold the delta
|
||||
# (manual: copy ADDED/MODIFIED/REMOVED requirement bodies into openspec/specs/<cap>/spec.md)
|
||||
mv /mnt/s/web/genesis/openspec/changes/<change-id> /mnt/s/web/genesis/openspec/archive/$(date +%Y-%m-%d)-<change-id>
|
||||
```
|
||||
|
||||
Refresh `MANIFEST.md` if package set or deps changed:
|
||||
|
||||
```bash
|
||||
cd /mnt/s/web/genesis && bun scripts/folder-reorg/generate-manifest.ts
|
||||
```
|
||||
|
||||
Update the package listing in `CLAUDE.md`.
|
||||
|
||||
## Red flags — stop and ask
|
||||
|
||||
- The user says "just stub it" / "we'll wire it later" / "ship it 95% done" — refuse. Genesis ships at 100% or not at all.
|
||||
- You discover the source `enterprise/<pkg>.md` references functionality not in scope (e.g., the file lists 5 things but the user only wants 3) — ask before scoping down. The default is "match the source"; partial migration is a judgment call.
|
||||
- A required dependency package isn't migrated yet → check `MANIFEST.md` for dep order. Migrate the dep first, or stop and ask.
|
||||
- The conformance script reports drift you can't fix without a breaking change to public surface → stop and ask. Breaking changes need explicit user approval.
|
||||
- The user pushes back on writing `**Verified by:**` lines at draft time — show them this skill and `openspec/AGENTS.md` Section 13. Do not skip the lines.
|
||||
|
||||
## Reference materials
|
||||
|
||||
- Canonical example: `openspec/changes/error-spec-conformance/` and `packages/core/error/src/base/suaveplan-error.spec-conformance.test.ts`
|
||||
- The 17 retroactive-ticking failure modes: `openspec/AGENTS.md` Section 12 (banned archival practices) and Section 13 (audit checklist)
|
||||
- Source mapping example: any `proposal.md` in `openspec/changes/*-spec-conformance/` after the conformance sweep
|
||||
159
.claude/skills/openspec-apply-change/SKILL.md
Normal file
159
.claude/skills/openspec-apply-change/SKILL.md
Normal file
@@ -0,0 +1,159 @@
|
||||
---
|
||||
name: openspec-apply-change
|
||||
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.3.1"
|
||||
---
|
||||
|
||||
Implement tasks from an OpenSpec change.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **Select the change**
|
||||
|
||||
If a name is provided, use it. Otherwise:
|
||||
- Infer from conversation context if the user mentioned a change
|
||||
- Auto-select if only one active change exists
|
||||
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
|
||||
|
||||
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
|
||||
|
||||
2. **Check status to understand the schema**
|
||||
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used (e.g., "spec-driven")
|
||||
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
|
||||
|
||||
3. **Get apply instructions**
|
||||
|
||||
```bash
|
||||
openspec instructions apply --change "<name>" --json
|
||||
```
|
||||
|
||||
This returns:
|
||||
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
|
||||
- Progress (total, complete, remaining)
|
||||
- Task list with status
|
||||
- Dynamic instruction based on current state
|
||||
|
||||
**Handle states:**
|
||||
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
|
||||
- If `state: "all_done"`: congratulate, suggest archive
|
||||
- Otherwise: proceed to implementation
|
||||
|
||||
4. **Read context files**
|
||||
|
||||
Read every file path listed under `contextFiles` from the apply instructions output.
|
||||
The files depend on the schema being used:
|
||||
- **spec-driven**: proposal, specs, design, tasks
|
||||
- Other schemas: follow the contextFiles from CLI output
|
||||
|
||||
5. **Show current progress**
|
||||
|
||||
Display:
|
||||
- Schema being used
|
||||
- Progress: "N/M tasks complete"
|
||||
- Remaining tasks overview
|
||||
- Dynamic instruction from CLI
|
||||
|
||||
6. **Implement tasks (loop until done or blocked)**
|
||||
|
||||
For each pending task:
|
||||
- Show which task is being worked on
|
||||
- Make the code changes required
|
||||
- Keep changes minimal and focused
|
||||
- Mark task complete in the tasks file: `- [ ]` → `- [x]`
|
||||
- Continue to next task
|
||||
|
||||
**Pause if:**
|
||||
- Task is unclear → ask for clarification
|
||||
- Implementation reveals a design issue → suggest updating artifacts
|
||||
- Error or blocker encountered → report and wait for guidance
|
||||
- User interrupts
|
||||
|
||||
7. **On completion or pause, show status**
|
||||
|
||||
Display:
|
||||
- Tasks completed this session
|
||||
- Overall progress: "N/M tasks complete"
|
||||
- If all done: suggest archive
|
||||
- If paused: explain why and wait for guidance
|
||||
|
||||
**Output During Implementation**
|
||||
|
||||
```text
|
||||
## Implementing: <change-name> (schema: <schema-name>)
|
||||
|
||||
Working on task 3/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
|
||||
Working on task 4/7: <task description>
|
||||
[...implementation happening...]
|
||||
✓ Task complete
|
||||
```
|
||||
|
||||
**Output On Completion**
|
||||
|
||||
```text
|
||||
## Implementation Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 7/7 tasks complete ✓
|
||||
|
||||
### Completed This Session
|
||||
- [x] Task 1
|
||||
- [x] Task 2
|
||||
...
|
||||
|
||||
All tasks complete! Ready to archive this change.
|
||||
```
|
||||
|
||||
**Output On Pause (Issue Encountered)**
|
||||
|
||||
```text
|
||||
## Implementation Paused
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Progress:** 4/7 tasks complete
|
||||
|
||||
### Issue Encountered
|
||||
<description of the issue>
|
||||
|
||||
**Options:**
|
||||
1. <option 1>
|
||||
2. <option 2>
|
||||
3. Other approach
|
||||
|
||||
What would you like to do?
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
|
||||
- Keep going through tasks until done or blocked
|
||||
- Always read context files before starting (from the apply instructions output)
|
||||
- If task is ambiguous, pause and ask before implementing
|
||||
- If implementation reveals issues, pause and suggest artifact updates
|
||||
- Keep code changes minimal and scoped to each task
|
||||
- Update task checkbox immediately after completing each task
|
||||
- Pause on errors, blockers, or unclear requirements - don't guess
|
||||
- Use contextFiles from CLI output, don't assume specific file names
|
||||
|
||||
**Fluid Workflow Integration**
|
||||
|
||||
This skill supports the "actions on a change" model:
|
||||
|
||||
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
|
||||
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly
|
||||
116
.claude/skills/openspec-archive-change/SKILL.md
Normal file
116
.claude/skills/openspec-archive-change/SKILL.md
Normal file
@@ -0,0 +1,116 @@
|
||||
---
|
||||
name: openspec-archive-change
|
||||
description: Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.3.1"
|
||||
---
|
||||
|
||||
Archive a completed change in the experimental workflow.
|
||||
|
||||
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no change name provided, prompt for selection**
|
||||
|
||||
Run `openspec list --json` to get available changes. Use the **AskUserQuestion tool** to let the user select.
|
||||
|
||||
Show only active changes (not already archived).
|
||||
Include the schema used for each change if available.
|
||||
|
||||
**IMPORTANT**: Do NOT guess or auto-select a change. Always let the user choose.
|
||||
|
||||
2. **Check artifact completion status**
|
||||
|
||||
Run `openspec status --change "<name>" --json` to check artifact completion.
|
||||
|
||||
Parse the JSON to understand:
|
||||
- `schemaName`: The workflow being used
|
||||
- `artifacts`: List of artifacts with their status (`done` or other)
|
||||
|
||||
**If any artifacts are not `done`:**
|
||||
- Display warning listing incomplete artifacts
|
||||
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
||||
- Proceed if user confirms
|
||||
|
||||
3. **Check task completion status**
|
||||
|
||||
Read the tasks file (typically `tasks.md`) to check for incomplete tasks.
|
||||
|
||||
Count tasks marked with `- [ ]` (incomplete) vs `- [x]` (complete).
|
||||
|
||||
**If incomplete tasks found:**
|
||||
- Display warning showing count of incomplete tasks
|
||||
- Use **AskUserQuestion tool** to confirm user wants to proceed
|
||||
- Proceed if user confirms
|
||||
|
||||
**If no tasks file exists:** Proceed without task-related warning.
|
||||
|
||||
4. **Assess delta spec sync state**
|
||||
|
||||
Check for delta specs at `openspec/changes/<name>/specs/`. If none exist, proceed without sync prompt.
|
||||
|
||||
**If delta specs exist:**
|
||||
- Compare each delta spec with its corresponding main spec at `openspec/specs/<capability>/spec.md`
|
||||
- Determine what changes would be applied (adds, modifications, removals, renames)
|
||||
- Show a combined summary before prompting
|
||||
|
||||
**Prompt options:**
|
||||
- If changes needed: "Sync now (recommended)", "Archive without syncing"
|
||||
- If already synced: "Archive now", "Sync anyway", "Cancel"
|
||||
|
||||
If user chooses sync, use Task tool (subagent_type: "general-purpose", prompt: "Use Skill tool to invoke openspec-sync-specs for change '<name>'. Delta spec analysis: <include the analyzed delta spec summary>"). Proceed to archive regardless of choice.
|
||||
|
||||
5. **Perform the archive**
|
||||
|
||||
Create the archive directory if it doesn't exist:
|
||||
|
||||
```bash
|
||||
mkdir -p openspec/changes/archive
|
||||
```
|
||||
|
||||
Generate target name using current date: `YYYY-MM-DD-<change-name>`
|
||||
|
||||
**Check if target already exists:**
|
||||
- If yes: Fail with error, suggest renaming existing archive or using different date
|
||||
- If no: Move the change directory to archive
|
||||
|
||||
```bash
|
||||
mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>
|
||||
```
|
||||
|
||||
6. **Display summary**
|
||||
|
||||
Show archive completion summary including:
|
||||
- Change name
|
||||
- Schema that was used
|
||||
- Archive location
|
||||
- Whether specs were synced (if applicable)
|
||||
- Note about any warnings (incomplete artifacts/tasks)
|
||||
|
||||
**Output On Success**
|
||||
|
||||
```text
|
||||
## Archive Complete
|
||||
|
||||
**Change:** <change-name>
|
||||
**Schema:** <schema-name>
|
||||
**Archived to:** openspec/changes/archive/YYYY-MM-DD-<name>/
|
||||
**Specs:** ✓ Synced to main specs (or "No delta specs" or "Sync skipped")
|
||||
|
||||
All artifacts complete. All tasks complete.
|
||||
```
|
||||
|
||||
**Guardrails**
|
||||
|
||||
- Always prompt for change selection if not provided
|
||||
- Use artifact graph (openspec status --json) for completion checking
|
||||
- Don't block archive on warnings - just inform and confirm
|
||||
- Preserve .openspec.yaml when moving to archive (it moves with the directory)
|
||||
- Show clear summary of what happened
|
||||
- If sync is requested, use openspec-sync-specs approach (agent-driven)
|
||||
- If delta specs exist, always run the sync assessment and show the combined summary before prompting
|
||||
299
.claude/skills/openspec-explore/SKILL.md
Normal file
299
.claude/skills/openspec-explore/SKILL.md
Normal file
@@ -0,0 +1,299 @@
|
||||
---
|
||||
name: openspec-explore
|
||||
description: Enter explore mode - a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.3.1"
|
||||
---
|
||||
|
||||
Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes.
|
||||
|
||||
**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal. You MAY create OpenSpec artifacts (proposals, designs, specs) if the user asks—that's capturing thinking, not implementing.
|
||||
|
||||
**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore.
|
||||
|
||||
---
|
||||
|
||||
## The Stance
|
||||
|
||||
- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script
|
||||
- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions.
|
||||
- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking
|
||||
- **Adaptive** - Follow interesting threads, pivot when new information emerges
|
||||
- **Patient** - Don't rush to conclusions, let the shape of the problem emerge
|
||||
- **Grounded** - Explore the actual codebase when relevant, don't just theorize
|
||||
|
||||
---
|
||||
|
||||
## What You Might Do
|
||||
|
||||
Depending on what the user brings, you might:
|
||||
|
||||
**Explore the problem space**
|
||||
|
||||
- Ask clarifying questions that emerge from what they said
|
||||
- Challenge assumptions
|
||||
- Reframe the problem
|
||||
- Find analogies
|
||||
|
||||
**Investigate the codebase**
|
||||
|
||||
- Map existing architecture relevant to the discussion
|
||||
- Find integration points
|
||||
- Identify patterns already in use
|
||||
- Surface hidden complexity
|
||||
|
||||
**Compare options**
|
||||
|
||||
- Brainstorm multiple approaches
|
||||
- Build comparison tables
|
||||
- Sketch tradeoffs
|
||||
- Recommend a path (if asked)
|
||||
|
||||
**Visualize**
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Use ASCII diagrams liberally │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────┐ ┌────────┐ │
|
||||
│ │ State │────────▶│ State │ │
|
||||
│ │ A │ │ B │ │
|
||||
│ └────────┘ └────────┘ │
|
||||
│ │
|
||||
│ System diagrams, state machines, │
|
||||
│ data flows, architecture sketches, │
|
||||
│ dependency graphs, comparison tables │
|
||||
│ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Surface risks and unknowns**
|
||||
|
||||
- Identify what could go wrong
|
||||
- Find gaps in understanding
|
||||
- Suggest spikes or investigations
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec Awareness
|
||||
|
||||
You have full context of the OpenSpec system. Use it naturally, don't force it.
|
||||
|
||||
### Check for context
|
||||
|
||||
At the start, quickly check what exists:
|
||||
|
||||
```bash
|
||||
openspec list --json
|
||||
```
|
||||
|
||||
This tells you:
|
||||
|
||||
- If there are active changes
|
||||
- Their names, schemas, and status
|
||||
- What the user might be working on
|
||||
|
||||
### When no change exists
|
||||
|
||||
Think freely. When insights crystallize, you might offer:
|
||||
|
||||
- "This feels solid enough to start a change. Want me to create a proposal?"
|
||||
- Or keep exploring - no pressure to formalize
|
||||
|
||||
### When a change exists
|
||||
|
||||
If the user mentions a change or you detect one is relevant:
|
||||
|
||||
1. **Read existing artifacts for context**
|
||||
- `openspec/changes/<name>/proposal.md`
|
||||
- `openspec/changes/<name>/design.md`
|
||||
- `openspec/changes/<name>/tasks.md`
|
||||
- etc.
|
||||
|
||||
2. **Reference them naturally in conversation**
|
||||
- "Your design mentions using Redis, but we just realized SQLite fits better..."
|
||||
- "The proposal scopes this to premium users, but we're now thinking everyone..."
|
||||
|
||||
3. **Offer to capture when decisions are made**
|
||||
|
||||
| Insight Type | Where to Capture |
|
||||
|----------------------------|--------------------------------|
|
||||
| New requirement discovered | `specs/<capability>/spec.md` |
|
||||
| Requirement changed | `specs/<capability>/spec.md` |
|
||||
| Design decision made | `design.md` |
|
||||
| Scope changed | `proposal.md` |
|
||||
| New work identified | `tasks.md` |
|
||||
| Assumption invalidated | Relevant artifact |
|
||||
|
||||
Example offers:
|
||||
- "That's a design decision. Capture it in design.md?"
|
||||
- "This is a new requirement. Add it to specs?"
|
||||
- "This changes scope. Update the proposal?"
|
||||
|
||||
4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture.
|
||||
|
||||
---
|
||||
|
||||
## What You Don't Have To Do
|
||||
|
||||
- Follow a script
|
||||
- Ask the same questions every time
|
||||
- Produce a specific artifact
|
||||
- Reach a conclusion
|
||||
- Stay on topic if a tangent is valuable
|
||||
- Be brief (this is thinking time)
|
||||
|
||||
---
|
||||
|
||||
## Handling Different Entry Points
|
||||
|
||||
**User brings a vague idea:**
|
||||
|
||||
```text
|
||||
User: I'm thinking about adding real-time collaboration
|
||||
|
||||
You: Real-time collab is a big space. Let me think about this...
|
||||
|
||||
COLLABORATION SPECTRUM
|
||||
════════════════════════════════════════════
|
||||
|
||||
Awareness Coordination Sync
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌────────┐ ┌────────┐ ┌────────┐
|
||||
│Presence│ │Cursors │ │ CRDT │
|
||||
│ "3 │ │ Multi │ │Conflict│
|
||||
│online" │ │ select │ │ free │
|
||||
└────────┘ └────────┘ └────────┘
|
||||
│ │ │
|
||||
trivial moderate complex
|
||||
|
||||
Where's your head at?
|
||||
```
|
||||
|
||||
**User brings a specific problem:**
|
||||
|
||||
```text
|
||||
User: The auth system is a mess
|
||||
|
||||
You: [reads codebase]
|
||||
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ CURRENT AUTH FLOW │
|
||||
└─────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ Google │ │ GitHub │ │ Email │
|
||||
│ OAuth │ │ OAuth │ │ Magic │
|
||||
└────┬────┘ └────┬────┘ └────┬────┘
|
||||
│ │ │
|
||||
└─────────────┼─────────────┘
|
||||
▼
|
||||
┌───────────┐
|
||||
│ Session │
|
||||
└─────┬─────┘
|
||||
│
|
||||
▼
|
||||
┌───────────┐
|
||||
│ Perms │
|
||||
└───────────┘
|
||||
|
||||
I see three tangles. Which one's burning?
|
||||
```
|
||||
|
||||
**User is stuck mid-implementation:**
|
||||
|
||||
```text
|
||||
User: /opsx:explore add-auth-system
|
||||
The OAuth integration is more complex than expected
|
||||
|
||||
You: [reads change artifacts]
|
||||
|
||||
You're on task 4: "Implement OAuth flow"
|
||||
|
||||
Let me trace what's involved...
|
||||
|
||||
[draws diagram, explores options, suggests paths]
|
||||
|
||||
Want to update the design to reflect this?
|
||||
Or add a spike task to investigate?
|
||||
```
|
||||
|
||||
**User wants to compare options:**
|
||||
|
||||
```text
|
||||
User: Should we use Postgres or SQLite?
|
||||
|
||||
You: Generic answer is boring. What's the context?
|
||||
|
||||
User: A CLI tool that tracks local dev environments
|
||||
|
||||
You: That changes everything.
|
||||
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ CLI TOOL DATA STORAGE │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
Key constraints:
|
||||
• No daemon running
|
||||
• Must work offline
|
||||
• Single user
|
||||
|
||||
SQLite Postgres
|
||||
Deployment embedded ✓ needs server ✗
|
||||
Offline yes ✓ no ✗
|
||||
Single file yes ✓ no ✗
|
||||
|
||||
SQLite. Not even close.
|
||||
|
||||
Unless... is there a sync component?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ending Discovery
|
||||
|
||||
There's no required ending. Discovery might:
|
||||
|
||||
- **Flow into a proposal**: "Ready to start? I can create a change proposal."
|
||||
- **Result in artifact updates**: "Updated design.md with these decisions"
|
||||
- **Just provide clarity**: User has what they need, moves on
|
||||
- **Continue later**: "We can pick this up anytime"
|
||||
|
||||
When it feels like things are crystallizing, you might summarize:
|
||||
|
||||
```text
|
||||
## What We Figured Out
|
||||
|
||||
**The problem**: [crystallized understanding]
|
||||
|
||||
**The approach**: [if one emerged]
|
||||
|
||||
**Open questions**: [if any remain]
|
||||
|
||||
**Next steps** (if ready):
|
||||
- Create a change proposal
|
||||
- Keep exploring: just keep talking
|
||||
```
|
||||
|
||||
But this summary is optional. Sometimes the thinking IS the value.
|
||||
|
||||
---
|
||||
|
||||
## Guardrails
|
||||
|
||||
- **Don't implement** - Never write code or implement features. Creating OpenSpec artifacts is fine, writing application code is not.
|
||||
- **Don't fake understanding** - If something is unclear, dig deeper
|
||||
- **Don't rush** - Discovery is thinking time, not task time
|
||||
- **Don't force structure** - Let patterns emerge naturally
|
||||
- **Don't auto-capture** - Offer to save insights, don't just do it
|
||||
- **Do visualize** - A good diagram is worth many paragraphs
|
||||
- **Do explore the codebase** - Ground discussions in reality
|
||||
- **Do question assumptions** - Including the user's and your own
|
||||
120
.claude/skills/openspec-propose/SKILL.md
Normal file
120
.claude/skills/openspec-propose/SKILL.md
Normal file
@@ -0,0 +1,120 @@
|
||||
---
|
||||
name: openspec-propose
|
||||
description: Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation.
|
||||
license: MIT
|
||||
compatibility: Requires openspec CLI.
|
||||
metadata:
|
||||
author: openspec
|
||||
version: "1.0"
|
||||
generatedBy: "1.3.1"
|
||||
---
|
||||
|
||||
Propose a new change - create the change and generate all artifacts in one step.
|
||||
|
||||
I'll create a change with artifacts:
|
||||
|
||||
- proposal.md (what & why)
|
||||
- design.md (how)
|
||||
- tasks.md (implementation steps)
|
||||
|
||||
When ready to implement, run /opsx:apply
|
||||
|
||||
---
|
||||
|
||||
**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build.
|
||||
|
||||
**Steps**
|
||||
|
||||
1. **If no clear input provided, ask what they want to build**
|
||||
|
||||
Use the **AskUserQuestion tool** (open-ended, no preset options) to ask:
|
||||
> "What change do you want to work on? Describe what you want to build or fix."
|
||||
|
||||
From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`).
|
||||
|
||||
**IMPORTANT**: Do NOT proceed without understanding what the user wants to build.
|
||||
|
||||
2. **Create the change directory**
|
||||
|
||||
```bash
|
||||
openspec new change "<name>"
|
||||
```
|
||||
|
||||
This creates a scaffolded change at `openspec/changes/<name>/` with `.openspec.yaml`.
|
||||
|
||||
3. **Get the artifact build order**
|
||||
|
||||
```bash
|
||||
openspec status --change "<name>" --json
|
||||
```
|
||||
|
||||
Parse the JSON to get:
|
||||
- `applyRequires`: array of artifact IDs needed before implementation (e.g., `["tasks"]`)
|
||||
- `artifacts`: list of all artifacts with their status and dependencies
|
||||
|
||||
4. **Create artifacts in sequence until apply-ready**
|
||||
|
||||
Use the **TodoWrite tool** to track progress through the artifacts.
|
||||
|
||||
Loop through artifacts in dependency order (artifacts with no pending dependencies first):
|
||||
|
||||
a. **For each artifact that is `ready` (dependencies satisfied)**:
|
||||
- Get instructions:
|
||||
|
||||
```bash
|
||||
openspec instructions <artifact-id> --change "<name>" --json
|
||||
```
|
||||
|
||||
- The instructions JSON includes:
|
||||
- `context`: Project background (constraints for you - do NOT include in output)
|
||||
- `rules`: Artifact-specific rules (constraints for you - do NOT include in output)
|
||||
- `template`: The structure to use for your output file
|
||||
- `instruction`: Schema-specific guidance for this artifact type
|
||||
- `outputPath`: Where to write the artifact
|
||||
- `dependencies`: Completed artifacts to read for context
|
||||
- Read any completed dependency files for context
|
||||
- Create the artifact file using `template` as the structure
|
||||
- Apply `context` and `rules` as constraints - but do NOT copy them into the file
|
||||
- Show brief progress: "Created <artifact-id>"
|
||||
|
||||
b. **Continue until all `applyRequires` artifacts are complete**
|
||||
- After creating each artifact, re-run `openspec status --change "<name>" --json`
|
||||
- Check if every artifact ID in `applyRequires` has `status: "done"` in the artifacts array
|
||||
- Stop when all `applyRequires` artifacts are done
|
||||
|
||||
c. **If an artifact requires user input** (unclear context):
|
||||
- Use **AskUserQuestion tool** to clarify
|
||||
- Then continue with creation
|
||||
|
||||
5. **Show final status**
|
||||
|
||||
```bash
|
||||
openspec status --change "<name>"
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
After completing all artifacts, summarize:
|
||||
|
||||
- Change name and location
|
||||
- List of artifacts created with brief descriptions
|
||||
- What's ready: "All artifacts created! Ready for implementation."
|
||||
- Prompt: "Run `/opsx:apply` or ask me to implement to start working on the tasks."
|
||||
|
||||
**Artifact Creation Guidelines**
|
||||
|
||||
- Follow the `instruction` field from `openspec instructions` for each artifact type
|
||||
- The schema defines what each artifact should contain - follow it
|
||||
- Read dependency artifacts for context before creating new ones
|
||||
- Use `template` as the structure for your output file - fill in its sections
|
||||
- **IMPORTANT**: `context` and `rules` are constraints for YOU, not content for the file
|
||||
- Do NOT copy `<context>`, `<rules>`, `<project_context>` blocks into the artifact
|
||||
- These guide what you write, but should never appear in the output
|
||||
|
||||
**Guardrails**
|
||||
|
||||
- Create ALL artifacts needed for implementation (as defined by schema's `apply.requires`)
|
||||
- Always read dependency artifacts before creating a new one
|
||||
- If context is critically unclear, ask the user - but prefer making reasonable decisions to keep momentum
|
||||
- If a change with that name already exists, ask if user wants to continue it or create a new one
|
||||
- Verify each artifact file exists after writing before proceeding to next
|
||||
126
.claude/templates/README.md
Normal file
126
.claude/templates/README.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# README-generation templates
|
||||
|
||||
This directory holds `@suaveplan/templates` engine templates for auto-generating
|
||||
`README.md` files at three levels of the `packages/` tree.
|
||||
|
||||
| Template | Target path | Purpose |
|
||||
|----------|-------------|---------|
|
||||
| `package-readme.template.md` | `packages/<category>/<plugin>/<pkg>/README.md` | Stand-alone npm-package readme |
|
||||
| `plugin-readme.template.md` | `packages/<category>/<plugin>/README.md` | Index of packages inside one plugin |
|
||||
| `category-readme.template.md` | `packages/<category>/README.md` | Index of all packages in a category |
|
||||
|
||||
## How to render
|
||||
|
||||
Load the template file as a string, build a context object, and pass both to
|
||||
`TemplateEngine` with the **loop-aware preset**:
|
||||
|
||||
```ts
|
||||
import {
|
||||
TemplateEngine,
|
||||
fullPresetWithLoops,
|
||||
} from "@suaveplan/templates";
|
||||
import type { ParseState } from "@suaveplan/templates";
|
||||
|
||||
const engine = new TemplateEngine();
|
||||
engine.usePreset(
|
||||
fullPresetWithLoops(
|
||||
(tpl, ctx, state) =>
|
||||
(engine as unknown as { parseInternal: Function }).parseInternal(
|
||||
tpl,
|
||||
ctx,
|
||||
state
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const template = await Bun.file(
|
||||
".claude/templates/package-readme.template.md"
|
||||
).text();
|
||||
|
||||
const context = {
|
||||
packageName: "@suaveplan/api-plugin-vault",
|
||||
shortDescription: "Per-user vault HTTP plugin for `@suaveplan/api`.",
|
||||
summary: "Per-user vault HTTP plugin...",
|
||||
installCommand: "bun add @suaveplan/api-plugin-vault",
|
||||
quickStartCode: `import { createVaultPlugin } from "@suaveplan/api-plugin-vault";`,
|
||||
quickStartDescription: "After `registry.start()`, the vault plugin...",
|
||||
features: [
|
||||
"Persists `site` records under SurrealDB...",
|
||||
"Enforces per-organization and per-site caps...",
|
||||
],
|
||||
modules: [
|
||||
{ name: "error-codes/error-codes", path: "./src/error-codes/error-codes.md", description: "9 stable `VAULT_PLUGIN_*` codes." },
|
||||
{ name: "plugin/plugin", path: "./src/plugin/plugin.md", description: "`createVaultPlugin` factory." },
|
||||
],
|
||||
otelInstruments: [
|
||||
{ type: "Counter", name: "webhook_plugin.deliveries_total", labels: "{event,status,endpoint_id}" },
|
||||
],
|
||||
otelStatusValues: "`delivered`, `exhausted`, `not_found`",
|
||||
license: "MIT",
|
||||
};
|
||||
|
||||
const readme = engine.parse(template, context);
|
||||
```
|
||||
|
||||
## Context shapes
|
||||
|
||||
### `package-readme.template.md`
|
||||
|
||||
| Key | Type | Required | Notes |
|
||||
|-----|------|----------|-------|
|
||||
| `packageName` | `string` | ✅ | Full scoped name, e.g. `@suaveplan/templates` |
|
||||
| `shortDescription` | `string` | ✅ | ≤120 characters; appended to H1 |
|
||||
| `summary` | `string` | ✅ | Multi-line paragraph(s) |
|
||||
| `installCommand` | `string` | ✅ | Shell command, e.g. `bun add @suaveplan/templates` |
|
||||
| `quickStartCode` | `string` | ✅ | Code inside the `ts` fence (no backticks) |
|
||||
| `quickStartDescription` | `string` | ❌ | Paragraph after the quick-start block |
|
||||
| `features` | `string[]` | ❌ | Renders the "What it does" section |
|
||||
| `modules` | `{name,path,description}[]` | ✅ | Module index bullets |
|
||||
| `otelInstruments` | `{type,name,labels?}[]` | ❌ | Renders the "OpenTelemetry" section |
|
||||
| `otelStatusValues` | `string` | ❌ | Sentence fragment after the instrument list |
|
||||
| `license` | `string` | ✅ | e.g. `MIT` |
|
||||
|
||||
### `plugin-readme.template.md`
|
||||
|
||||
| Key | Type | Required |
|
||||
|-----|------|----------|
|
||||
| `pluginName` | `string` | ✅ |
|
||||
| `pluginDescription` | `string` | ✅ |
|
||||
| `packages` | `{name,path,description}[]` | ✅ |
|
||||
|
||||
### `category-readme.template.md`
|
||||
|
||||
| Key | Type | Required |
|
||||
|-----|------|----------|
|
||||
| `categoryName` | `string` | ✅ |
|
||||
| `categoryDescription` | `string` | ✅ |
|
||||
| `packages` | `{name,path,description}[]` | ✅ |
|
||||
|
||||
## Brace-escaping caveat
|
||||
|
||||
The `@suaveplan/templates` engine scans the **entire** template (including text
|
||||
inside code blocks) for `{identifier}` tokens. If a context string contains a
|
||||
literal brace pair that looks like a variable — e.g. `GET /sites/{id}/public` —
|
||||
the engine will try to resolve it and, when the identifier is absent from
|
||||
context, replace it with an empty string.
|
||||
|
||||
When building context objects, avoid bare `{foo}` patterns. Safe work-arounds:
|
||||
|
||||
1. **Add a space** — `{ id }` does not match any plugin regex.
|
||||
2. **Back-tick wrapping** — `` `{id}` `` is still scanned, but if the value is
|
||||
not in context the engine replaces it with `""`. Only use this when the
|
||||
loss of the braces is acceptable.
|
||||
3. **Post-process** — after rendering, run a regex replace that restores any
|
||||
sentinels you placed in the context strings (e.g. `«LBRACE»` → `{`).
|
||||
|
||||
Because the package-level readme is aimed at npm consumers with no knowledge of
|
||||
the monorepo, the generated text should already avoid monorepo-specific tags
|
||||
(`genesis`, migration phases, universal tiers, etc.). The AI generating the
|
||||
context object is responsible for stripping that content before rendering.
|
||||
|
||||
## Conditional nesting
|
||||
|
||||
The `@suaveplan/templates` conditional plugin (`{if:…}…{/if}`) does not support
|
||||
nesting. The package template keeps all conditionals at the top level so they
|
||||
never wrap each other. Loop bodies may contain inner conditionals safely because
|
||||
the loop plugin resolves them recursively before the outer conditional runs.
|
||||
8
.claude/templates/category-readme.template.md
Normal file
8
.claude/templates/category-readme.template.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# {categoryName}
|
||||
|
||||
{categoryDescription}
|
||||
|
||||
## Packages
|
||||
|
||||
{each:packages:pkg}- [`{pkg.name}`]({pkg.path}) — {pkg.description}
|
||||
{/each}
|
||||
237
.claude/templates/module-docs.md
Normal file
237
.claude/templates/module-docs.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# [Module Name]
|
||||
|
||||
## Purpose
|
||||
|
||||
Brief description of what this module does and why it exists. Explain the problem it solves and the value it provides.
|
||||
|
||||
## Features
|
||||
|
||||
- Feature 1 - Brief description
|
||||
- Feature 2 - Brief description
|
||||
- Feature 3 - Brief description
|
||||
- Additional features as needed
|
||||
|
||||
## Architecture
|
||||
|
||||
High-level architecture explanation
|
||||
|
||||
- Design patterns used (e.g., Factory, Singleton, Observer)
|
||||
- Key abstractions and interfaces
|
||||
- How components interact
|
||||
- Performance considerations
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Example
|
||||
|
||||
```typescript
|
||||
import { MyClass } from '@suaveplan/package';
|
||||
|
||||
// Basic usage example
|
||||
const instance = new MyClass();
|
||||
instance.doSomething();
|
||||
```
|
||||
|
||||
### Advanced Example
|
||||
|
||||
```typescript
|
||||
import { MyClass, type MyOptions } from '@suaveplan/package';
|
||||
|
||||
// More complex usage patterns
|
||||
const options: MyOptions = {
|
||||
// Configuration options
|
||||
};
|
||||
|
||||
const instance = new MyClass(options);
|
||||
|
||||
// Advanced usage with callbacks, generics, etc.
|
||||
instance.advancedMethod((result) => {
|
||||
console.log('Result:', result);
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Example
|
||||
|
||||
```typescript
|
||||
// Example showing how this module integrates with other modules
|
||||
import { MyClass } from '@suaveplan/package';
|
||||
import { RelatedClass } from '@suaveplan/related-package';
|
||||
|
||||
const myInstance = new MyClass();
|
||||
const relatedInstance = new RelatedClass(myInstance);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### ClassName
|
||||
|
||||
Main class description and purpose.
|
||||
|
||||
#### constructor(options?: Options)
|
||||
|
||||
**`Parameters:`**
|
||||
|
||||
- `options` (optional) - Configuration object
|
||||
- `option1: string` - Description of option1
|
||||
- `option2: number` - Description of option2
|
||||
- `option3?: boolean` - Description of optional option3
|
||||
|
||||
**Returns:** Instance of ClassName
|
||||
|
||||
**`Example:`**
|
||||
|
||||
```typescript
|
||||
const instance = new ClassName({
|
||||
option1: 'value',
|
||||
option2: 42
|
||||
});
|
||||
```
|
||||
|
||||
#### method(param: Type): ReturnType
|
||||
|
||||
Description of what the method does.
|
||||
|
||||
**`Parameters:`**
|
||||
|
||||
- `param: Type` - Description of the parameter
|
||||
- Include constraints or validation rules
|
||||
- Mention default values if applicable
|
||||
|
||||
**Returns:** Description of the return value
|
||||
|
||||
**`Throws:`**
|
||||
|
||||
- `ErrorType` - When this error occurs
|
||||
|
||||
**`Example:`**
|
||||
|
||||
```typescript
|
||||
const result = instance.method('value');
|
||||
```
|
||||
|
||||
#### asyncMethod(param: Type): Promise<ReturnType>
|
||||
|
||||
Description of what the async method does.
|
||||
|
||||
**`Parameters:`**
|
||||
|
||||
- `param: Type` - Description
|
||||
|
||||
**Returns:** Promise that resolves to description
|
||||
|
||||
**`Example:`**
|
||||
|
||||
```typescript
|
||||
const result = await instance.asyncMethod('value');
|
||||
```
|
||||
|
||||
### InterfaceName
|
||||
|
||||
Description of the interface and when to implement it.
|
||||
|
||||
```typescript
|
||||
interface InterfaceName {
|
||||
property: Type;
|
||||
method(param: Type): ReturnType;
|
||||
}
|
||||
```
|
||||
|
||||
**`Properties:`**
|
||||
|
||||
- `property: Type` - Description of the property
|
||||
|
||||
**`Methods:`**
|
||||
|
||||
- `method(param: Type): ReturnType` - Description of the method
|
||||
|
||||
### TypeName
|
||||
|
||||
Description of the type and its use cases.
|
||||
|
||||
```typescript
|
||||
type TypeName = /* type definition */;
|
||||
```
|
||||
|
||||
**`Usage:`**
|
||||
|
||||
```typescript
|
||||
const example: TypeName = /* example value */;
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Design Decisions
|
||||
|
||||
- **Decision 1**: Explanation of why this approach was chosen
|
||||
- Alternatives considered
|
||||
- Trade-offs made
|
||||
|
||||
- **Decision 2**: Another important design decision
|
||||
- Technical rationale
|
||||
- Impact on performance or maintainability
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- **Time Complexity**: O(n) for operation X
|
||||
- **Space Complexity**: O(1) for operation Y
|
||||
- **Optimizations**: Specific optimizations implemented (caching, memoization, etc.)
|
||||
- **Benchmarks**: Performance metrics if available
|
||||
|
||||
### Known Limitations
|
||||
|
||||
- **Limitation 1**: Description and potential workarounds
|
||||
- **Limitation 2**: Description and potential workarounds
|
||||
- **Browser Compatibility**: Any browser-specific limitations
|
||||
- **Platform Support**: Node.js, Deno, Browser support notes
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
- Unit tests cover: X, Y, Z
|
||||
- Integration tests cover: A, B, C
|
||||
- E2E tests cover: P, Q, R
|
||||
- Test coverage: XX%
|
||||
|
||||
### Migration Guide (if applicable)
|
||||
|
||||
If this replaces or significantly changes an existing module
|
||||
|
||||
```typescript
|
||||
// Old way (deprecated)
|
||||
import { OldClass } from '@suaveplan/package';
|
||||
const old = new OldClass();
|
||||
|
||||
// New way (recommended)
|
||||
import { NewClass } from '@suaveplan/package';
|
||||
const updated = new NewClass();
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Related Module 1](../related-module/related-module.md) - Brief description of relationship
|
||||
- [Related Module 2](../../other-package/module/module.md) - Brief description of relationship
|
||||
- [External Documentation](https://example.com/docs) - Relevant external resources
|
||||
- [RFC/ADR](../../docs/adr/001-decision.md) - Architecture decision records if applicable
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version X.Y.Z (YYYY-MM-DD)
|
||||
|
||||
- **Added**: New features
|
||||
- **Changed**: Changes to existing features
|
||||
- **Deprecated**: Soon-to-be removed features
|
||||
- **Removed**: Removed features
|
||||
- **Fixed**: Bug fixes
|
||||
- **Security**: Security improvements
|
||||
|
||||
## Contributing
|
||||
|
||||
Guidelines specific to this module
|
||||
|
||||
- Code style preferences
|
||||
- Testing requirements (maintain XX% coverage)
|
||||
- Documentation requirements
|
||||
- Review process
|
||||
|
||||
## License
|
||||
|
||||
Indicate if different from project license, otherwise remove this section.
|
||||
37
.claude/templates/package-readme.template.md
Normal file
37
.claude/templates/package-readme.template.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# {packageName} - {shortDescription}
|
||||
|
||||
## Summary
|
||||
|
||||
{summary}
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
{installCommand}
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
{quickStartCode}
|
||||
```
|
||||
|
||||
{quickStartDescription}
|
||||
|
||||
{if:features}## What it does
|
||||
|
||||
{each:features:feature}- {feature}
|
||||
{/each}
|
||||
{/if}{if:modules}## Modules
|
||||
|
||||
{each:modules:mod}- [`{mod.name}`]({mod.path}) — {mod.description}
|
||||
{/each}
|
||||
{/if}{if:otelInstruments}## OpenTelemetry
|
||||
|
||||
{each:otelInstruments:inst}- {inst.type} `{inst.name}{if:inst.labels}{inst.labels}{/if}`
|
||||
{/each}
|
||||
{/if}{if:otelStatusValues}Status label values: {otelStatusValues}.
|
||||
{/if}
|
||||
## License
|
||||
|
||||
{license}
|
||||
8
.claude/templates/plugin-readme.template.md
Normal file
8
.claude/templates/plugin-readme.template.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# {pluginName}
|
||||
|
||||
{pluginDescription}
|
||||
|
||||
## Packages
|
||||
|
||||
{each:packages:pkg}- [`{pkg.name}`]({pkg.path}) — {pkg.description}
|
||||
{/each}
|
||||
11
.ctirc
Normal file
11
.ctirc
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"options": [
|
||||
{
|
||||
"mode": "create",
|
||||
"generationStyle": "default-alias-named-destructive",
|
||||
"overwrite": true,
|
||||
"skipEmptyDir": true,
|
||||
"excludeFiles": ["**/*.test.ts", "**/*.spec.ts", "**/*.stories.tsx"]
|
||||
}
|
||||
]
|
||||
}
|
||||
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
NPM_TOKEN=
|
||||
NPM_URL=https://npmjs.com
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
59
.github/pull_request_template/flip-primitive.md
vendored
Normal file
59
.github/pull_request_template/flip-primitive.md
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
# Flip primitive to `native`
|
||||
|
||||
This PR changes one or more `crypto.config.ts` entries from `'noble'` to
|
||||
`'native'`. Landing a flip permanently swaps the repo's routing for the
|
||||
primitive — every consumer picks up the in-house implementation on the
|
||||
next build.
|
||||
|
||||
Do not merge this PR without every box checked. Dangerous-tier primitives
|
||||
(curves, AEADs, argon2id) additionally require a linked audit document.
|
||||
|
||||
## Primitive(s) affected
|
||||
|
||||
<!-- List each flipped primitive and its new flavour. -->
|
||||
- [ ] `<primitive-id>`: `noble` → `native`
|
||||
|
||||
## Gate 1 — KAT (known-answer tests)
|
||||
|
||||
- [ ] All vectors pass on the new impl.
|
||||
- [ ] Link to the KAT run:
|
||||
<!-- paste CI link for `bunx turbo crypto:kat` -->
|
||||
|
||||
## Gate 2 — Differential fuzz history
|
||||
|
||||
- [ ] At least 7 consecutive nightly `crypto:fuzz` runs pass native-vs-noble
|
||||
with zero mismatches.
|
||||
- [ ] Link to the 7-day nightly history:
|
||||
<!-- paste CI dashboard link -->
|
||||
|
||||
## Gate 3 — Timing report
|
||||
|
||||
- [ ] `crypto:timing` run for this primitive reports `constantTime: true`
|
||||
against the default threshold (t < 4.5).
|
||||
- [ ] Link to the timing report:
|
||||
<!-- paste artifact link -->
|
||||
|
||||
## Audit document (dangerous tier only)
|
||||
|
||||
Required for: `secp256k1`, `ed25519`, `x25519`, `p256`, `aes-256-gcm`,
|
||||
`chacha20-poly1305`, `xchacha20-poly1305`, `argon2id`.
|
||||
|
||||
- [ ] N/A — primitive is not dangerous tier.
|
||||
- [ ] Audit document linked below. Include: reviewer name, scope, findings,
|
||||
sign-off date.
|
||||
<!-- link to audit doc -->
|
||||
|
||||
## Rollback plan
|
||||
|
||||
- [ ] Reverting `crypto.config.ts` and regenerating selects restores `noble`
|
||||
routing with zero other changes.
|
||||
- [ ] Legacy noble impl at `impl/<id>.noble.ts` is untouched by this PR.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] `bun scripts/gen-crypto-selects.ts` has been run and `<id>.select.ts`
|
||||
has been updated to re-export from `./impl/<id>.native.js`.
|
||||
- [ ] `bun scripts/check-crypto-selects.ts` is green.
|
||||
- [ ] `bun scripts/ct-lint.ts` is green.
|
||||
- [ ] `bunx turbo lint typecheck test` is green across every crypto-*
|
||||
package.
|
||||
180
.github/workflows/registry-deploy.yml
vendored
Normal file
180
.github/workflows/registry-deploy.yml
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
name: registry-deploy
|
||||
|
||||
# Builds the @suaveplan/react-ui shadcn registry + Storybook static
|
||||
# and deploys both to ui.suaveplan.com on every push to genesis that
|
||||
# touches the registry sources or the ops scaffolding.
|
||||
#
|
||||
# Required secrets (set via `gh secret set ...`):
|
||||
# OVH_SSH_HOST — OVH hostname or IP
|
||||
# OVH_SSH_USER — scoped deploy user (NOT root)
|
||||
# OVH_SSH_PRIVATE_KEY — Ed25519 private key whose public side is
|
||||
# in the deploy user's authorized_keys
|
||||
#
|
||||
# Security note: this workflow does NOT consume any github.event.*
|
||||
# strings (issue titles, PR bodies, commit messages) inside `run:`
|
||||
# scripts. All variable expansion is from `secrets.*` or job
|
||||
# `outputs.*`, both of which are trusted.
|
||||
#
|
||||
# See ops/registry/README.md for first-time OVH setup.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [genesis]
|
||||
paths:
|
||||
- "packages/react/react-ui/**"
|
||||
- "packages/react/react-ui-theme-*/**"
|
||||
- "packages/react/react-ui-pack-*/**"
|
||||
- "ops/registry/**"
|
||||
- ".github/workflows/registry-deploy.yml"
|
||||
workflow_dispatch: {}
|
||||
|
||||
# Only one deploy at a time per ref. Back-to-back pushes serialise
|
||||
# rather than race; the second push waits for the first to finish.
|
||||
concurrency:
|
||||
group: registry-deploy-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build registry + storybook
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
snapshot_tag: ${{ steps.tag.outputs.tag }}
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: setup bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: build react-ui
|
||||
# The `if` guard is intentional ONLY while @suaveplan/react-ui
|
||||
# does not yet exist on genesis (Wave 2 ships it). Once Wave 2
|
||||
# lands, drop the guard and require the build to succeed.
|
||||
run: |
|
||||
if bunx turbo run build --filter=@suaveplan/react-ui --dry=json | grep -q '"@suaveplan/react-ui"'; then
|
||||
bunx turbo run build --filter=@suaveplan/react-ui
|
||||
else
|
||||
echo "::warning::@suaveplan/react-ui not yet present on genesis; skipping package build"
|
||||
fi
|
||||
|
||||
- name: build registry json
|
||||
run: bun run ops/registry/scripts/build-registry.ts
|
||||
|
||||
- name: build storybook static
|
||||
run: |
|
||||
if [[ -f packages/react/react-ui/package.json ]] && \
|
||||
grep -q '"build-storybook"' packages/react/react-ui/package.json; then
|
||||
cd packages/react/react-ui && bun run build-storybook
|
||||
else
|
||||
echo "::warning::storybook script not yet present; emitting placeholder"
|
||||
mkdir -p packages/react/react-ui/storybook-static
|
||||
printf '<!doctype html><meta charset=utf-8><title>Storybook pending</title><p>Storybook for @suaveplan/react-ui ships in Wave 2.</p>' \
|
||||
> packages/react/react-ui/storybook-static/index.html
|
||||
fi
|
||||
|
||||
- name: stage deploy artifact
|
||||
run: |
|
||||
mkdir -p .deploy/r .deploy/storybook
|
||||
if [[ -d packages/react/react-ui/registry/r ]]; then
|
||||
cp -R packages/react/react-ui/registry/r/. .deploy/r/
|
||||
fi
|
||||
if [[ -d packages/react/react-ui/storybook-static ]]; then
|
||||
cp -R packages/react/react-ui/storybook-static/. .deploy/storybook/
|
||||
fi
|
||||
ls -la .deploy/r .deploy/storybook
|
||||
|
||||
- name: snapshot tag
|
||||
id: tag
|
||||
run: echo "tag=$(date -u +%Y%m%dT%H%M%SZ)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: upload deploy artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: registry-deploy-${{ steps.tag.outputs.tag }}
|
||||
path: .deploy/
|
||||
retention-days: 7
|
||||
if-no-files-found: error
|
||||
|
||||
deploy:
|
||||
name: rsync to ui.suaveplan.com
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
environment:
|
||||
name: production-registry
|
||||
url: https://ui.suaveplan.com/
|
||||
steps:
|
||||
- name: download deploy artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: registry-deploy-${{ needs.build.outputs.snapshot_tag }}
|
||||
path: .deploy/
|
||||
|
||||
- name: configure ssh
|
||||
env:
|
||||
OVH_SSH_PRIVATE_KEY: ${{ secrets.OVH_SSH_PRIVATE_KEY }}
|
||||
OVH_SSH_HOST: ${{ secrets.OVH_SSH_HOST }}
|
||||
run: |
|
||||
if [[ -z "${OVH_SSH_PRIVATE_KEY:-}" || -z "${OVH_SSH_HOST:-}" ]]; then
|
||||
echo "::error::OVH_SSH_PRIVATE_KEY and OVH_SSH_HOST secrets must be set; see ops/registry/README.md"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
printf '%s\n' "${OVH_SSH_PRIVATE_KEY}" > ~/.ssh/registry_deploy
|
||||
chmod 600 ~/.ssh/registry_deploy
|
||||
# Pin host key on first run; refuse silent replacement after.
|
||||
ssh-keyscan -H "${OVH_SSH_HOST}" >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
- name: rsync registry tree (with snapshot)
|
||||
env:
|
||||
OVH_SSH_HOST: ${{ secrets.OVH_SSH_HOST }}
|
||||
OVH_SSH_USER: ${{ secrets.OVH_SSH_USER }}
|
||||
SNAPSHOT_TAG: ${{ needs.build.outputs.snapshot_tag }}
|
||||
run: |
|
||||
rsync -azh --delete \
|
||||
--backup --backup-dir="/srv/registry/.previous/${SNAPSHOT_TAG}/r/" \
|
||||
-e "ssh -i ~/.ssh/registry_deploy -o StrictHostKeyChecking=yes" \
|
||||
.deploy/r/ \
|
||||
"${OVH_SSH_USER}@${OVH_SSH_HOST}:/srv/registry/r/"
|
||||
|
||||
- name: rsync storybook tree (with snapshot)
|
||||
env:
|
||||
OVH_SSH_HOST: ${{ secrets.OVH_SSH_HOST }}
|
||||
OVH_SSH_USER: ${{ secrets.OVH_SSH_USER }}
|
||||
SNAPSHOT_TAG: ${{ needs.build.outputs.snapshot_tag }}
|
||||
run: |
|
||||
rsync -azh --delete \
|
||||
--backup --backup-dir="/srv/registry/.previous/${SNAPSHOT_TAG}/storybook/" \
|
||||
-e "ssh -i ~/.ssh/registry_deploy -o StrictHostKeyChecking=yes" \
|
||||
.deploy/storybook/ \
|
||||
"${OVH_SSH_USER}@${OVH_SSH_HOST}:/srv/registry/storybook/"
|
||||
|
||||
- name: post-deploy health probe
|
||||
run: |
|
||||
for i in 1 2 3 4 5; do
|
||||
if curl -fsSL https://ui.suaveplan.com/health | grep -q '^ok$'; then
|
||||
echo "health probe ok on attempt ${i}"
|
||||
exit 0
|
||||
fi
|
||||
echo "attempt ${i} failed; sleeping 5s"
|
||||
sleep 5
|
||||
done
|
||||
echo "::error::health probe failed after 5 attempts"
|
||||
exit 1
|
||||
|
||||
- name: cleanup
|
||||
if: always()
|
||||
run: rm -f ~/.ssh/registry_deploy
|
||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
wip
|
||||
node_modules
|
||||
.bd*_modules
|
||||
.bd*.tmp_modules
|
||||
*.tmp_modules
|
||||
dist
|
||||
.turbo
|
||||
.DS_Store
|
||||
coverage
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
*.log
|
||||
*.tsbuildinfo
|
||||
.vite
|
||||
.remember
|
||||
.claude/scheduled_tasks.lock
|
||||
.claude/worktrees/
|
||||
__benchmarks__
|
||||
lcov-report
|
||||
playwright-report
|
||||
.playwright-mcp
|
||||
test-results
|
||||
coverage.json
|
||||
44
.markdownlint-cli2.jsonc
Normal file
44
.markdownlint-cli2.jsonc
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
// Sensible defaults for the monorepo. See:
|
||||
// https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md
|
||||
"config": {
|
||||
"default": true,
|
||||
// Long lines are fine in docs that carry code / API refs / paths.
|
||||
"MD013": false,
|
||||
// Allow HTML (we occasionally need <details>, <br>, <sub>, badges).
|
||||
"MD033": false,
|
||||
// Allow duplicate headings under different parents (e.g. "Example"
|
||||
// appearing in both "Basic Example" and "Advanced Example" trees).
|
||||
"MD024": { "siblings_only": true },
|
||||
// Bare URLs are fine in changelogs and tables.
|
||||
"MD034": false,
|
||||
// Allow trailing punctuation in headings (colons etc).
|
||||
"MD026": false,
|
||||
// Enforce fenced-code language hint so docs stay scannable.
|
||||
"MD040": true,
|
||||
// Allow **Bold** sub-section dividers inside API/Methods sections —
|
||||
// promoting them to real headings would inflate the TOC.
|
||||
"MD036": false,
|
||||
// Don't insist on a first-line H1 — we occasionally have YAML front
|
||||
// matter or status badges before the title.
|
||||
"MD041": false
|
||||
},
|
||||
"globs": ["**/*.md", "**/*.mdx"],
|
||||
"ignores": [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/coverage/**",
|
||||
"**/.turbo/**",
|
||||
"**/.git/**",
|
||||
"**/.remember/**",
|
||||
"**/lcov-report/**",
|
||||
"**/CHANGELOG.md",
|
||||
// Archived openspec changes are frozen historical records.
|
||||
"openspec/archive/**",
|
||||
// Plan docs embed inline doc stubs with repeated section headings.
|
||||
"docs/superpowers/plans/**",
|
||||
"docs/superpowers/specs/**",
|
||||
// Installed plugin marketplace files are externally managed.
|
||||
".claude/plugins/suaveplan-plugins/**"
|
||||
]
|
||||
}
|
||||
3
.npmrc
Normal file
3
.npmrc
Normal file
@@ -0,0 +1,3 @@
|
||||
@suaveplan:registry=https://npmjs.fq.io/
|
||||
//npmjs.fq.io/:_authToken=${NPM_TOKEN}
|
||||
strict-ssl=false
|
||||
41
.vscode/settings.json
vendored
Normal file
41
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "vscode.css-language-features"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"files.eol": "\n",
|
||||
"editor.formatOnSave": true,
|
||||
"biome.enabled": true,
|
||||
"biome.requireConfiguration": true,
|
||||
"css.lint.unknownAtRules": "ignore",
|
||||
"editor.codeActions.triggerOnFocusChange": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.biome": "explicit",
|
||||
"source.fixAll.biome": "explicit",
|
||||
"source.organizeImports.biome": "explicit"
|
||||
},
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"eslint.enable": false,
|
||||
"prettier.enable": false,
|
||||
"terminal.integrated.defaultProfile.osx": "zsh",
|
||||
"testing.automaticallyOpenTestResults": "neverOpen",
|
||||
"yaml.customTags": ["!reference sequence"],
|
||||
"npm.packageManager": "bun"
|
||||
}
|
||||
61
.vscode/tasks.json
vendored
Normal file
61
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"type": "shell",
|
||||
"command": "bun run build",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "format",
|
||||
"type": "shell",
|
||||
"command": "bun run format"
|
||||
},
|
||||
{
|
||||
"label": "lint",
|
||||
"type": "shell",
|
||||
"command": "bun run lint"
|
||||
},
|
||||
{
|
||||
"label": "lint:md",
|
||||
"type": "shell",
|
||||
"command": "bun run lint:md"
|
||||
},
|
||||
{
|
||||
"label": "lint:md:fix",
|
||||
"type": "shell",
|
||||
"command": "bun run lint:md:fix"
|
||||
},
|
||||
{
|
||||
"label": "check",
|
||||
"type": "shell",
|
||||
"command": "bun run check"
|
||||
},
|
||||
{
|
||||
"label": "check:all",
|
||||
"type": "shell",
|
||||
"command": "bun run check:all"
|
||||
},
|
||||
{
|
||||
"label": "test",
|
||||
"type": "shell",
|
||||
"command": "bun run test",
|
||||
"group": "test"
|
||||
},
|
||||
{
|
||||
"label": "typecheck",
|
||||
"type": "shell",
|
||||
"command": "bun run typecheck"
|
||||
},
|
||||
{
|
||||
"label": "gate",
|
||||
"type": "shell",
|
||||
"command": "bun run gate"
|
||||
}
|
||||
]
|
||||
}
|
||||
32
AGENTS.md
Normal file
32
AGENTS.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Agent Compliance Checklist
|
||||
|
||||
1. read STATUS.md to be aware of all the libraries we have migrated and have yet to migrate. STATUS.md is the dashboard table — `✅` migrated, `–` yet to be migrated, `❌` not yet written. SUMMARY.md holds the longer descriptions per package; consult it when STATUS.md's one-liner isn't enough. When working on a library, make the very best effort not to duplicate code, but use a suitable library if available. If the library has yet to be migrated, you must prioritise migrating the library first.
|
||||
|
||||
2. read ./openspec/AGENTS.md before doing any work to learn of all the work done and work pending. we are migrating libraries from within ../suaveplan/ (main branch) into this branch (genesis), make sure you adhere to the guidlines set out.
|
||||
|
||||
2a. before authoring ANY OpenSpec change, read `./openspec/AGENTS.md` §0c — it defines the change-folder structure and the mandatory spec-delta format. Spec content goes in `openspec/changes/<id>/specs/<capability>/spec.md` as a requirement/scenario delta; it NEVER goes into `openspec/specs/` until the change archives. Architecture and rationale go in `design.md`, not the spec delta.
|
||||
|
||||
2b. **Spec-first inventory — mandatory before any code.** Before writing a single line of implementation, open `openspec/changes/<id>/specs/<capability>/spec.md` and create a numbered checklist of every `### Requirement` and every `#### Scenario`. You may NOT start coding until this checklist exists. As you implement each requirement, check it off. If a requirement feels hard, inconvenient, or "not needed yet," you must either (a) implement it exactly as written, or (b) open a follow-up OpenSpec change that explicitly scopes it out with architectural justification. **Never silently skip a requirement because tests can pass without it.** This is the single most common cause of spec drift.
|
||||
|
||||
3. These migrations are meant to take existing libraries and elevate them to feature-rich enterprise libraries that are extensible/modular, tree-shakeable. they are not meant to be trimmed down versions, they must retain full functionality. you may split them into loadable plugin libraries that plug into the modularised main migrated library.
|
||||
|
||||
4. Code coverage must be 100% on all code files you work on, with 100% passing. use "bunx turbo lint test typecheck build --filter=@suaveplan/<package_name>"
|
||||
|
||||
5. When you have completed a library, flip its row in STATUS.md from `–` to `✅` and replace the suaveplan summary with a one-line description from the package's README.md. Place the longer summary in the corresponding plugin section of SUMMARY.md.
|
||||
|
||||
6. **Before declaring any library done, verify it against its OpenSpec spec — the package must match the spec, not the other way round.** Past audits found packages that passed their own tests yet silently drifted from the contract. Check every one of these:
|
||||
|
||||
- **Dependencies match the spec allowlist exactly.** If the spec's purity/dependency requirement names an allowed set, `package.json` `dependencies` MUST be a subset of it — no extras. Every declared dependency MUST be imported somewhere in `src/` (no unused deps), and every dependency the spec mandates the package *use* MUST actually be imported, not just declared. Grep to confirm: a dep that appears only in `package.json` is a defect.
|
||||
- **Every requirement is implemented.** Walk each `### Requirement` in the spec and confirm the named classes, functions, constants, spans, counters, histograms, events, and error codes actually exist in `src/` — `grep` for each. A requirement with zero implementing code is a defect even if all tests pass.
|
||||
- **Every registered error code is reachable.** Each code in the package's error metadata MUST be thrown from (or carried on a report by) a real call site exercised by a test. A registered-but-never-used code is a defect.
|
||||
- **Every `**Verified by:**` line resolves.** The spec names exact `file::"test name"` references. Each file MUST exist and contain a test whose name matches *verbatim*. Renaming or consolidating a test without updating the spec breaks traceability — keep them in lockstep.
|
||||
- **Every `.ts` has its sibling `.md`, every feature module has its subpath export.** Confirm the `package.json` `exports` map and the docs-coverage gate before claiming completion.
|
||||
|
||||
Run the full pre-publish gate (`bunx turbo lint typecheck test build --filter=@suaveplan/<pkg>` plus `test:coverage`) — but treat green tests as necessary, not sufficient. The spec is the contract; reconcile the package to it.
|
||||
|
||||
7. **Error codes: always go through `@suaveplan/error`'s `defineCodes` — never hand-write a code string literal.** Every package's error codes MUST be declared with `defineCodes("<NAMESPACE>", ["REASON_A", "REASON_B", …] as const)`. The resulting code string value is the namespaced **`NAMESPACE.REASON`** form (e.g. `defineCodes("AUDIO_CORE", ["PLUGIN_NOT_FOUND"])` yields the value `"AUDIO_CORE.PLUGIN_NOT_FOUND"`, **not** `"AUDIO_PLUGIN_NOT_FOUND"`). You MUST adhere to this everywhere:
|
||||
|
||||
- **In code:** throw `new XxxError(CODES.REASON, …)` using the exported `defineCodes` constant. Never type the raw `"NAMESPACE.REASON"` string by hand anywhere — not in `src/`, not in tests.
|
||||
- **In tests:** assert against the exported code constant (`expect(err.code).toBe(CODES.REASON)`), never against a hand-typed string literal. Hand-typed literals silently rot when the namespace changes.
|
||||
- **In specs:** when an OpenSpec requirement or scenario references a code — whether in prose backticks or a `code === "…"` assertion — it MUST use the exact `defineCodes` output (`NAMESPACE.REASON`). A spec that names `AUDIO_PLUGIN_NOT_FOUND` when the code is really `AUDIO_CORE.PLUGIN_NOT_FOUND` is a defect in the spec.
|
||||
- **On mismatch:** if you find a spec literal that does not match the actual `defineCodes` output, fix the **spec** to match the convention-correct value — do not bend the code's namespace to match a mistaken literal. (`**Verified by:**` test-name strings are human-readable labels and may keep a shortened form, but they must stay verbatim-identical to the real test name.)
|
||||
290
CLAUDE.md
Normal file
290
CLAUDE.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# Suaveplan Monorepo — Genesis
|
||||
|
||||
Bottom-up rebuild at enterprise quality. Branch `genesis` starts empty; packages land in dependency-order waves at `0.1.0` until graduation to `1.0.0`.
|
||||
|
||||
Main branch (`/mnt/s/web/suaveplan`) is reference-only for design intent (`enterprise/*.md`) and historical audit data. Do not pull code from main into genesis — every package is rewritten fresh.
|
||||
|
||||
## Change workflow
|
||||
|
||||
All non-trivial changes go through **OpenSpec**. Before coding:
|
||||
|
||||
1. Read `openspec/project.md` and `openspec/AGENTS.md`.
|
||||
2. Create `openspec/changes/<change-id>/` with `proposal.md`, `tasks.md`, and spec deltas under `specs/<capability>/spec.md` (`## ADDED` / `## MODIFIED` / `## REMOVED Requirements`).
|
||||
3. On ship: fold the delta into `openspec/specs/<capability>/spec.md` and move the folder to `openspec/archive/<yyyy-mm-dd>-<change-id>/`.
|
||||
|
||||
## Stack
|
||||
|
||||
- **Turborepo + Bun** monorepo at `/mnt/s/web/genesis`
|
||||
- **Biome** (lint/format), **Vitest** (tests), **Playwright** (E2E), **TypeScript**
|
||||
- Use `bunx turbo` for turbo commands
|
||||
- Private registry: Verdaccio at `https://npmjs.fq.io/` (auth via `NPM_TOKEN`)
|
||||
|
||||
## Packages
|
||||
|
||||
### Layout (`packages/<category>/<plugin>/<pkg>/`)
|
||||
|
||||
Domain-first 3-level layout: every package lives at `packages/<category>/<plugin>/<pkg>/` where `<category>` is one of the 18 canonical super-categories — `foundation`, `persistence`, `networking`, `communication`, `api-stack`, `identity`, `security-stack`, `observability`, `media`, `ui`, `threejs`, `localization`, `geospatial`, `money`, `commerce`, `testkit`, `devtools`, `gaming` — and `<plugin>` matches a plugin in [`suaveplan-skills`](https://github.com/SuavePlan/suaveplan-skills). Tier (universal / browser / react / r3f / server / python) is per-package via `package.json` (`"browser"` field, peer deps, `engines`), not encoded in the path. Full category → plugin → package map in `scripts/folder-reorg/assignment.yml`; summarised in `MANIFEST.md` at repo root. The super-category vocabulary is closed — adding/removing a category requires an OpenSpec change.
|
||||
|
||||
Add new packages, then run `bun scripts/folder-reorg/generate-manifest.ts` to refresh `MANIFEST.md` (dep graph auto-derived from `workspace:` ranges).
|
||||
|
||||
> **Note:** Detailed package descriptions live in each package's `README.md` and co-located `.md` files. This file describes foundational packages widely depended on; everything else is listed by name only — see `MANIFEST.md` and the package sources for details.
|
||||
|
||||
### Foundation (zero or near-zero deps, widely consumed)
|
||||
|
||||
- **types** — types-only foundation: `Clock`, `IdGenerator`, `Rng`, `MessageResolver`, `EventBusLike`, `LoggerLike`, `StorageLike`, `DatabaseLike`, `HashLike`, `TimersLike`, shared option bundles + cross-package crypto contract (`Authority`, `KeySlotPublic`/`Private`, `SlotId`, `DomainTag`, `AuthorityVerifier`, `AeadOps`, `AeadAlgorithm`). Zero runtime, zero deps. Permitted as testing-infra dev dep (rule 8 exception).
|
||||
- **error** — `SuaveplanError` base, stable code registry via `defineCodes`, cause-chain traversal, Zod-validated wire format, redaction, OTel span-event adapter, i18n resolver hook. Deps: `tslib`, `zod`.
|
||||
- **events** — typed event bus: pub/sub FIFO, interceptors, bounded-queue backpressure, idempotent listeners. Deps: `error`, `types`.
|
||||
- **env** — type-safe env loading with schema validation, dotenv + object loaders, `maskSensitive`, `createEnv`. Deps: `error`, `types`, `zod`.
|
||||
- **config** — layered config (5 layers: defaults/file/object/env/CLI), Zod validation, dot-path get/has, hot reload, EventBus watch. Deps: `error`, `types`, `events`, `env`, `zod`.
|
||||
- **logger** — structured logger: level filtering, OTel trace enrichment, sampling, redaction, pluggable sinks. Deps: `error`, `types`.
|
||||
- **opentelemetry** — OTel SDK utilities: `NoopSdk`, `SdkFactory`, W3C propagation, span/metrics utilities, resource builder.
|
||||
- **typescript-config** — shared TSConfig presets. No runtime.
|
||||
- **overview** — machine-readable monorepo manifest with Zod schemas + graph utilities. Leaf.
|
||||
- **api-schemas** — shared Zod request/response schemas. Leaf.
|
||||
|
||||
### Testing infrastructure (no `@suaveplan/*` runtime deps; rule 8)
|
||||
|
||||
- **testing** — runner-agnostic utilities (FakeClock, Factory, FixtureManager, MemoryStore, builders). Owns the `./runner` conditional-export wrapper resolving to `vitest` (default) or `bun:test` (under `bun` condition) — see rule 11.
|
||||
- **testing-dom**, **testing-e2e**, **testing-integration** — DOM helpers + dual-runner adapters; Playwright config factory; `ServiceFixture`/`TestEnvironment`/`HealthPoller`.
|
||||
- **testing-integration-{surrealdb, redis, postgres, mailpit, typesense, garage-s3, step-ca}** — real-service fixtures via testcontainers; each provides env-bridge + health probe + injectable container/exec/fetch factories.
|
||||
|
||||
### Crypto family
|
||||
|
||||
Foundation (widely consumed):
|
||||
|
||||
- **crypto-utils** — hex/base64/base58, byte helpers, constant-time equality, random bytes, zeroize, Merkle, KAT/fuzz harnesses, `PrimitiveId` registry.
|
||||
- **crypto-hash** — sha256/384/512, hmac, hkdf, pbkdf2, argon2id. Noble + native, build-time selection.
|
||||
- **crypto-curve** — secp256k1, ed25519, x25519, p256, p384, p521. Noble + native.
|
||||
- **crypto-cipher** — aes-256-gcm, chacha20-poly1305, xchacha20-poly1305. Nonce caller-supplied.
|
||||
- **crypto** — multi-algorithm façade over the four primitive packages. Engine-pluggable.
|
||||
- **crypto-rsa** — RFC 8017 PKCS#1 v2.2: PKCS1v15+PSS sign/verify, OAEP, all 8 key formats, FIPS 186-5 keygen, RFC 7638 thumbprints, blinding-by-default, pluggable engine, HSM bridge. Gates `x509`/`jws`/`webauthn`.
|
||||
|
||||
Specialty: **crypto-core**, **crypto-sodium**, **crypto-browser**, **crypto-cli** (`keychain` binary), **crypto-bin** (`suaveplan-hash-password`), **crypto-vault** (BIP39 + Argon2id + 2SKD + HKDF + SRP-6a + SecureBuffer; gates `vault`).
|
||||
|
||||
### Drive (file VFS)
|
||||
|
||||
- **drive** — modular file-VFS core. Consumes `AbstractStorage` directly via structural `DriveStorage` view. Plugin slot + middleware chain. Template hydration (`hydrateFrom`/`getKeyStatus`/`presence`). 11 `DRIVE_*` codes.
|
||||
- **drive-template** — server-signed dehydrated descriptor for lazy hydration. 8-step validation pipeline, Ed25519 over canonical bytes, `TemplateNonceLru`, CDN URL builder, upload-token signing, retroactive key-grant validation. 10 `DTP_*` codes.
|
||||
- **drive-mesh-protocol** — wire protocol for chunk discovery/request/delivery. `ChunkRequest` union (whole/specific/bitset), `GenerationTracker`, `AdvertiseThrottle`, `TrustMap` with sample-verify. 10 `DMP_*` codes.
|
||||
- **drive-plugin-chunks** — chunk addressing: 164-byte fixed-width header, XChaCha20-Poly1305 streaming AEAD, Ed25519 inline-hint signing, sparse-file + chunk-keys layouts, server-signed manifest. 12 `CHUNKS_*` codes.
|
||||
|
||||
Consumers: **drive-sharing**, **drive-transport-https**, **drive-transport-webrtc**, **drive-webrtc** (browser-tier P2P mesh plugin), **drive-mesh** (P2P orchestrator: `PeerRegistry`, `GossipEngine`, `ParallelPuller`, `ServerFallback`), **drive-react-sharing** (React 19 hooks + accessible components).
|
||||
|
||||
### Vault
|
||||
|
||||
- **vault** — orchestration: `open`/`unlock`/`lock` FSM, encrypted single-blob container via `DriveLike`, items/attachments/shares/sync, `OpaqueKeyHandle`, forward-only migrations, bitpacked manifest, 25+ error codes.
|
||||
- **vault-sync** — sync + conflict resolution: 3 merge strategies (lww/append/atomic) with vector clocks, RFC 6902 diff/patch, `SyncEngine`/`SyncQueue`, payload encryption with AAD + 10 MiB cap. 10 error codes.
|
||||
|
||||
### Network
|
||||
|
||||
- **network** — plugin-based core. `NetworkPlugin` lifecycle, `NetworkNode`, `MessageEnvelope` Zod schema, `MessageRouter` (unicast/broadcast/fan-out), `PluginManager`, `NetworkBus`, `VersionInfo`/`VersionManager`/`VersionNegotiator`, `NetworkTimestamp`, `NetworkEvents` registry, 5 `NET_*` codes. Concrete transports/governance/registry/failover/topology all live in sibling `network-plugin-*` packages.
|
||||
|
||||
### i18n family
|
||||
|
||||
- **i18n** — slim integration core (post-Wave-6 split). Owns `MessageCatalog`, `createI18n`/`I18nInstance`, locale negotiation, server middleware, `defaultFormatBridge`. Re-exports siblings for back-compat.
|
||||
- **i18n-shared** — universal-tier shared atoms: locale constants, `LOCALE_BCP47_MAP`, `RTL_LOCALES`, `I18N_CODES` (18), `I18nError`, `./testing` `runBackendConformance` harness.
|
||||
- **i18n-icu-parser** — ICU MessageFormat parser (tokenizer + AST + CLDR plurals).
|
||||
- **i18n-detector** — 9-detector chain (querystring, userPreference, cookie, localStorage, htmlLang, acceptLanguage, navigator, cloudflareIpGeo, fallback) + `bcp47Negotiate` + RTL helpers.
|
||||
- **i18n-backend-{memory, http, storage, chained, fs-multifile}** — translation backends; all conform to `runBackendConformance` harness. `fs-multifile` is server-tier.
|
||||
- **i18n-formatter-{number, currency, date, relative-time, list, display-name, collator, segmenter}** — `Intl.*` adapters plugged into `defaultFormatBridge`.
|
||||
|
||||
### Time, sequencing, validation, control flow
|
||||
|
||||
- **datetime**, **timing**, **cron** (gates `job-scheduler`), **job-scheduler** (consumes `cron` + `retry`), **validation**, **retry**, **idempotency**, **fsm**, **schema-registry**, **feature-flags**, **workflow** (universal-tier pipeline orchestration: fluent builder, sequential / parallel / conditional steps, saga compensation, retry middleware, hook registry, transition-data pipeline, Zod-validated `WorkflowConfig`, injectable `Clock`, six `WORKFLOW.*` codes, OTel counters + histogram). See per-package READMEs.
|
||||
|
||||
### Rules
|
||||
|
||||
- **rules** — universal-tier rule engine + validators + actions + hooks + serializer + optimizer. 24 codes. Discriminated `Rule` union, `RuleBuilder`, 33 built-in operators.
|
||||
- **rules-hono** — Hono middleware suite (6 factories: rule, validation, accessControl, responseValidation, rateLimit, combine). Hono peer dep.
|
||||
|
||||
### Security / vault leaves
|
||||
|
||||
- **slsa**, **security-codes**, **data-masking**, **suppression**, **macos-keychain**, **hibp**, **password-generator**, **password-strength**, **x509**, **jws**, **tpm-struct**, **cbor**, **otp**, **srp**, **webauthn** — see per-package READMEs. `webauthn` consumes cbor/x509/jws/tpm-struct/crypto-{rsa,curve}; covers all 7 attestation formats.
|
||||
|
||||
### Serialization / data shape
|
||||
|
||||
- **serialize**, **diff**, **pagination**, **mime**, **templates**, **bitpacker**, **bitpacker-compressed**, **packer**, **packer-browser** (browser-tier runtime for packer archives: SW interceptor + `PackedResourceManager` + Ed25519 signature verification; no React dep), **react-packer** (React 19 companion: `usePackedResource` / `usePackedImage` / `usePackedAudio` / `usePackedVideo` / `usePackedFont` hooks + `PackedResourceProvider` context; consumes `@suaveplan/packer-browser`; React peer-dep only).
|
||||
|
||||
### File / media / domain utilities (mostly leaves)
|
||||
|
||||
- **metadata**, **exif**, **dmarc**, **sitemap**, **domain-normalizer**, **dedup**, **password-formats**, **barcode**, **qrcode**, **compress-image**, **font-subset**, **font-convert**, **font-compress** (bin: `suaveplan-font-compress` — TTF/OTF/WOFF/WOFF2 → BTF pipeline).
|
||||
|
||||
### Geo / astronomy / tides
|
||||
|
||||
- **geo**, **geo-planet**, **geo-earth**, **solar-system**, **stars**, **tides**, **universe** — abstract spherical geometry → planet config → Earth specifics → injectable astronomy/star/tidal providers aggregated through `universe`.
|
||||
|
||||
### Network clients / IO / DB / installer
|
||||
|
||||
- **oauth-client**, **webhook-client**, **readline**, **cli**.
|
||||
- **db** (adapter foundation), **db-sqlite** (universal: `bun:sqlite` on Bun, `@sqlite.org/sqlite-wasm` in browser).
|
||||
- **installer** (plugin host, bin `suaveplan-install`), **install-agent-skills** (skill bundle installer).
|
||||
|
||||
### Games (universal-tier)
|
||||
|
||||
- **ecs** — archetype-based Entity Component System: `Uint32Array` BitSet, column-oriented archetype storage with O(1) swap-remove, phase+priority `SystemScheduler` (`preUpdate→update→postUpdate→render`), entity-ID recycling with generation counters, Zod-validated component schemas, `EventBus<WorldEventMap>` entity lifecycle events, OTel counter (`ecs.entity.lifecycle_total`) + histogram (`ecs.world.execute_duration_ms`), `WorldSnapshot` round-trip. 7 `ECS.*` codes. Deps: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/opentelemetry`, `tslib`, `zod`.
|
||||
|
||||
### Contacts
|
||||
|
||||
- **contacts** — universal-tier contact and group management: hierarchical n-level groups (adjacency + materialised path), channel-agnostic contacts with dedup, many-to-many memberships with ancestor count propagation, audience resolution with descendant walk + dedup + filters, contact merger, CSV/JSON import-export, injectable `Clock` + `IdGenerator`, Zod input boundaries, six OTel counters + three histograms, typed `ContactsError` hierarchy with stable `CONTACTS.*` code registry.
|
||||
- **macos-contacts** — macOS Contacts.app bridge: AppleScript reader, `Contact` ↔ macOS `Person` mapping, deduplication via `@suaveplan/contacts/dedup`, push/pull/sync, vCard import-export.
|
||||
- **api-sandbox-vm** — `vm.Context`-based sandbox host (Stage 8.2). Builds a Proxy-guarded `vm.Context` with frozen-globals allowlist (`URL`/`URLSearchParams`/`TextEncoder`/`TextDecoder`/`structuredClone` + logger-routed `console` + trimmed `crypto`), `vm.SourceTextModule` plugin loader with refuse-everything linker (rejects `node:*`, bare specifiers, parent traversal), `eval`/`Function` traps, host-proxied fs/fetch with permission gating, consolidated `__suaveplan` capability receiver, in-package `PermissionChecker` default from `PermissionsManifest`, idempotent `dispose()`, OTel `sandbox_vm.{boot_duration_ms,eval_rejected_total,module_import_rejected_total}` + shared `plugin.permission_denied_total`, 5-code `SAND_VM.*` registry, `./testing/runVmSandboxConformance` harness. Requires Node ≥ 20 with `--experimental-vm-modules`.
|
||||
- **api-plugin-contacts** — Contacts + groups HTTP plugin for `@suaveplan/api`. Mounts `/contacts` and `/groups` (with `/groups/:id/members`, `/groups/:id/tree`, `/groups/:id/move`, `/groups/:id/resolve`) on a Hono `OpenAPIHono` backed by SurrealDB implementations of the `@suaveplan/contacts` `ContactStore` / `GroupStore` / `MembershipStore` interfaces. Bulk-import with per-row error reporting, repository-level `maxGroupDepth` enforcement, six OTel instruments (4 counters + 2 histograms), typed Hono `ContextVariableMap` + `ServiceRegistry` augmentations, and the 12-code `CONTACTS_API.*` error namespace.
|
||||
|
||||
### API sandboxing
|
||||
|
||||
- **api-sandbox-wasm** — Stage-8 WebAssembly sandbox host for sandboxed-tier plugins. Instantiates a guest WASM module against a `wasi_snapshot_preview1` snapshot built from the plugin's `PermissionPolicy`: read-only WASI preopens from `permissions.fs[]` (with `rw:` opt-in), outbound socket allowlist from `permissions.fetch[]` (`sock_connect` gate raises `SAND_WASM.OUTBOUND_DENIED` BEFORE any OS socket), and capability host imports under `suaveplan_caps` permission-gated per call (denied → `WASI_ERRNO.ACCES` + `plugin.permission_denied_total{plugin,capability,reason}`). Owns a 4-code `SAND_WASM.*` registry, a `runWasmSandboxConformance` harness on `./testing`, and a hand-encoded fixture WASM builder so the test suite needs no `wabt` build step. `WasmSandboxHandle.dispose()` idempotent.
|
||||
|
||||
## Rules
|
||||
|
||||
### Critical (never compromise)
|
||||
|
||||
1. **NEVER run `tsc` directly** — use Vite/tsup via `bunx turbo build`. Bun runs `.ts` natively.
|
||||
2. **NEVER use mocks** — real services only (no `vi.mock`/`jest.mock` unless absolutely necessary).
|
||||
3. **Bun `overrides` with exact versions.**
|
||||
4. **NEVER re-export** external deps for convenience — import directly from source.
|
||||
5. **NEVER auto-commit/push** without user approval — never use `--no-verify`.
|
||||
6. **Always use absolute paths** in tool calls.
|
||||
7. **Read files before editing** — understand context first.
|
||||
8. **Testing-infrastructure packages** (`testing`, `testing-dom`, `testing-e2e`) must not depend on any `@suaveplan/*` **runtime** package — external catalog deps only. Sole exception: `@suaveplan/types` (types-only, byte-size-test guarded).
|
||||
9. **NEVER write `// biome-ignore` (or any static-analysis suppression comment) in migrated/new code.** Stale rule names → hard parse-error fails; right names that no longer match → `Suppression comment has no effect` warnings. Both block CI. To exercise a deliberate violation, use a coding pattern biome cannot statically reject. Full rule: `openspec/AGENTS.md §2a`.
|
||||
10. **NEVER write `export *` in any non-root barrel.** Feature-module barrels MUST use explicit named re-exports. Exempt: generated `*.select/index.ts` forwarders, single-line `*.native/`/`*.noble/` impl-variant forwarders, namespace re-exports. Package roots MAY currently use `export *` but new ones SHOULD prefer namespace exports. **Migrate `export *` → named exports as part of the migration commit, not a follow-up.** Full rule: `openspec/AGENTS.md §6a`.
|
||||
11. **NEVER import test-runner primitives directly from `vitest` or `bun:test` in non-exempt packages.** Test files MUST import from `@suaveplan/testing/runner`. Exempt allowlist (5): `testing`, `testing-dom`, `testing-e2e`, `testing-integration`, `testing-integration-step-ca`. CI gate: `bun scripts/lint/check-test-runner-imports.ts`. Verification: `grep -rnE "from ['\"](vitest|bun:test)['\"]" packages/<category>/<plugin>/<pkg>/src/` → zero output. Full rule: `openspec/AGENTS.md §7`.
|
||||
|
||||
### Git safety (non-negotiable — code has been lost twice on main)
|
||||
|
||||
Before ANY git operation: run `git status` in **every** worktree (`git worktree list`) and confirm each is clean or its state is understood.
|
||||
|
||||
**NEVER run these without explicit per-use user approval:**
|
||||
|
||||
- `git filter-repo` / `git filter-branch` — rewrites history, destroys staged + working tree.
|
||||
- `git push --force` / `--force-with-lease` — only after user explicitly says "force push".
|
||||
- `git reset --hard`, `git clean -f[d]`, `git rebase`, `git stash drop`/`clear`, `git gc`/`prune`, `git checkout -- <file>`.
|
||||
|
||||
**Large-file push errors:** stop, do NOT rewrite history. Run `git status`/`git stash list` in all worktrees first, then ask the user (`.gitignore` + new commit, Git LFS, or manual removal).
|
||||
|
||||
Staged changes are irreplaceable — treat them as more precious than committed history.
|
||||
|
||||
**Worktree discipline** — long-lived worktrees are never the answer.
|
||||
|
||||
- Before opening a worktree or dispatching a parallel agent: `git worktree list` and `git status` in each existing one. Account for every entry: active agent, work-pending-merge, or stale-and-prune.
|
||||
- After a worktree-based agent ships (commit on a `feat/*` branch), remove the worktree (`git worktree remove`) so the count returns to baseline. The branch + commit survive; the directory does not need to.
|
||||
- The baseline is **one worktree** (the main checkout). Anything beyond that is an open ticket waiting to be closed.
|
||||
- Run `bun scripts/worktree-prune.ts` to audit + prune in one command. It's idempotent and refuses to remove worktrees whose HEAD commit isn't reachable from any branch ref (so it never loses work).
|
||||
|
||||
**Worktree-isolated agents: never edit the main checkout.** When dispatched into an isolated worktree at `.claude/worktrees/agent-<id>/`, every file operation MUST target that worktree path, never `/home/paul/web/genesis/`. The hazards are real and observed:
|
||||
|
||||
1. First action in every agent session: `pwd && git worktree list` to confirm you're in the right tree. If `pwd` shows `/home/paul/web/genesis` instead of a `.claude/worktrees/agent-*` path, STOP and report — you are not in an isolated worktree.
|
||||
2. Use **relative paths** (`packages/...`, `openspec/...`) for every read/edit/write. Avoid absolute paths in tool calls unless reading reference material from outside the worktree (e.g. main-branch source at `/mnt/s/web/suaveplan/...`).
|
||||
3. Never `cd` to `/home/paul/web/genesis/` from a worktree. The Read/Edit/Write tools resolve relative paths against the working directory of the bash session, which starts in the worktree.
|
||||
4. Before committing: `git rev-parse HEAD` and `git rev-parse --show-toplevel` must show your worktree's branch + path, not main's. If either is wrong, your work landed in the wrong place — recover with `git status` in BOTH locations, then move the changes back via patch (`git diff > /tmp/work.patch`, `cd $WORKTREE`, `git apply /tmp/work.patch`).
|
||||
|
||||
Cross-worktree leakage has been the failure mode in three recent agent sessions — wasted ~1 hour each time. This rule is the prevention.
|
||||
|
||||
**§0a Sync — worktrees fork from a stale base.** Empirically the harness's `isolation: "worktree"` may fork from an earlier session-stable ref rather than current `genesis` HEAD. This bit Wave B (5 agents all forked from a pre-Wave-A commit). Prevention: every parallel-dispatch prompt MUST include a §0a Sync block that fast-forwards the worktree to a known base SHA before any work begins:
|
||||
|
||||
```bash
|
||||
EXPECTED_BASE=<orchestrator-supplied-sha>
|
||||
if ! git merge-base --is-ancestor "$EXPECTED_BASE" HEAD; then
|
||||
git fetch /home/paul/web/genesis genesis
|
||||
git merge FETCH_HEAD --no-edit
|
||||
fi
|
||||
git merge-base --is-ancestor "$EXPECTED_BASE" HEAD || { echo "STILL OUT OF DATE"; exit 1; }
|
||||
```
|
||||
|
||||
The orchestrator passes the expected SHA at dispatch time. Agents that can't reach it STOP and report rather than proceeding Wave-N-blind. Without this gate, agents re-invent interfaces from earlier merges and produce work that doesn't integrate.
|
||||
|
||||
### Workflow
|
||||
|
||||
1. **TaskCreate for multi-step tasks** (>3 steps).
|
||||
2. **Complete all implementations** — no stubs, no placeholders, no TODOs, no mocks in production. Missing dep? Add it and wire the real implementation.
|
||||
3. **Build feature-rich, production-grade packages** — comprehensive enterprise-level over minimal MVP.
|
||||
4. **Professional honesty** — no marketing language or fake metrics.
|
||||
5. **Preserve full public surface** during rewrites. Trim ONLY when duplicated in-repo or replaceable by an existing `@suaveplan/*` library.
|
||||
|
||||
### Web App & Page Verification
|
||||
|
||||
When generating client-side pages/web apps, verification is NOT optional and a 200 response is NOT proof of correctness:
|
||||
|
||||
1. **Docker staging environment** — mirror production before declaring done.
|
||||
2. **Full Playwright E2E sweep** — every page, every component, every navigation.
|
||||
3. **Never trust HTTP 200 alone** — verify rendered DOM contains expected content.
|
||||
4. **Inspect browser console** — any unexpected error or warning fails the check.
|
||||
5. **Inspect network activity** — failed requests, missing assets, CORS, mixed content.
|
||||
6. **Zero-defect push to live** — fix root cause first.
|
||||
|
||||
### Package Lifecycle
|
||||
|
||||
- **New packages**: add to listing, then `bun scripts/folder-reorg/generate-manifest.ts`.
|
||||
- **Editing**: every package starts at `0.1.0`; bump only at `1.0.0` graduation.
|
||||
- **Pre-publish gate (non-negotiable)**: `bunx turbo lint typecheck test --filter=@suaveplan/<pkg>` MUST be green AND `bunx turbo test:coverage --filter=@suaveplan/<pkg>` must meet 100% on touched modules. Full-package `turbo test` required — single-file `vitest run` is not a substitute.
|
||||
- **After verification passes**: `bun scripts/publish.ts --package @suaveplan/<pkg>`.
|
||||
|
||||
### Package Structure (library packages)
|
||||
|
||||
Five files per feature module, co-located:
|
||||
|
||||
```text
|
||||
src/category/
|
||||
feature-name/
|
||||
index.ts # Barrel — explicit named re-exports (rule 10)
|
||||
feature-name.ts # Implementation
|
||||
feature-name.test.ts # Vitest tests
|
||||
feature-name.types.ts # Type definitions (no .md required)
|
||||
feature-name.md # Co-located docs (required)
|
||||
```
|
||||
|
||||
Special: `types/` (shared types, flat), `utils/` (utilities, flat with tests + .md), `constants/index.ts` (simple exports, no tests/docs).
|
||||
|
||||
**No flat modules at `src/` root.** Every implementation file paired with a `.test.ts` MUST live in a `src/<feature>/` folder, not flat at `src/` root. The only flat entries permitted at `src/` root are `index.ts` (package barrel) and the three exempt directories above. This includes `error-codes`, `telemetry`, `schemas`, and similar "single-concept" modules — they get their own folder + `index.ts` barrel just like any other feature. Empirically this gets cut by agents shortcutting "atomic" modules to flat siblings of `index.ts`; lint+typecheck+coverage all accept it. CI gate: `bun scripts/lint/check-feature-layout.ts` (wired into `bunx turbo lint`).
|
||||
|
||||
Subpath exports in `package.json` for tree-shaking: `./category/feature/feature`.
|
||||
|
||||
**README layout:** one `README.md` at package root only. No category-level READMEs inside `src/<category>/`.
|
||||
|
||||
### Quality
|
||||
|
||||
- **100% test coverage** — `bun run test:coverage --filter=@suaveplan/<pkg>` (`provider: 'istanbul'`).
|
||||
- **100% docs coverage** — `bun run docs:check --filter=@suaveplan/<pkg>`.
|
||||
- **~250 lines** soft / **300 lines** hard limit per code file, **500 lines** max per test file.
|
||||
- `.test.ts` for Vitest, `.spec.ts` for Playwright E2E.
|
||||
- Always add `{ timeout: 5000 }` (30000 for async) to `it()`/`test()`.
|
||||
- Root cause analysis — never skip/disable tests.
|
||||
- Every React component needs `.stories.tsx` (default + playground + variants).
|
||||
|
||||
### Documentation (co-authored `.md` per implementation file)
|
||||
|
||||
Every `.ts`/`.tsx` MUST have a sibling `.md`. Exclusions: `.test.ts`, `.test.tsx`, `.spec.ts`, `.types.ts` (inside a feature dir), `.stories.tsx`, barrel `index.ts`, `constants/index.ts`.
|
||||
|
||||
Required sections: **Purpose** (≥2 sentences), **Features**, **Usage: Basic**, **Usage: Advanced**, **API Reference**, **Implementation Notes**.
|
||||
|
||||
Hard constraints: ≥200 words excl. code; both simple AND advanced examples mandatory; code must compile with real imports; no marketing/placeholder text; update docs when code changes.
|
||||
|
||||
### Vitest / WSL2
|
||||
|
||||
- **Use `createTestConfig()` from `@suaveplan/testing/config`** for vitest configs. For configs needing aliases/plugins, write standalone with `pool: 'forks'` + `setupFiles: ['@suaveplan/testing/polyfills']`.
|
||||
- **NEVER `mergeConfig` with `baseVitestConfig`** — missing polyfills cause `crypto is not defined`.
|
||||
- **`vmThreads` and `threads` pools both fail in Bun on WSL2** — use default `forks`.
|
||||
- **`fileParallelism: false`** required for browser tests.
|
||||
- **SurrealDB**: check `error?.code === 'ConnectionRefused' || error?.message?.includes('Unable to connect')`.
|
||||
|
||||
### Code Organization
|
||||
|
||||
- **Universal**: ZERO browser/React deps | **Browser**: React/DOM ok | **Server**: Node.js ok.
|
||||
- Co-locate tests with source, `.md` docs per implementation file.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `workspace:^` for internal deps, Bun catalogs for external.
|
||||
- `"sideEffects": false` on all pure library packages.
|
||||
|
||||
## Quick Commands
|
||||
|
||||
```bash
|
||||
bunx turbo test --filter=@suaveplan/<pkg> # Test one package
|
||||
bunx turbo test:coverage --filter=@suaveplan/<pkg> # Coverage for one package
|
||||
bunx turbo lint --filter=@suaveplan/<pkg> # Lint one package
|
||||
bunx turbo typecheck --filter=@suaveplan/<pkg> # Type check one package
|
||||
bunx turbo lint typecheck test --filter=@suaveplan/<pkg> # Pre-publish gate
|
||||
bunx turbo test # All tests (no DB)
|
||||
bunx turbo lint typecheck # All lint+types
|
||||
```
|
||||
275
FOLDERS.md
Normal file
275
FOLDERS.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Package Categories
|
||||
|
||||
> One-word category folders for `packages/`. No hyphens. Every `@suaveplan/*` package lives in exactly one category.
|
||||
|
||||
## api
|
||||
|
||||
API framework, gateway, shared utilities, and domain-agnostic plugins.
|
||||
|
||||
**Existing:** `api-schemas`
|
||||
|
||||
**Pending:** `api` · `api-gateway` · `api-plugin-admin` · `api-plugin-analytics` · `api-plugin-audit` · `api-plugin-auth` · `api-plugin-campaigns` · `api-plugin-cms` · `api-plugin-database` · `api-plugin-events` · `api-plugin-export` · `api-plugin-file-upload` · `api-plugin-graphql` · `api-plugin-import` · `api-plugin-rate-limit` · `api-plugin-search` · `api-plugin-site` · `api-plugin-social` · `api-plugin-sse` · `api-plugin-tenant` · `api-plugin-user` · `api-plugin-webhook` · `api-shared`
|
||||
|
||||
## audio
|
||||
|
||||
Audio engines, effects, controllers, DMX lighting, sprite packing, metadata, and fingerprinting.
|
||||
|
||||
**Existing:** `audio-metadata` · `voice`
|
||||
|
||||
**Pending:** `audio-controllers` · `audio-engine` · `audio-plugins-fx` · `audio-sprite-packer` · `dmx` · `python-audio-fingerprint` · `r3f-audio` · `r3f-dmx` · `react-audio` · `react-audio-fingerprint` · `react-audio-player` · `react-audio-stream`
|
||||
|
||||
## auth
|
||||
|
||||
Authentication, authorization, identity, and tenant-scoped credential management.
|
||||
|
||||
**Existing:** `auth` · `jwt-tenant` · `macos-keychain` · `oauth-client` · `permissions` · `webauthn`
|
||||
|
||||
**Pending:** `react-auth` · `react-auth-whitelabel` · `react-permissions`
|
||||
|
||||
## barcode
|
||||
|
||||
Barcode and QR code generation, parsing, and validation.
|
||||
|
||||
**Existing:** `barcode` · `qrcode`
|
||||
|
||||
## bitpacker
|
||||
|
||||
Bit-level binary packing family. Dedicated folder because the ecosystem is large and vertically integrated.
|
||||
|
||||
**Existing:** `bitpacker` · `bitpacker-compressed` · `bitpacker-crypto` · `bitpacker-delta` · `bitpacker-game` · `bitpacker-network` · `bitpacker-schema` · `bitpacker-strategies` · `bitpacker-streaming` · `bitpacker-validation`
|
||||
|
||||
**Pending:** `packer-browser`
|
||||
|
||||
## commerce
|
||||
|
||||
Payments, currency, banking, and booking.
|
||||
|
||||
**Pending:** `api-plugin-asset-register` · `api-plugin-bank` · `api-plugin-banking` · `api-plugin-payment` · `bank` · `banking` · `booking` · `currency` · `currency-providers` · `money` · `payment` · `payment-providers` · `react-bank`
|
||||
|
||||
## contacts
|
||||
|
||||
Contact and group management, address books, and macOS Contacts integration.
|
||||
|
||||
**Pending:** `api-plugin-contacts` · `contacts` · `macos-contacts` · `react-contacts`
|
||||
|
||||
## core
|
||||
|
||||
Core primitives and utilities that the rest of the monorepo builds on: configuration, environment, errors, schemas, types, and shared utilities.
|
||||
|
||||
**Existing:** `config` · `env` · `error` · `schema-registry` · `types` · `typescript-config` · `utils`
|
||||
|
||||
## crypto
|
||||
|
||||
Cryptographic primitives, protocols, and credential tooling. Ciphers, curves, hashes, signing, key exchange, and the crypto vault layer.
|
||||
|
||||
**Existing:** `crypto` · `crypto-bin` · `crypto-browser` · `crypto-cipher` · `crypto-cli` · `crypto-core` · `crypto-curve` · `crypto-hash` · `crypto-rsa` · `crypto-sodium` · `crypto-utils` · `crypto-vault`
|
||||
|
||||
**Pending:** `api-plugin-vault` · `cryptocurrency` · `keys` · `react-crypto`
|
||||
|
||||
## data
|
||||
|
||||
Data manipulation and transformation: byte utilities, deduplication, diffing, JSON, MIME, packing, pagination, serialization, templates, and validation.
|
||||
|
||||
**Existing:** `bytes` · `data-masking` · `dedup` · `diff` · `json` · `mime` · `packer` · `pagination` · `serialize` · `templates` · `validation`
|
||||
|
||||
## database
|
||||
|
||||
Relational and NoSQL adapters, migration tooling, and seeding.
|
||||
|
||||
**Existing:** `db` · `db-sqlite` · `migration` · `surrealdb`
|
||||
|
||||
**Pending:** `seed`
|
||||
|
||||
## docs
|
||||
|
||||
Documents, sitemaps, PDF generation, and documentation site generators.
|
||||
|
||||
**Existing:** `compress-svg` · `sitemap`
|
||||
|
||||
**Pending:** `api-docs` · `docs` · `pdf` · `react-docs` · `react-pdf` · `react-rich-text`
|
||||
|
||||
## drive
|
||||
|
||||
Virtual filesystem, P2P mesh, transport layers, and sharing plugins.
|
||||
|
||||
**Existing:** `drive` · `drive-mesh` · `drive-mesh-protocol` · `drive-plugin-chunks` · `drive-react-sharing` · `drive-sharing` · `drive-template` · `drive-transport-https` · `drive-transport-webrtc` · `drive-webrtc`
|
||||
|
||||
## emails
|
||||
|
||||
Email delivery, templates, DMARC, and suppression lists.
|
||||
|
||||
**Existing:** `dmarc` · `domain-normalizer` · `suppression`
|
||||
|
||||
**Pending:** `email-processing` · `email-templates` · `emailer`
|
||||
|
||||
## events
|
||||
|
||||
Typed event bus, transport bridges, schema validation, OTel companions, and event sourcing.
|
||||
|
||||
**Existing:** `event-sourcing` · `events` · `events-metrics` · `events-otel` · `events-schema` · `events-transport`
|
||||
|
||||
**Pending:** `broadcast-channel` · `message-bridge`
|
||||
|
||||
## flow
|
||||
|
||||
Control flow, caching, queues, scheduling, rate limiting, retry, rules engines, and streams.
|
||||
|
||||
**Existing:** `cache` · `cbor` · `crdt` · `idempotency` · `job-scheduler` · `queue` · `queue-distribution` · `queue-distribution-http` · `queue-distribution-worker-thread` · `rate-limiter` · `retry` · `rules` · `rules-hono` · `streams` · `undo`
|
||||
|
||||
## fonts
|
||||
|
||||
Font compression, conversion, subsetting, decompression, and React font loading hooks.
|
||||
|
||||
**Existing:** `font-compress` · `font-convert` · `font-subset`
|
||||
|
||||
**Pending:** `font-decompress` · `react-font-decompress`
|
||||
|
||||
## forms
|
||||
|
||||
Form components, builders, wizards, file uploads, date pickers, and color pickers.
|
||||
|
||||
**Existing:** `react-forms`
|
||||
|
||||
**Pending:** `form-builder` · `react-admin-forms` · `react-calendar` · `react-color-picker` · `react-file-upload` · `react-wizard`
|
||||
|
||||
## games
|
||||
|
||||
Game engine systems, simulation, mechanics, AI, progression, and world building.
|
||||
|
||||
**Pending:** `animation-events` · `api-plugin-anticheat` · `api-plugin-game-persistence` · `api-plugin-game-server` · `api-plugin-matchmaking` · `behavior-tree` · `bone-mapping` · `building` · `character-physics` · `character-vehicle-picker` · `chat` · `combat` · `controller` · `crafting` · `day-night` · `destructibles` · `dialogue` · `economy` · `ecs` · `emotes` · `fauna` · `fruit-machine` · `game-loop` · `game-persistence` · `game-server` · `game-state-schemas` · `heightmap-tiles` · `hitbox` · `ik-solver` · `leaderboard` · `lobby` · `mixamo` · `npc` · `osm-buildings` · `party` · `progression` · `quest` · `react-controller` · `react-crafting` · `react-dialogue` · `react-game-hud` · `react-game-map` · `react-game-settings` · `react-social-game` · `vehicle-physics` · `weapons` · `weather`
|
||||
|
||||
## geo
|
||||
|
||||
Geospatial geometry, planetary models, Earth utilities, solar system, stars, and tides.
|
||||
|
||||
**Existing:** `geo` · `geo-earth` · `geo-planet` · `solar-system` · `stars` · `tides` · `universe`
|
||||
|
||||
## gtap
|
||||
|
||||
Global Trade Automation Platform integration. Import/export licenses, tariff classification, sanctioned-party screening, certificates of origin, PTA eligibility, and AEO status.
|
||||
|
||||
**Pending:** `api-plugin-gtap`
|
||||
|
||||
## i18n
|
||||
|
||||
Internationalization core, backends, detectors, formatters, locale data, and shared helpers.
|
||||
|
||||
**Existing:** `i18n` · `i18n-backend-chained` · `i18n-backend-fs-multifile` · `i18n-backend-http` · `i18n-backend-memory` · `i18n-backend-storage` · `i18n-detector` · `i18n-formatter-collator` · `i18n-formatter-currency` · `i18n-formatter-date` · `i18n-formatter-display-name` · `i18n-formatter-list` · `i18n-formatter-number` · `i18n-formatter-relative-time` · `i18n-formatter-segmenter` · `i18n-icu-parser` · `i18n-shared` · `locale-data` · `react-i18n`
|
||||
|
||||
## image
|
||||
|
||||
Image processing, compression, EXIF, metadata, quantisation, and sprite tools.
|
||||
|
||||
**Existing:** `compress-image` · `exif`
|
||||
|
||||
**Pending:** `caddy-imgproxy` · `emblem-solver` · `image` · `image-quantisation` · `react-image`
|
||||
|
||||
## logistics
|
||||
|
||||
Supply chain operations: shipments, carriers, ports, customs, delivery tracking, warehousing, and procurement.
|
||||
|
||||
**Pending:** `api-plugin-customs` · `api-plugin-logistics` · `api-plugin-port-tracking` · `api-plugin-procurement` · `api-plugin-warehouse-ops` · `customs` · `delivery-tracking` · `logistics` · `port-tracking` · `ports` · `procurement` · `react-port-tracking` · `warehouse-ops`
|
||||
|
||||
## messaging
|
||||
|
||||
SMS, chat bots, push notifications, and social messaging.
|
||||
|
||||
**Existing:** `elevenlabs`
|
||||
|
||||
**Pending:** `api-plugin-messaging` · `api-plugin-notifications` · `discord-client` · `imessage-adapter` · `react-notifications` · `slack-client` · `sms` · `sms-providers` · `telegram-client`
|
||||
|
||||
## monitor
|
||||
|
||||
Observability, audit logging, and structured logging.
|
||||
|
||||
**Existing:** `audit-logging` · `logger` · `opentelemetry`
|
||||
|
||||
## network
|
||||
|
||||
Mesh networking, transports, discovery strategies, peer registries, protocol plugins, gateways, and service discovery.
|
||||
|
||||
**Existing:** `network` · `network-bootstrap` · `network-discovery-crdt` · `network-discovery-gundb` · `network-discovery-hybrid` · `network-discovery-peerjs` · `network-discovery-trystero` · `network-discovery-webtorrent` · `network-gundb-server` · `network-libp2p-server` · `network-plugin-assignment` · `network-plugin-audit-mesh` · `network-plugin-auth` · `network-plugin-circuit-breaker` · `network-plugin-content-delivery` · `network-plugin-crypto-messages` · `network-plugin-devtools` · `network-plugin-failover` · `network-plugin-governance` · `network-plugin-intra-relay` · `network-plugin-middleware` · `network-plugin-migration` · `network-plugin-monitor` · `network-plugin-partition` · `network-plugin-protocol` · `network-plugin-qos` · `network-plugin-registry` · `network-plugin-relay` · `network-plugin-server-bridge` · `network-plugin-session` · `network-plugin-simulation` · `network-plugin-state` · `network-plugin-topology` · `network-plugin-transport-manager` · `network-testing` · `network-transport-hyperswarm` · `network-transport-libp2p` · `network-transport-standby` · `network-transport-webrtc` · `network-transport-websockets` · `network-trystero-server` · `react-network-libp2p-demo` · `service-discovery` · `websocket-gateway`
|
||||
|
||||
**Pending:** `network-observability` · `network-quality`
|
||||
|
||||
## platform
|
||||
|
||||
Backend platform services: caching, search, ETL, webhooks, audit, logging, observability, feature flags, rules, workflows, health checks, PWA, and operational security.
|
||||
|
||||
**Existing:** `etl` · `feature-flags` · `search` · `webhook-client`
|
||||
|
||||
**Pending:** `analytics-browser` · `audience-rules` · `campaigns` · `health-check` · `notification-channel` · `pwa`
|
||||
|
||||
## r3f
|
||||
|
||||
React Three Fiber rendering, physics, animation, audio, UI-in-3D, XR, and game-system visualisation layers.
|
||||
|
||||
**Existing:** `r3f-networking`
|
||||
|
||||
**Pending:** `r3f-animation` · `r3f-booking` · `r3f-building` · `r3f-buildings` · `r3f-camera` · `r3f-character` · `r3f-character-vehicle` · `r3f-character-vehicle-picker` · `r3f-combat` · `r3f-controller` · `r3f-earth` · `r3f-emulator` · `r3f-environment` · `r3f-lod` · `r3f-minimap` · `r3f-npc` · `r3f-number-plate` · `r3f-parallax` · `r3f-particles` · `r3f-pathfinding` · `r3f-performance` · `r3f-physics-cascade` · `r3f-physics-core` · `r3f-physics-rapier` · `r3f-shaders` · `r3f-terrain` · `r3f-ui` · `r3f-vegetation` · `r3f-vehicle` · `r3f-water` · `r3f-weather` · `r3f-world` · `r3f-xr`
|
||||
|
||||
## random
|
||||
|
||||
Randomness, noise functions, finite-state machines, and seeded PRNGs.
|
||||
|
||||
**Existing:** `fsm` · `noise` · `pseudo-random` · `random`
|
||||
|
||||
## react
|
||||
|
||||
General-purpose React components, hooks, and UI primitives that do not map to a more specific domain category.
|
||||
|
||||
**Existing:** `react-ui`
|
||||
|
||||
**Pending:** `react-campaigns` · `react-charts` · `react-command-palette` · `react-consent-overlay` · `react-data-table` · `react-dnd` · `react-error-boundary` · `react-hotkeys` · `react-infinite-scroll` · `react-iphone` · `react-kanban` · `react-loading` · `react-modal` · `react-skeleton` · `react-social` · `react-timeline` · `react-tour` · `react-tree-view` · `react-ui-social` · `react-virtual`
|
||||
|
||||
## security
|
||||
|
||||
Security tooling, password management, breach checking, signing formats, certificates, and vaults.
|
||||
|
||||
**Existing:** `hibp` · `jws` · `otp` · `password-formats` · `password-generator` · `password-strength` · `security-codes` · `slsa` · `srp` · `tpm-struct` · `vault` · `vault-sync` · `x509`
|
||||
|
||||
## storage
|
||||
|
||||
Key-value and file-system storage abstractions.
|
||||
|
||||
**Existing:** `storage` · `storage-browser-indexeddb` · `storage-browser-localstorage` · `storage-browser-opfs` · `storage-file` · `storage-s3`
|
||||
|
||||
**Pending:** `cache-browser` · `cache-memory` · `cache-redis`
|
||||
|
||||
## store
|
||||
|
||||
E-commerce, inventory, subscriptions, and virtual currency.
|
||||
|
||||
**Pending:** `api-plugin-inventory` · `api-plugin-store` · `api-plugin-subscription` · `api-plugin-virtual-currency` · `r3f-inventory` · `react-store` · `store` · `subscription` · `virtual-currency`
|
||||
|
||||
## testing
|
||||
|
||||
Test runners, DOM utilities, E2E helpers, integration fixtures, and Storybook test utils.
|
||||
|
||||
**Existing:** `ab-testing` · `testing` · `testing-dom` · `testing-e2e` · `testing-integration` · `testing-integration-garage-s3` · `testing-integration-mailpit` · `testing-integration-postgres` · `testing-integration-redis` · `testing-integration-step-ca` · `testing-integration-surrealdb` · `testing-integration-typesense`
|
||||
|
||||
## timing
|
||||
|
||||
Time utilities, cron expressions, and scheduling primitives.
|
||||
|
||||
**Existing:** `cron` · `datetime` · `timing`
|
||||
|
||||
## tooling
|
||||
|
||||
CLI frameworks, developer tools, installers, and agent skills.
|
||||
|
||||
**Existing:** `cli` · `install-agent-skills` · `installer` · `readline`
|
||||
|
||||
**Pending:** `electrobun` · `license` · `linter` · `overview` · `profiler` · `sandbox` · `server-keys` · `test-vite` · `wip`
|
||||
|
||||
## vatdash
|
||||
|
||||
VAT return preparation, transaction tagging, rate lookups, and digital tax reporting.
|
||||
|
||||
**Pending:** `api-plugin-vatdash-bridge`
|
||||
|
||||
## video
|
||||
|
||||
Video metadata, thumbnails, and format handling.
|
||||
|
||||
**Pending:** `react-video` · `video` · `video-thumbnails`
|
||||
463
GENERIC-CLAUDE.md
Normal file
463
GENERIC-CLAUDE.md
Normal file
@@ -0,0 +1,463 @@
|
||||
# Project Engineering Standards (Generic CLAUDE.md)
|
||||
|
||||
A reusable engineering rulebook for AI coding agents and human contributors. Drop into any TypeScript monorepo; replace bracketed `<placeholders>` with project-specific values. Paired with **GENERIC-OPENSPEC-AGENTS.md** for the change-management workflow.
|
||||
|
||||
---
|
||||
|
||||
## 1. Stack baseline (adjust to your project)
|
||||
|
||||
- **Monorepo orchestrator**: Turborepo (or Nx / Lerna / Moon).
|
||||
- **Package manager / runtime**: Bun (or pnpm / npm / yarn).
|
||||
- **Lint / format**: Biome (or ESLint + Prettier).
|
||||
- **Unit tests**: Vitest (or Bun test / Jest).
|
||||
- **E2E**: Playwright.
|
||||
- **Language**: TypeScript, strict.
|
||||
- **Registry**: private (Verdaccio / GitHub Packages) or public npm.
|
||||
|
||||
Pin every tool version in the root manifest; never accept "latest" implicitly.
|
||||
|
||||
---
|
||||
|
||||
## 2. Change workflow
|
||||
|
||||
All non-trivial changes go through the **OpenSpec** process (see `GENERIC-OPENSPEC-AGENTS.md`). Before coding:
|
||||
|
||||
1. Read the project's spec/agents docs.
|
||||
2. Create `openspec/changes/<change-id>/` with `proposal.md`, `tasks.md`, and spec deltas under `specs/<capability>/spec.md` using `## ADDED` / `## MODIFIED` / `## REMOVED Requirements` headings.
|
||||
3. On ship: fold the delta into `openspec/specs/<capability>/spec.md` and move the folder to `openspec/archive/<yyyy-mm-dd>-<change-id>/`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Critical rules (never compromise)
|
||||
|
||||
1. **Never run `tsc` directly.** Use your bundler (Vite/tsup/esbuild) via your task runner. Native TS runtimes (Bun, ts-node) execute `.ts` files without compilation.
|
||||
2. **Never use mocks** for things you can run for real. Prefer real services (testcontainers, in-memory adapters, local servers) over `vi.mock`/`jest.mock`.
|
||||
3. **Pin exact versions** in dependency overrides. No drift.
|
||||
4. **Never re-export external deps** for convenience — consumers import from the upstream source.
|
||||
5. **Never auto-commit/push** without user approval. Never use `--no-verify`.
|
||||
6. **Always use absolute paths** in tool calls.
|
||||
7. **Read files before editing them** — understand context first.
|
||||
8. **Testing-infrastructure packages must not depend on the project's runtime packages.** Only external deps (and a zero-runtime types package, if you have one).
|
||||
9. **Never write static-analysis suppression comments** (`// biome-ignore`, `// eslint-disable*`, `// ts-ignore`, `// ts-expect-error`) in new or migrated code. Stale rule names break the lint gate; comments that no longer apply produce "has no effect" warnings. To exercise a deliberate violation, use a pattern the linter cannot statically reject (see §10b).
|
||||
10. **Never write `export *` in any non-root barrel.** Feature-module barrels MUST use explicit named re-exports. Exempt: generated forwarder files, single-line variant forwarders, and namespace re-exports (`export * as ns from "..."`). Migrate `export *` → named exports inside the same commit, never as a follow-up.
|
||||
11. **Never import test-runner primitives directly from your runner.** If you support both Bun and Vitest (or want to keep the option open), route all test imports through a single neutral wrapper module that re-exports from the active runner via conditional exports. Allowlist only the wrapper packages.
|
||||
|
||||
---
|
||||
|
||||
## 4. Git safety (non-negotiable)
|
||||
|
||||
Before ANY git operation: run `git status` in **every** worktree (`git worktree list`) and confirm each is clean or its state is understood.
|
||||
|
||||
**Never run these without explicit per-use user approval:**
|
||||
|
||||
- `git filter-repo` / `git filter-branch` — rewrites history, destroys staged + working tree.
|
||||
- `git push --force` / `--force-with-lease` — only after the user explicitly says "force push".
|
||||
- `git reset --hard`, `git clean -fd`, `git rebase`, `git stash drop`/`clear`, `git gc`/`prune`, `git checkout -- <file>`.
|
||||
|
||||
**Large-file push errors:** stop. Do NOT rewrite history. Run `git status` / `git stash list` in all worktrees, then ask the user (`.gitignore` + new commit, Git LFS, or manual removal).
|
||||
|
||||
Staged changes are irreplaceable — treat them as more precious than committed history.
|
||||
|
||||
---
|
||||
|
||||
## 5. Workflow rules
|
||||
|
||||
1. **Create a task list for multi-step work** (>3 steps).
|
||||
2. **Complete all implementations.** No stubs, no placeholders, no TODOs, no mocks in production. Missing dep? Add it and wire the real implementation.
|
||||
3. **Build feature-rich, production-grade packages.** Prefer comprehensive over MVP. Every package ships at full functional completeness on first landing — never defer a feature its name reasonably implies to a follow-up proposal.
|
||||
4. **Professional honesty.** No marketing language. No fake metrics.
|
||||
5. **Preserve the full public surface** during rewrites. Trim only when duplicated in-repo or replaceable by an existing in-repo library.
|
||||
|
||||
---
|
||||
|
||||
## 6. Package layout
|
||||
|
||||
### 6.1 Directory structure
|
||||
|
||||
Domain-first organization, with **per-feature folders** containing five co-located files:
|
||||
|
||||
```text
|
||||
src/<category>/
|
||||
<feature>/
|
||||
index.ts # Barrel — explicit named re-exports
|
||||
<feature>.ts # Implementation
|
||||
<feature>.test.ts # Unit tests
|
||||
<feature>.types.ts # Type definitions (optional)
|
||||
<feature>.md # Co-located documentation (mandatory for impl files)
|
||||
```
|
||||
|
||||
Special directories:
|
||||
|
||||
- `types/` — shared types, flat
|
||||
- `utils/` — flat with tests + docs
|
||||
- `constants/index.ts` — simple exports (no tests/docs needed)
|
||||
|
||||
### 6.2 Naming rules
|
||||
|
||||
- **No package-name prefix inside package folders.** Inside `packages/<pkg>/`, child folders/files use the feature name only — never `<pkg>-<feature>`. The package name is already in the path.
|
||||
- **No redundant ancestor names.** A child name must not contain the singular or plural form of any ancestor folder. If parent is `adapters/`, children must not be `memory-adapter/`; use `memory/`. Apply to: `strategies`, `providers`, `builders`, `plugins`, `handlers`, `resolvers`, `factories`, `parsers`, `services`, etc.
|
||||
- **One `README.md` per package root only.** No category-level READMEs inside `src/<category>/`.
|
||||
|
||||
### 6.3 Subpath exports
|
||||
|
||||
Every feature directory must have its own entry in `package.json` `exports` so consumers can tree-shake:
|
||||
|
||||
```json
|
||||
"./retry/retry": {
|
||||
"import": { "types": "./dist/retry/retry.d.ts", "default": "./dist/retry/retry.js" }
|
||||
}
|
||||
```
|
||||
|
||||
A single `"."` barrel is insufficient. The count of subpath exports should match the count of feature modules.
|
||||
|
||||
### 6.4 Barrel rules (explicit named exports only)
|
||||
|
||||
```ts
|
||||
// CORRECT — feature barrel: explicit named exports
|
||||
export {
|
||||
decryptStream,
|
||||
type DecryptStreamOptions,
|
||||
encryptStream,
|
||||
type EncryptStreamOptions,
|
||||
} from "./streaming.js";
|
||||
|
||||
// WRONG — leaks every new export silently
|
||||
export * from "./streaming.js";
|
||||
|
||||
// CORRECT — top-level namespace re-export (recommended for primitives with shared names)
|
||||
export * as ed25519 from "./primitives/ed25519/ed25519.js";
|
||||
```
|
||||
|
||||
Why: the barrel **is** the contract; `export *` makes the contract whatever the source happens to declare today. Tree-shaking is equivalent under `"sideEffects": false`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Dependency conventions
|
||||
|
||||
- **External deps** use catalog refs (or workspace-level pinning) — never bare versions like `"^1.2.3"`.
|
||||
- **Internal deps** use `workspace:^`.
|
||||
- `"sideEffects": false` on all pure library packages.
|
||||
- After changing `overrides`, nuke and reinstall:
|
||||
|
||||
```bash
|
||||
rm -rf node_modules <lockfile> && find . -type d -name "node_modules" -exec rm -rf {} + && <install>
|
||||
```
|
||||
|
||||
- For browser/React packages: verify only one React version exists after install:
|
||||
|
||||
```bash
|
||||
find . -path "*/node_modules/react/package.json" | head -5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Banned patterns in `src/`
|
||||
|
||||
Before closing any task, scan for zero matches:
|
||||
|
||||
```bash
|
||||
grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\.skip\|\.only\|\bany\b\|console\.log" \
|
||||
packages/<pkg>/src/ --include="*.ts"
|
||||
```
|
||||
|
||||
- `@deprecated` JSDoc → replace with prose: "Backwards-compatibility alias for X."
|
||||
- `any` → use `unknown` (narrow via `instanceof` / type guards) or a proper generic. Never substitute `object`, `{}`, or a redundant union.
|
||||
- `console.log` → use a real logger (injectable `LoggerLike` interface preferred).
|
||||
|
||||
### 8.1 No stubs, placeholders, or "WIP" code
|
||||
|
||||
Concretely banned in any commit:
|
||||
|
||||
- **Stub functions** — `throw new Error("not implemented")`, identity pass-throughs that pretend to do work, no-op factories.
|
||||
- **Placeholder types** — `type Foo = unknown`, empty `interface Foo {}`, `as never` hiding incomplete data flow.
|
||||
- **TODO / FIXME / XXX comments** in source AND docs.
|
||||
- **Deprecation aliases without a working replacement.**
|
||||
- **"Phase 1 / Phase 2" splits** where Phase 2 is unwritten and Phase 1 ships a function that doesn't do the thing its name claims.
|
||||
- **Marketing language masking incomplete work** ("comprehensive", "fully-featured" applied to code with unimplemented branches).
|
||||
- **Tautological tests** — `it.todo`, `it.skip`, `expect(true).toBe(true)`, `expect(result).toBeDefined()` standing alone, tests that assert against the exact hardcoded value the implementation returns. Every `it(...)` MUST verify a behavior the implementation could plausibly get wrong.
|
||||
- **Stub docs** — `.md` files that say "TODO", empty Features/API sections, or fail word-count gates.
|
||||
- **Empty test files** kept only to keep coverage thresholds passing.
|
||||
- **Silently dropped spec requirements** — see §2c of the OpenSpec doc. If a requirement is in the spec delta, it ships.
|
||||
|
||||
If the work isn't ready: don't merge it. The default branch is the product, not a scratchpad.
|
||||
|
||||
### 8.2 Never delete spec elements to satisfy a gate
|
||||
|
||||
When a gate fails because a registered element (error code, schema field, event topic) is **declared but unused**, the failure is telling you the implementation hasn't caught up to the proposal. **Wire it in**, do NOT delete it. Deletion is the path of least resistance and silently strips features the proposal committed to.
|
||||
|
||||
If the element genuinely crept in by mistake, remove it with a one-line note under `### Deferred` or `### Removed` in `proposal.md` so the deletion is auditable.
|
||||
|
||||
### 8.3 Never delete code that serves a purpose to make tests pass
|
||||
|
||||
When a coverage report flags a function, branch, or guard as uncovered, write a test that exercises real behavior — do NOT delete the code that's hard to reach.
|
||||
|
||||
Concretely banned:
|
||||
|
||||
- Deleting defensive guards because current call sites don't trigger them.
|
||||
- Deleting rollback/cleanup helpers because the synchronous code path can't reach them.
|
||||
- Replacing `instanceof Error ? cause : undefined` with `cause as Error` to "simplify."
|
||||
- Deleting branches inside `try/catch` because the library doesn't currently throw.
|
||||
|
||||
Instead, in priority order:
|
||||
|
||||
1. **Write a test that exercises the path** — inject a controlled failure (custom storage that throws, custom Clock that returns a chosen instant, AbortSignal scheduled via `queueMicrotask`).
|
||||
2. **Restructure to make the path reachable** without changing semantics (e.g. `await Promise.resolve()` between iterations so a caller-side abort can land).
|
||||
3. **If genuinely unreachable**, annotate with `/* c8 ignore next */` and a comment naming the unreachability proof.
|
||||
|
||||
### 8.4 Suppression workaround pattern
|
||||
|
||||
When a deliberate violation is required (e.g. testing a defensive non-Error branch), use a pattern the linter can't statically reject:
|
||||
|
||||
```ts
|
||||
// BANNED — suppression rots when rule renames.
|
||||
// biome-ignore lint/suspicious/noThrowLiterals: ...
|
||||
throw "literal";
|
||||
|
||||
// REQUIRED — linter can't prove the value isn't Error-like.
|
||||
const failure: unknown = "literal";
|
||||
throw failure;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. File-size budgets
|
||||
|
||||
- **Source files**: 300 lines hard / 250 soft.
|
||||
- **Test files**: 500 lines max.
|
||||
- **Functions**: 50 lines soft — extract helpers at natural responsibility boundaries.
|
||||
|
||||
```bash
|
||||
find packages/<pkg>/src -name "*.ts" ! -name "*.test.ts" | \
|
||||
xargs awk 'END { if (NR > 300) print FILENAME, NR " lines" }'
|
||||
```
|
||||
|
||||
Split at natural boundaries. Preserve all public exports.
|
||||
|
||||
---
|
||||
|
||||
## 10. Quality gates
|
||||
|
||||
### 10.1 Coverage
|
||||
|
||||
**100 / 100 / 100 / 100** for every package, no exceptions:
|
||||
|
||||
```ts
|
||||
thresholds: { statements: 100, branches: 100, functions: 100, lines: 100 }
|
||||
```
|
||||
|
||||
The pre-publish gate MUST hard-fail below 100%. Mutation / property / fuzz testing layer on top — they do not substitute.
|
||||
|
||||
### 10.2 Tests
|
||||
|
||||
- Use **Arrange-Act-Assert**.
|
||||
- Name tests after the **behavior**, not the implementation:
|
||||
|
||||
```ts
|
||||
// CORRECT
|
||||
it('returns empty array when no results match', ...)
|
||||
it('throws ValidationError when required field is missing', ...)
|
||||
|
||||
// WRONG
|
||||
it('test1', ...)
|
||||
it('works correctly', ...)
|
||||
it('handleSearch', ...)
|
||||
```
|
||||
|
||||
- File suffixes: `.test.ts` for unit, `.spec.ts` for E2E.
|
||||
- Always pass a timeout: `it(name, { timeout: 5000 }, fn)` (30000 for async-heavy).
|
||||
- Root-cause every failure — never `.skip` or `.only` to ship.
|
||||
- Every UI component needs `.stories.tsx` (default + playground + variants).
|
||||
|
||||
### 10.3 Pre-publish gate
|
||||
|
||||
```bash
|
||||
<task-runner> lint typecheck test --filter=<pkg>
|
||||
<task-runner> test:coverage --filter=<pkg>
|
||||
<task-runner> build --filter=<pkg>
|
||||
```
|
||||
|
||||
All must be green. Single-file test runs are not a substitute for the full-package gate.
|
||||
|
||||
---
|
||||
|
||||
## 11. Documentation
|
||||
|
||||
### 11.1 Sibling `.md` per implementation file
|
||||
|
||||
Every `.ts`/`.tsx` MUST have a sibling `.md`. **Exclusions**: `.test.ts`, `.test.tsx`, `.spec.ts`, `.types.ts` (inside a feature dir), `.stories.tsx`, barrel `index.ts`, `constants/index.ts`.
|
||||
|
||||
**Required sections** (≥200 words excl. code):
|
||||
|
||||
1. **Purpose** (≥2 sentences — what and why)
|
||||
2. **Features** (bullet list)
|
||||
3. **Usage: Basic Example**
|
||||
4. **Usage: Advanced Example**
|
||||
5. **API Reference** (every exported symbol — params, return type, throws)
|
||||
6. **Implementation Notes** (design decisions, limits, edge cases)
|
||||
|
||||
Hard constraints: both simple AND advanced examples mandatory; code must compile with real imports; no marketing/placeholder text; update docs when code changes.
|
||||
|
||||
### 11.2 Three-pass writing workflow
|
||||
|
||||
Never one-shot:
|
||||
|
||||
1. **Read** — full `.ts` file; note every exported symbol, param, thrown error.
|
||||
2. **Structure** — Purpose + Features + API Reference first.
|
||||
3. **Examples** — Basic (simplest happy path), then Advanced (real composition / edge case).
|
||||
|
||||
### 11.3 Doc-drift gate
|
||||
|
||||
When you modify any `<feature>.ts`, you MUST update its sibling `<feature>.md` in the same change if the modification affects:
|
||||
|
||||
- public API surface (added/removed/renamed export, changed types, changed throws)
|
||||
- documented behavior (error codes, side effects, semantics)
|
||||
- examples in the `.md` (must still compile)
|
||||
|
||||
Editing `.ts` without touching `.md` is treated as a defect, identical in severity to a failing test.
|
||||
|
||||
```bash
|
||||
# For every changed .ts, confirm the sibling .md is also touched
|
||||
git diff --name-only HEAD | awk '
|
||||
/\.ts$/ && !/\.test\.ts$/ && !/\.spec\.ts$/ && !/\.types\.ts$/ && !/\/index\.ts$/ {
|
||||
md = $0; sub(/\.ts$/, ".md", md)
|
||||
cmd = "git diff --name-only HEAD -- " md
|
||||
cmd | getline touched; close(cmd)
|
||||
if (touched == "") print "DOC DRIFT: " md " not updated alongside " $0
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
### 11.4 Root `README.md`
|
||||
|
||||
Every package needs `README.md` at the root with:
|
||||
|
||||
1. Package name (H1)
|
||||
2. One-paragraph description
|
||||
3. Install command
|
||||
4. Quick-start code example
|
||||
5. **Modules** — links to every co-located `.md` in `src/` via relative paths
|
||||
|
||||
---
|
||||
|
||||
## 12. Type design
|
||||
|
||||
### 12.1 No nested object literals in interfaces
|
||||
|
||||
Every nested object shape must be a named `interface` or `type`.
|
||||
|
||||
```ts
|
||||
// WRONG
|
||||
interface CreateUserOptions {
|
||||
profile: { displayName: string; avatarUrl: string }
|
||||
notifications: { email: boolean; push: boolean }
|
||||
}
|
||||
|
||||
// CORRECT
|
||||
interface UserProfile { displayName: string; avatarUrl: string }
|
||||
interface NotificationPreferences { email: boolean; push: boolean }
|
||||
interface CreateUserOptions { profile: UserProfile; notifications: NotificationPreferences }
|
||||
```
|
||||
|
||||
### 12.2 No inline shapes in function signatures
|
||||
|
||||
```ts
|
||||
// WRONG
|
||||
function createSession(user: { id: string }, opts: { ttl: number }): { token: string } { ... }
|
||||
|
||||
// CORRECT
|
||||
function createSession(user: SessionUser, opts: SessionOptions): SessionResult { ... }
|
||||
```
|
||||
|
||||
### 12.3 Shared types belong in a zero-dep types package
|
||||
|
||||
If a type is referenced by ≥2 packages, define it in a shared `types`/`contracts` package with zero runtime code and zero workspace deps. This prevents circular dependencies and makes the public contract single-sourced.
|
||||
|
||||
---
|
||||
|
||||
## 13. Error handling
|
||||
|
||||
Use a centralized error system (`defineCodes("<NAMESPACE>", ["REASON_A", "REASON_B"] as const)` style) rather than hand-written string literals. Rules:
|
||||
|
||||
- **In code**: throw `new XxxError(CODES.REASON, …)` using the exported constant. Never type the raw `"NAMESPACE.REASON"` string by hand.
|
||||
- **In tests**: assert against the constant (`expect(err.code).toBe(CODES.REASON)`), never against a hand-typed literal — those rot silently.
|
||||
- **In specs**: when a spec references a code, use the exact namespaced form. Spec values must match the code-generated value verbatim.
|
||||
- **On mismatch**: fix the spec to match the convention-correct value — do not bend the code's namespace to match a mistaken literal.
|
||||
|
||||
Every registered error code MUST be reachable from a real call site exercised by a test. A registered-but-never-used code is a defect.
|
||||
|
||||
---
|
||||
|
||||
## 14. Web app / page verification
|
||||
|
||||
When generating client-side pages, verification is NOT optional and a 200 response is NOT proof of correctness:
|
||||
|
||||
1. **Staging environment** — mirror production before declaring done (Docker / preview deploy).
|
||||
2. **Full Playwright E2E sweep** — every page, every component, every navigation.
|
||||
3. **Never trust HTTP 200 alone** — verify rendered DOM contains expected content.
|
||||
4. **Inspect browser console** — any unexpected error or warning fails the check.
|
||||
5. **Inspect network activity** — failed requests, missing assets, CORS, mixed content.
|
||||
6. **Zero-defect to live** — fix root cause first.
|
||||
|
||||
---
|
||||
|
||||
## 15. Vitest / WSL2 specifics (optional, if applicable)
|
||||
|
||||
- Use a shared `createTestConfig()` helper. For configs needing aliases/plugins, write standalone with `pool: 'forks'` + a polyfills setup file.
|
||||
- **Never `mergeConfig` with a base config** that may omit polyfills — causes `crypto is not defined` and similar at runtime.
|
||||
- **`vmThreads` and `threads` pools fail in Bun on WSL2** — use default `forks`.
|
||||
- **`fileParallelism: false`** required for browser tests.
|
||||
|
||||
---
|
||||
|
||||
## 16. Code-tier organization
|
||||
|
||||
Encode the runtime tier in `package.json` (`"browser"` field, peer deps, `engines`), not in the folder path.
|
||||
|
||||
- **Universal**: ZERO browser/React/Node-only deps.
|
||||
- **Browser**: React/DOM ok.
|
||||
- **Server**: Node.js ok.
|
||||
|
||||
Co-locate tests with source; sibling `.md` docs per implementation file.
|
||||
|
||||
---
|
||||
|
||||
## 17. Package lifecycle
|
||||
|
||||
- **New packages**: add to manifest, regenerate dep graph index.
|
||||
- **Editing**: every package starts at `0.1.0`; bump only at `1.0.0` graduation.
|
||||
- **Pre-publish gate (non-negotiable)**: lint + typecheck + test + 100% coverage on touched modules + build all green.
|
||||
- **Foundation packages**: when changing a widely-consumed package, run the **dependent sweep** (`<task-runner> test --filter=...<pkg>`) before declaring done. Green local tests do not prove you didn't break consumers.
|
||||
|
||||
---
|
||||
|
||||
## 18. Quick reference commands
|
||||
|
||||
```bash
|
||||
# Test one package
|
||||
<task-runner> test --filter=<pkg>
|
||||
|
||||
# Coverage for one package
|
||||
<task-runner> test:coverage --filter=<pkg>
|
||||
|
||||
# Lint + typecheck + tests
|
||||
<task-runner> lint typecheck test --filter=<pkg>
|
||||
|
||||
# Pre-publish gate
|
||||
<task-runner> lint typecheck test build --filter=<pkg> && <task-runner> test:coverage --filter=<pkg>
|
||||
|
||||
# Dependent sweep (foundation packages)
|
||||
<task-runner> test --filter=...<pkg>
|
||||
|
||||
# Changed since main
|
||||
<task-runner> test --filter=...[main]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 19. Behavior-change propagation
|
||||
|
||||
When a change introduces consumer-visible behavior changes (new error code, renamed metric, method that now throws, default that flipped, return shape change), do all three:
|
||||
|
||||
1. **Self-doc**: update sibling `.md` + `README.md` Features section.
|
||||
2. **Dependent surface scan**: `grep -rln "<old-name>" packages/ | grep -v "/dist/\|/<own-pkg>/"`. Every hit needs updating in the same change set.
|
||||
3. **Migration note**: add `## Migration` to `proposal.md` listing old behavior, new behavior, search command for consumers, recommended replacement.
|
||||
533
GENERIC-OPENSPEC-AGENTS.md
Normal file
533
GENERIC-OPENSPEC-AGENTS.md
Normal file
@@ -0,0 +1,533 @@
|
||||
# OpenSpec — Generic Agent Compliance Checklist
|
||||
|
||||
A reusable spec-driven change-management workflow for any project. Read this file before implementing **any** OpenSpec change. Every section is a non-negotiable gate. Paired with **GENERIC-CLAUDE.md** for general engineering standards.
|
||||
|
||||
---
|
||||
|
||||
## 0. Research before implementing
|
||||
|
||||
Before writing a new package or migrating a library — mandatory, not optional:
|
||||
|
||||
1. **Search the registry** (npm / PyPI / crates.io / etc.) for existing implementations. Prefer battle-tested over hand-rolled. Check the root manifest's `catalog` / `dependencies` block for deps already pinned.
|
||||
2. **Check vendor docs** (via Context7 or similar) to confirm API behavior and version-specific details before wrapping an external library.
|
||||
3. **Search your own repo** for reference implementations and patterns. Try local copies of upstream sources first.
|
||||
4. If an existing implementation covers 80%+ of the requirement, **port or wrap it** — do not start from scratch.
|
||||
5. If a starting-from-scratch is unavoidable but a prior implementation exists, **read its nested `.md` files first** to understand its purpose.
|
||||
6. **Check the dep graph** — if a new type or abstraction is shared across ≥2 packages, it belongs in a zero-dep shared types package. Add it there first, then reference it. Prevents circular dependencies and keeps the public contract in one place.
|
||||
|
||||
---
|
||||
|
||||
## 0a. Migration parity — no cut-down replacements
|
||||
|
||||
When migrating a library from an upstream source, **a greenfield rewrite is a last resort, not a default.** The upstream library exists because it solved real problems. Dropping features because they are "complex" or "not needed yet" is how you end up with broken consumers later.
|
||||
|
||||
### Mandatory parity checklist
|
||||
|
||||
Every migration proposal MUST include a `## Parity Checklist` section. Before any code, read the upstream source, any upstream specs, and any upstream audit docs. Then produce a table:
|
||||
|
||||
```markdown
|
||||
| Capability | Upstream | Migrated | Notes |
|
||||
|---|---|---|---|
|
||||
| Feature A | ✅ | ✅ | |
|
||||
| Feature B | ✅ | 🚫 | Scoped out: reason … |
|
||||
| Feature C | ✅ | ⬆️ | Enhanced: added … |
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- **No implicit drops.** If upstream has it and the migration does not, the box is 🚫 and MUST have a justification.
|
||||
- **"Not needed yet" is banned as a justification.** Scope out only with a concrete architectural reason (e.g. "replaced by interceptor API which covers the same use case via composition").
|
||||
- **If the justification is longer than one sentence, the feature is not scoped out — it is required.**
|
||||
- **The checklist is a gate.** A proposal with unchecked 🚫 items without justification is rejected.
|
||||
|
||||
### Design quality bar
|
||||
|
||||
Migrations must be **modular and extensible**:
|
||||
|
||||
- Use TypeScript generics for type parameters — not `any` or `unknown` escape hatches.
|
||||
- Prefer composition over inheritance. Public surfaces are interfaces callers can implement.
|
||||
- Avoid closed unions where module augmentation or registry patterns are possible.
|
||||
- Preserve upstream subpath exports — every public module must be tree-shakeable.
|
||||
- Merge upstream and downstream improvements. The migrated package gains every enhancement from both sides.
|
||||
|
||||
### Anti-patterns that block approval
|
||||
|
||||
| Anti-pattern | Why it fails |
|
||||
|---|---|
|
||||
| "Fresh build" with a different API | Consumers expecting upstream contracts break silently. |
|
||||
| `export *` in any non-root barrel | Banned — the barrel IS the contract; convert to explicit named exports inside the migration commit, not a follow-up. |
|
||||
| Direct runner imports in non-exempt test files | Tests must route through the runner-neutral wrapper; rewrite during the migration commit. |
|
||||
| Dropping schema-driven features for "simplicity" | Callers abandon the package and hand-roll the missing logic. |
|
||||
| Replacing upstream errors with plain `Error` | Loses programmatic handling, telemetry, and i18n keys. |
|
||||
| Ignoring upstream enterprise audit | The audit flagged real gaps; ignoring them recreates them. |
|
||||
|
||||
### Reference docs you must read
|
||||
|
||||
Before migrating any package:
|
||||
|
||||
1. The canonical upstream capability spec.
|
||||
2. Any upstream change proposals (active and archived) that modified the package.
|
||||
3. Any upstream enterprise audit with coverage / error / quality gaps.
|
||||
4. Any design docs referenced by those proposals.
|
||||
|
||||
If any are missing, note it in the parity checklist and explain the risk.
|
||||
|
||||
---
|
||||
|
||||
## 0b. Ecosystem awareness — read your status dashboard
|
||||
|
||||
Before writing a new package or migrating a library, **read the root status file** (e.g. `STATUS.md`) to learn which packages already exist, which are completed, and which are pending.
|
||||
|
||||
Rules:
|
||||
|
||||
- **If your package depends on something in `Pending`**, that dependency **must be migrated first** (or a compatible substitute must already exist). Do not hand-roll a replacement without explicit justification in the parity checklist.
|
||||
- **If the package itself is in `Pending`**, it **must have an OpenSpec change written first** (`proposal.md` + `tasks.md`) before any code.
|
||||
- **If the package does not exist anywhere**, it is an original. It **still** must have an OpenSpec change written first.
|
||||
- Never start coding without either (a) completed migration of all pending dependencies, or (b) an approved proposal that explicitly scopes out missing dependencies with architectural justification.
|
||||
|
||||
Checking the status file prevents duplicate work, broken downstream consumers, and incompatible re-implementations.
|
||||
|
||||
---
|
||||
|
||||
## 0c. Authoring the change — folder structure and spec-delta format
|
||||
|
||||
**Read this before you create a single file in `openspec/changes/`.** Every rule here is a gate.
|
||||
|
||||
### The change folder has exactly this shape
|
||||
|
||||
```text
|
||||
openspec/changes/<change-id>/
|
||||
proposal.md # what & why, impact, affected dependents
|
||||
tasks.md # ordered implementation checklist
|
||||
design.md # OPTIONAL — non-normative design: rationale, algorithms,
|
||||
# code sketches, file layout, diagrams, trade-offs
|
||||
specs/<capability>/spec.md # the SPEC DELTA — normative requirements only
|
||||
```
|
||||
|
||||
- `<capability>` is the **package short name** without any scope prefix. One capability folder per package. Mirrors the eventual canonical path `openspec/specs/<capability>/spec.md`.
|
||||
- `design.md` is optional but **strongly preferred** for any non-trivial package. It is the home for everything that is *not* a testable requirement: architecture, algorithm notes, file layout, integration narrative, benchmark plans, decisions & trade-offs. If you're writing prose that isn't a `### Requirement:`, it belongs in `design.md`, not the spec delta.
|
||||
|
||||
### `openspec/specs/` is OFF LIMITS until archival
|
||||
|
||||
`openspec/specs/<capability>/spec.md` is the **canonical, shipped** spec. It is written to **only** by the §14 archival fold step, **after** all gates pass. While a change is `PROPOSED` or in progress:
|
||||
|
||||
- **NEVER create or edit anything under `openspec/specs/`.** Not a stub, not a placeholder, not a "draft for reference". If `openspec/specs/<your-capability>/` exists before your change archives, delete it and move the content into your change's `specs/<capability>/spec.md` delta.
|
||||
- The file inside your **change** folder is a **delta**, not the canonical spec. It is folded into `openspec/specs/` on archive and the change folder is moved to `openspec/archive/`.
|
||||
|
||||
### The spec delta is a normative requirements document — not an architecture dump
|
||||
|
||||
The delta MUST use the OpenSpec requirement/scenario format. Skeleton:
|
||||
|
||||
```markdown
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: <capability>
|
||||
|
||||
**Package**: `<pkg>`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: <Imperative name>
|
||||
|
||||
The package SHALL … / MUST … — normative, testable prose. One requirement = one capability.
|
||||
|
||||
#### Scenario: <observable behaviour>
|
||||
|
||||
- **WHEN** <precondition / action>
|
||||
- **THEN** <observable, checkable outcome>
|
||||
|
||||
**Verified by:** `src/<feature>/<feature>.test.ts::"<planned test name>"` AND `<another planned test>`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: <next one>
|
||||
…
|
||||
```
|
||||
|
||||
Rules for the delta:
|
||||
|
||||
- Top-level headings are exactly `## ADDED Requirements`, `## MODIFIED Requirements`, and/or `## REMOVED Requirements` — nothing else. A fresh package uses only `## ADDED Requirements`.
|
||||
- Every `### Requirement:` has **normative prose** (SHALL / MUST / SHALL NOT), **one or more** `#### Scenario:` blocks as `- **WHEN**` / `- **THEN**` bullets, and **exactly one** `**Verified by:**` line. Separate requirements with `---`.
|
||||
- `**Verified by:**` is authored **at draft time**. The change is not implemented yet, so it names **planned** test files (paths from `tasks.md` / `design.md` file layout) in `path::"test name"` form. That is expected and correct — it is the contract the implementation must satisfy.
|
||||
- Cover the **whole contract**: public API surface, every error code (each must be reachable from a real call site), boundary validation, telemetry instrumentation, `AbortSignal` propagation, subpath exports, tier purity (universal → no browser/Node APIs; browser → no Node; server → no browser). Match the package's depth — a large package is 15–25 requirements, a small leaf is 8–12.
|
||||
|
||||
### `proposal.md` and `tasks.md` obligations
|
||||
|
||||
- `proposal.md` MUST link its design and delta in the header block (`- **Design**: ./design.md`, `- **Spec delta**: ./specs/<capability>/spec.md`) — **never** a `Reference spec:` pointer into `openspec/specs/`. It MUST carry the `## Impact` → `### Affected dependents` section per §14a.
|
||||
- `tasks.md` MUST end with a "Fold `specs/<capability>/spec.md` into `openspec/specs/<capability>/spec.md`" step immediately before the archive step (see §14).
|
||||
|
||||
### Multi-package / cross-cutting feature sets
|
||||
|
||||
When one feature spans many packages (a family, a wave of transports, etc.):
|
||||
|
||||
- **One change per package.** Each package gets its own `openspec/changes/<id>/` with its own `specs/<capability>/spec.md` delta. Do **not** write a single umbrella spec covering the whole family, and do **not** put a shared `specs/<family>/` folder anywhere.
|
||||
- **Shared architecture lives once, in the foundation package's `design.md`.** Sibling `design.md` files **reference** it by relative path rather than duplicating.
|
||||
- Each change still stands alone: its `proposal.md`, `design.md`, `tasks.md`, and delta are self-sufficient.
|
||||
|
||||
### Pre-flight checklist — before you write any change file
|
||||
|
||||
1. The package is in the project status dashboard, or is an original — confirmed per §0b.
|
||||
2. You have read the upstream references in §0a (for migrations) or the design inputs (for originals).
|
||||
3. You will create `proposal.md` + `tasks.md` + `specs/<capability>/spec.md` (+ `design.md`), and **nothing** under `openspec/specs/`.
|
||||
4. The delta will be in `## ADDED/MODIFIED/REMOVED Requirements` form with `### Requirement:` / `#### Scenario:` / `**Verified by:**` — not an API dump.
|
||||
5. `proposal.md` will carry `### Affected dependents` (§14a); `tasks.md` will carry the fold step (§14).
|
||||
6. **You have created a numbered checklist of every `### Requirement` and every `#### Scenario` in the spec delta.** This lives in your working notes. You may not begin implementation until every requirement is listed as a discrete, checkable item. As you implement, check each off. An unchecked item at the end means the package is not done. **Never silently skip a requirement because "tests already pass without it."** That is the single most common cause of spec drift.
|
||||
|
||||
---
|
||||
|
||||
## 1. Dependency conventions
|
||||
|
||||
- All external deps use catalog refs — **never bare version strings** like `"^1.2.3"`.
|
||||
- All internal deps use `workspace:^`.
|
||||
- If a catalog key does not exist, add it to the root manifest first.
|
||||
- Coverage provider must be installed as a `devDependency` via catalog ref. Without `@vitest/coverage-istanbul` (or equivalent), Istanbul-style coverage silently falls back to v8.
|
||||
- Testing-infra packages have **zero** internal workspace deps.
|
||||
|
||||
---
|
||||
|
||||
## 2. No banned patterns in `src/`
|
||||
|
||||
Before closing any task, scan for zero matches:
|
||||
|
||||
```bash
|
||||
grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\.skip\|\.only\|\bany\b\|console\.log" \
|
||||
packages/<pkg>/src/ --include="*.ts"
|
||||
```
|
||||
|
||||
- `@deprecated` → prose: "Backwards-compatibility alias for X."
|
||||
- `any` → `unknown` + narrowing, or a proper generic.
|
||||
- `console.log` → real logger / injectable `LoggerLike`.
|
||||
|
||||
## 2a. Lint suppressions
|
||||
|
||||
Stale `// biome-ignore` / `// eslint-disable*` comments break the lint gate hard.
|
||||
|
||||
Rules:
|
||||
|
||||
1. **Never write a suppression comment for transient lint noise.** If the linter flags something, it's a real signal — fix the code.
|
||||
2. **If a deliberate violation is required**, use a coding pattern the linter cannot statically reject (see §8.4 of GENERIC-CLAUDE.md).
|
||||
3. **If a suppression genuinely is the only path**, pin both the current rule category and a one-sentence reason. Verify the category resolves before committing.
|
||||
4. **Scan before closing any task:** `grep -rn "biome-ignore\|eslint-disable" packages/<pkg>/src/ --include="*.ts"`. Every hit MUST be justified in code review.
|
||||
|
||||
## 2b. NEVER ship stubs, placeholders, or "WIP" code
|
||||
|
||||
See **GENERIC-CLAUDE.md §8.1** for the full list. Banned: stub functions, placeholder types, TODO/FIXME, "Phase 2 unwritten" splits, marketing language masking incomplete work, tautological tests, stub docs, empty test files, silently dropped spec requirements.
|
||||
|
||||
If you discover an existing stub during audit (40-line README that fails `docs:check`, a function with `return undefined as never`, a barrel re-exporting nothing), the right answer is to **fix it inside the same change** as your other work. Leaving it is a vote for the placeholder.
|
||||
|
||||
## 2c. NEVER delete spec elements to satisfy a gate — wire them in
|
||||
|
||||
When a gate fails because a registered element (error code, schema, event topic, requirement, capability constant) is **declared but unused**, the failure is telling you the implementation hasn't caught up to the proposal. The fix is to **wire the element into the call site the proposal designed it for**, NOT to delete the element from the registry.
|
||||
|
||||
Deletion is the path of least resistance and silently strips features the proposal committed to. The proposal is the contract; the registry is the proposal expressed in code. Removing an entry to make a tool happy is the same defect as marking a failing test `.skip` to make CI green.
|
||||
|
||||
Concretely banned:
|
||||
|
||||
- **Removing an error code** from the codes registry because conformance check flags it as unused. Find the call site that should be raising / reporting it and wire it in.
|
||||
- **Deleting a schema field** because no caller populates it yet.
|
||||
- **Removing an event topic** because no emitter exists. Add the emitter; the topic is an API surface the proposal owns.
|
||||
- **Quietly removing a `## ADDED Requirements` line** because the implementation doesn't cover it. Either implement it or open a follow-up change that explicitly defers it with reasoning — never silently retract.
|
||||
- **Lowering coverage / docs-check thresholds** to make a gate pass.
|
||||
|
||||
When in doubt, ask: *"Was this element part of the proposal I'm implementing?"* If yes, **wire it in**. If genuinely no (it crept in by mistake), removing it requires a one-line note in `proposal.md` under `### Deferred` or `### Removed` so the deletion is auditable.
|
||||
|
||||
---
|
||||
|
||||
## 3. File-size budget, documentation, README
|
||||
|
||||
See **GENERIC-CLAUDE.md §§9, 11**. Summary:
|
||||
|
||||
- Source files 300 lines hard / 250 soft; test files 500 max; functions 50 soft.
|
||||
- Every `.ts` impl file needs a sibling `.md` with Purpose / Features / Basic / Advanced / API / Implementation Notes (≥200 words excl. code).
|
||||
- Doc-drift gate: changing a `.ts` requires touching the sibling `.md` in the same change set when the public surface or documented behavior changes.
|
||||
- Root `README.md` per package: name, description, install, quick-start, Modules link list.
|
||||
|
||||
---
|
||||
|
||||
## 4. Subpath exports per feature module
|
||||
|
||||
Every feature directory must have its own entry in `package.json` `exports`. A single `"."` barrel is insufficient. The subpath count should match the feature module count.
|
||||
|
||||
Verify entry points import nothing with side effects (telemetry registration, global state mutations). A `sideEffects: false` declaration with a leaking side effect is a silent bug.
|
||||
|
||||
---
|
||||
|
||||
## 5. Barrel exports — explicit named only
|
||||
|
||||
See **GENERIC-CLAUDE.md §6.4**. Feature barrels MUST use explicit named exports; `export *` is allowed only in generated forwarder files and top-level namespace re-exports.
|
||||
|
||||
---
|
||||
|
||||
## 6. Coverage thresholds
|
||||
|
||||
**100 / 100 / 100 / 100** for every package, no exceptions, no "foundation only" carve-out:
|
||||
|
||||
```ts
|
||||
thresholds: { statements: 100, branches: 100, functions: 100, lines: 100 }
|
||||
```
|
||||
|
||||
The pre-publish gate MUST hard-fail below 100%. Mutation, contract, and property/fuzz testing layer on top — they do not substitute, and 100% line coverage does not substitute for them.
|
||||
|
||||
### Never delete code to make coverage pass
|
||||
|
||||
See **GENERIC-CLAUDE.md §8.3**. When a path is uncovered, write a test that exercises real behavior — do NOT delete defensive guards, rollback helpers, or catch arms.
|
||||
|
||||
---
|
||||
|
||||
## 7. Test runner imports — mandatory
|
||||
|
||||
All test files MUST import runner primitives (`describe`, `it`, `expect`, `vi`, `beforeAll`/`beforeEach`/`afterAll`/`afterEach`, etc.) from a **single project-wide neutral wrapper** that re-exports the active runner via conditional exports:
|
||||
|
||||
```ts
|
||||
// CORRECT — runner-neutral
|
||||
import { describe, expect, it, vi } from "<project>/testing/runner";
|
||||
|
||||
// BANNED — pins the test file to one runner
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { describe, expect, it } from "vitest";
|
||||
```
|
||||
|
||||
The wrapper is a conditional-export barrel: `bun` condition → re-exports `bun:test`; `default` / `vitest` condition → re-exports `vitest`. Tests then execute identically under either runner.
|
||||
|
||||
**Exempt allowlist**: only the wrapper-authoring packages (the testing-infra packages themselves). Adding a package to the allowlist requires its own OpenSpec change with justification.
|
||||
|
||||
**Migration prerequisite**: when migrating any library from upstream, every `from "vitest"` / `from "bun:test"` import MUST be rewritten to the wrapper **inside the migration commit, not as a follow-up**. Same rule applies when porting test files between packages or when an LLM agent generates a fresh test file.
|
||||
|
||||
**Verification grep** (zero output for any non-exempt package):
|
||||
|
||||
```bash
|
||||
grep -rnE "from ['\"](vitest|bun:test)['\"]" \
|
||||
packages/<pkg>/src/ --include="*.ts" --include="*.tsx"
|
||||
```
|
||||
|
||||
A repo-wide CI lint gate must run this across every non-exempt package on every commit. Code review must independently flag direct runner imports — relying solely on CI is how 19 packages drifted before the gate existed.
|
||||
|
||||
### Test structure
|
||||
|
||||
Arrange-Act-Assert. Name tests after **behavior**, not implementation. See GENERIC-CLAUDE.md §10.2.
|
||||
|
||||
---
|
||||
|
||||
## 8. Task-runner filtering reference
|
||||
|
||||
```bash
|
||||
<task-runner> test --filter=<pkg> # one package
|
||||
<task-runner> test --filter=<pkg>... # pkg + its deps
|
||||
<task-runner> test --filter=...<pkg> # pkg + its dependents
|
||||
<task-runner> test --filter=...[main] # changed since main
|
||||
<task-runner> test --filter=...[HEAD^1] # changed in last commit
|
||||
<task-runner> test --filter={a,b,c} # multiple (OR)
|
||||
<task-runner> test --filter=!<pkg> # exclude
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Benchmarks
|
||||
|
||||
Every package should include benchmarks in `src/__benchmarks__/`. Use Markdown profiling output (CPU + heap) for LLM-assisted analysis. Publish baseline numbers in README.
|
||||
|
||||
---
|
||||
|
||||
## 10. Non-redundant path naming
|
||||
|
||||
A file or folder name must **not** contain the singular or plural form of any ancestor folder. See GENERIC-CLAUDE.md §6.2 for the rule and table.
|
||||
|
||||
Check:
|
||||
|
||||
```bash
|
||||
find packages/<pkg>/src -type d | awk -F/ '
|
||||
NF>=2 {
|
||||
parent=$(NF-1); child=$NF
|
||||
singular=parent; sub(/ies$/, "y", singular); sub(/s$/, "", singular)
|
||||
if (index(child, singular) > 0) print $0
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Type design — no inline shapes
|
||||
|
||||
See **GENERIC-CLAUDE.md §12**. Named interfaces/types only — no nested object literals in interfaces; no inline shapes in function signatures. Cross-package types live in the shared zero-dep types package.
|
||||
|
||||
---
|
||||
|
||||
## 12. Clean workspace
|
||||
|
||||
Before declaring any task done:
|
||||
|
||||
- Remove scratch files, debug outputs, temporary artifacts.
|
||||
- Verify `git status` shows only intentional changes.
|
||||
|
||||
## 12a. Spec-completeness verification — before any gate
|
||||
|
||||
**Never trust a compaction summary, archive status, or status-dashboard badge as proof that work is complete.** The only source of truth is the change's own `tasks.md` and `design.md`.
|
||||
|
||||
Before running ANY command in §13, manually:
|
||||
|
||||
1. **Open the change's `tasks.md`.** For migrations, also open the upstream `tasks.md`.
|
||||
2. **Walk every task item.** For each item that claims a file/module/type/feature exists, verify it is present. A task that says "Implement `geo/distance` — `greatCircleDistance` and `vincentyDistance`" means BOTH functions must exist, have tests, have docs, and be exported from the barrel.
|
||||
3. **For migrations, compare to upstream.** Diff the upstream `src/` against your equivalent. If upstream has `providers/admiralty/admiralty-provider.ts` with real fetch logic and yours has a 3-line stub, the migration is incomplete.
|
||||
4. **Empty directories are a red flag, not cleanup.** If a directory exists in `src/` and is empty, it was scaffolded for a feature that was never implemented. **Implement and populate it** — do NOT delete the directory to make the tree look tidy.
|
||||
5. **Cross-check against the parity checklist.** Every 🚫 row MUST have a written justification; every ⬆️ row must be real. If the table disagrees with the file tree, the table is wrong — fix the implementation, not the table.
|
||||
|
||||
If any task item is unchecked or unverified, the change is **not done**. Do not proceed to §13, do not archive, do not update the status dashboard.
|
||||
|
||||
---
|
||||
|
||||
## 13. Pre-ship verification sequence
|
||||
|
||||
Run in order — all must be green. **Step 0 is a hard gate; do not skip it.**
|
||||
|
||||
```bash
|
||||
# 0. SPEC RECONCILIATION — NON-NEGOTIABLE. Run BEFORE any other gate.
|
||||
# Walk every `### Requirement` and `#### Scenario` in the spec delta.
|
||||
# For each requirement, grep the implementation to confirm the named
|
||||
# class, function, constant, span, counter, histogram, event, or error
|
||||
# code actually exists in src/. For each `**Verified by:**`, confirm
|
||||
# the test file exists and the test name matches *verbatim*.
|
||||
# If any requirement is unimplemented or any Verified-by is broken,
|
||||
# STOP. Do not run step 1.
|
||||
|
||||
# 1. Lint + typecheck + tests + format
|
||||
<task-runner> lint typecheck test --filter=<pkg>
|
||||
<formatter> check # e.g. biome check --write --unsafe src/
|
||||
|
||||
# 2. Coverage at 100%
|
||||
<task-runner> test:coverage --filter=<pkg>
|
||||
|
||||
# 3. Build
|
||||
<task-runner> build --filter=<pkg>
|
||||
|
||||
# 4. Banned-pattern scan (zero output expected)
|
||||
grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\.skip\|\.only" \
|
||||
packages/<pkg>/src/ --include="*.ts"
|
||||
|
||||
# 4a. Barrel-shape scan (zero output for any feature barrel)
|
||||
grep -rn "^\s*export\s\+\(type\s\+\)\?\*\s\+from" \
|
||||
packages/<pkg>/src/ --include="index.ts" \
|
||||
| grep -v "/src/index\.ts:" \
|
||||
| grep -v "\.select/" \
|
||||
| grep -v "export \* as"
|
||||
|
||||
# 4b. Test-runner import scan (zero output for non-exempt packages)
|
||||
grep -rnE "from ['\"](vitest|bun:test)['\"]" \
|
||||
packages/<pkg>/src/ --include="*.ts" --include="*.tsx"
|
||||
|
||||
# 5. Missing .md scan
|
||||
for f in $(find packages/<pkg>/src -name "*.ts" \
|
||||
! -name "*.test.ts" ! -name "*.spec.ts" ! -name "*.types.ts" \
|
||||
! -name "index.ts"); do
|
||||
md="${f%.ts}.md"; [ -f "$md" ] || echo "MISSING: $md"
|
||||
done
|
||||
|
||||
# 6. README check
|
||||
test -f packages/<pkg>/README.md && echo "README OK" || echo "MISSING README"
|
||||
|
||||
# 7. Bare-version check (zero output expected)
|
||||
node -e "
|
||||
const p = JSON.parse(require('fs').readFileSync('packages/<pkg>/package.json','utf8'));
|
||||
const all = {...p.dependencies,...p.devDependencies,...p.peerDependencies};
|
||||
for (const [k,v] of Object.entries(all ?? {})) {
|
||||
if (typeof v === 'string' && /^[\^~]?\d/.test(v)) console.log('BARE VERSION:', k, v);
|
||||
}
|
||||
"
|
||||
|
||||
# 8. Subpath export count
|
||||
node -e "
|
||||
const p = JSON.parse(require('fs').readFileSync('packages/<pkg>/package.json','utf8'));
|
||||
const sub = Object.keys(p.exports ?? {}).filter(k => k.startsWith('./') && k !== '.');
|
||||
console.log('Subpath exports:', sub.length, sub);
|
||||
"
|
||||
|
||||
# 9. Security scan — hardcoded secrets (zero output expected)
|
||||
grep -rn \
|
||||
"password\s*=\s*['\"][^'\"]\|secret\s*=\s*['\"][^'\"]\|apiKey\s*=\s*['\"][^'\"]\|token\s*=\s*['\"][^'\"]" \
|
||||
packages/<pkg>/src/ --include="*.ts"
|
||||
|
||||
# 10. Dependent sweep — mandatory for foundation packages
|
||||
<task-runner> test --filter=...<pkg>
|
||||
```
|
||||
|
||||
For **foundation packages** (widely-consumed: error / types / events / logger / validation in this project's case), step 10 is mandatory — catches API-shape regressions BEFORE shipping. Green local tests do not prove you didn't break consumers.
|
||||
|
||||
For non-foundation packages, step 10 is optional — but if you altered the public surface, run it anyway. The cost is one command; the cost of skipping is finding out from a consumer's CI next week.
|
||||
|
||||
---
|
||||
|
||||
## 13a. Behavior-change propagation
|
||||
|
||||
See **GENERIC-CLAUDE.md §19**. When a change introduces consumer-visible behavior changes (new error code, renamed metric, method that now throws, default flipped, return shape change), you MUST:
|
||||
|
||||
1. **Self-doc**: update sibling `.md` + `README.md` Features section.
|
||||
2. **Dependent surface scan**: `grep -rln "<old-name>" packages/ | grep -v "/dist/\|/<own-pkg>/"`. Every hit needs updating in the same change set.
|
||||
3. **Migration note**: add `## Migration` to `proposal.md` listing (a) old behavior, (b) new behavior, (c) consumer search command, (d) recommended replacement.
|
||||
|
||||
Skipping any of these means a consumer somewhere is acting on an obsolete contract.
|
||||
|
||||
---
|
||||
|
||||
## 13b. Root-doc refresh + markdown-lint
|
||||
|
||||
Once §13 is green, but **before** archival (§14), refresh the repo-root manifest docs and run the markdown-lint pass.
|
||||
|
||||
Mandatory updates:
|
||||
|
||||
- **Auto-generated manifests** (`MANIFEST.md`, `MISSING.yml`, dep-graph indexes): run the generators.
|
||||
- **Hand-edited status dashboard** (e.g. `STATUS.md`): move the package row from `Pending` → `Completed`, keeping icon and link conventions.
|
||||
- **Hand-edited summary doc** (e.g. `SUMMARY.md`): update or add the per-package paragraph to reflect the shipped surface.
|
||||
- **Migration narrative** (e.g. `MIGRATION.md`): update only if the change affects it.
|
||||
|
||||
Then auto-fix markdown:
|
||||
|
||||
```bash
|
||||
bunx markdownlint-cli2 --fix
|
||||
```
|
||||
|
||||
Re-stage anything the lint pass modified. Verify `git status` shows only intentional changes before archiving. If unfixable violations remain, resolve them by hand — never ship with lint failures, never silence with inline disables.
|
||||
|
||||
Verification before archival:
|
||||
|
||||
```bash
|
||||
git status # only intentional changes
|
||||
git diff MANIFEST.md MISSING.yml STATUS.md SUMMARY.md MIGRATION.md
|
||||
bunx markdownlint-cli2 # second pass, no --fix — must exit 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. OpenSpec archival
|
||||
|
||||
After all gates pass:
|
||||
|
||||
1. **Fold** the spec delta into `openspec/specs/<capability>/spec.md`.
|
||||
2. **Move** `openspec/changes/<change-id>/` → `openspec/archive/<yyyy-mm-dd>-<change-id>/`.
|
||||
|
||||
---
|
||||
|
||||
## 14a. Proposal must list affected dependents
|
||||
|
||||
Every `proposal.md` MUST contain an `## Impact` section that explicitly enumerates downstream packages and what was done in each:
|
||||
|
||||
```markdown
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
Run `<task-runner> ls --filter=...<pkg>` to enumerate. For each dependent, check one box:
|
||||
|
||||
- `<dep-1>` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: …)
|
||||
- `<dep-2>` — [x] tests run / [x] docs reviewed / [ ] no action needed
|
||||
- `<dep-3>` — …
|
||||
|
||||
If `tests run` is unchecked for any dependent, the change MUST NOT be folded into the canonical spec — finish that work first or split the proposal.
|
||||
```
|
||||
|
||||
The "no action needed" path requires a one-line reason (e.g. "consumer doesn't touch the changed surface"). It is NOT a free pass; it documents the conscious decision for the next agent reading the archive.
|
||||
|
||||
For **foundation packages**, the Affected-dependents list is non-optional. For non-foundation packages with zero dependents, write: "No dependents (verified by `<task-runner> ls --filter=...<pkg>`)."
|
||||
|
||||
Combined with §13 step 10 (dependent sweep) and §13a (behavior-change propagation), this closes the loop: the proposal names the blast radius, the verification proves the blast radius compiles and tests, and the migration note tells future readers what changed and how to adapt.
|
||||
2803
MANIFEST.md
Normal file
2803
MANIFEST.md
Normal file
File diff suppressed because it is too large
Load Diff
466
MIGRATION.md
Normal file
466
MIGRATION.md
Normal file
@@ -0,0 +1,466 @@
|
||||
# Vaulted Ecosystem — Enterprise Migration Guide
|
||||
|
||||
> **Status: Future reference — no active OpenSpec changes.**
|
||||
> This document is planning for the Vaulted password-manager project, not the current genesis enterprise library migration. OpenSpec changes will be created when these packages are scheduled.
|
||||
>
|
||||
> Consolidated architecture and migration plan for building a 1Password-equivalent password manager using the Suaveplan enterprise library stack.
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
This document consolidates all architectural decisions, new package requirements, and migration steps for building **Vaulted** — a cross-platform password manager (desktop, mobile, browser extensions) on top of the Suaveplan enterprise library ecosystem. It covers:
|
||||
|
||||
- **Technology stack** per platform (Electrobun, Capacitor+React, Bun, WebExtension MV2/MV3)
|
||||
- **New `@suaveplan/*` packages** required to fill critical gaps
|
||||
- **Storage architecture** — unifying `@suaveplan/drive` and `@suaveplan/storage-*`
|
||||
- **Vault orchestration layer** — the `@suaveplan/vault` package that ties everything together
|
||||
- **Migration sequencing** for tonight's library migration session
|
||||
|
||||
---
|
||||
|
||||
## 2. Platform Architecture Summary
|
||||
|
||||
| Platform | Runtime | UI | Build Tool | Native Messaging |
|
||||
|----------|---------|-----|------------|------------------|
|
||||
| **Desktop** | Electrobun | React 19 + Tailwind | Bun + Turborepo | Unix socket / named pipe |
|
||||
| **Mobile** | Capacitor (WKWebView/WebView) | React 19 + Tailwind | Bun + Vite | In-process (Capacitor bridge) |
|
||||
| **Chrome** | Manifest V3 service worker | React 19 + Tailwind | Bun + Vite | `runtime.connectNative` → local socket |
|
||||
| **Firefox** | Manifest V2 persistent background | React 19 + Tailwind | Bun + Vite | `runtime.connectNative` → local socket |
|
||||
| **Safari** | Safari App Extension (embedded in macOS app) | React 19 + Tailwind | Bun + Vite + Xcode | In-process `NSExtensionContext` |
|
||||
|
||||
**Icon generation** for all platforms is handled by `@suaveplan/iconz` (migrated from npm `iconz`), generating app/extension icons from a single source SVG.
|
||||
|
||||
---
|
||||
|
||||
## 3. Critical New Packages — Build These First
|
||||
|
||||
These packages have no viable external alternative and unblock core authentication, 2FA, passkeys, and extension communication.
|
||||
|
||||
### 3.1 `@suaveplan/srp` — Secure Remote Password (SRP-6a)
|
||||
|
||||
- **Purpose:** Zero-knowledge password authentication. Core to 1Password's auth model.
|
||||
- **Dependencies:** `@suaveplan/crypto-curve`, `@suaveplan/crypto-hash`
|
||||
- **Scope:** Client-side SRP implementation for signup, signin, session key agreement.
|
||||
- **Runtime:** Bun (desktop), browser (web), Capacitor (mobile)
|
||||
- **Estimate:** ~2-3K lines of big-integer math
|
||||
|
||||
### 3.2 `@suaveplan/otp` — TOTP/HOTP Generation & Verification
|
||||
|
||||
- **Purpose:** 2FA code generation for vault items (RFC 6238/4226).
|
||||
- **Dependencies:** `@suaveplan/crypto-hash` (HMAC-SHA1), `@suaveplan/bytes` (base32)
|
||||
- **Note:** `@suaveplan/security-codes` detects OTP secrets in strings (leak detection) but cannot *generate* time-based codes.
|
||||
- **Estimate:** ~500-800 lines
|
||||
|
||||
### 3.3 `@suaveplan/cbor` — Concise Binary Object Representation
|
||||
|
||||
- **Purpose:** CBOR encoder/decoder (RFC 8949). Required for WebAuthn/passkey support.
|
||||
- **Dependencies:** None
|
||||
- **Estimate:** ~500-800 lines, tree-shakeable
|
||||
|
||||
### 3.4 `@suaveplan/webauthn` — WebAuthn / Passkey Client Library
|
||||
|
||||
- **Purpose:** CBOR attestation parsing, client data construction, COSE key parsing, assertion formatting. Monkey-patches `navigator.credentials` for browser extensions.
|
||||
- **Dependencies:** `@suaveplan/crypto-curve`, `@suaveplan/crypto-hash`, `@suaveplan/cbor`
|
||||
- **Note:** `@suaveplan/crypto-browser` provides non-extractable keys but no WebAuthn client logic.
|
||||
- **Estimate:** ~2-3K lines
|
||||
|
||||
### 3.5 `@suaveplan/native-messaging` — Encrypted Extension↔Desktop Protocol
|
||||
|
||||
- **Purpose:** Typed, encrypted transport between browser extensions and the desktop app.
|
||||
- **Protocol:** X25519 handshake + ChaCha20-Poly1305 framing. Abstracts Chrome stdio, Firefox stdio, and Safari `NSExtensionContext` behind one interface.
|
||||
- **Dependencies:** `@suaveplan/crypto-curve`, `@suaveplan/crypto-cipher`
|
||||
- **Estimate:** ~1K lines
|
||||
|
||||
---
|
||||
|
||||
## 4. High-Priority New Packages
|
||||
|
||||
### 4.1 `@suaveplan/password-strength` — Entropy & Crack-Time Estimation
|
||||
|
||||
- **Purpose:** zxcvbn-style password strength scoring.
|
||||
- **Architecture:** Two-tier — lightweight heuristic core (~5KB) + optional lazy-loaded dictionary (~200KB).
|
||||
- **Note:** `@suaveplan/security-codes` has Shannon entropy detection for leaked secrets, not crack-time estimation.
|
||||
|
||||
### 4.2 `@suaveplan/form-detection` — DOM Form Heuristics for Autofill
|
||||
|
||||
- **Purpose:** Detect login, registration, and password-change forms with confidence scoring.
|
||||
- **Runtime:** Browser extension content scripts (Chrome, Firefox, Safari).
|
||||
- **Note:** No good external standalone library exists. Bitwarden and 1Password keep this proprietary.
|
||||
- **Estimate:** ~10KB tree-shakeable engine
|
||||
|
||||
### 4.3 `@suaveplan/site-icons` — Favicon Fetching & Caching
|
||||
|
||||
- **Purpose:** Fetch and cache website icons (favicon.ico, Apple touch icons, manifest icons, Open Graph images) for display next to vault items.
|
||||
- **Dependencies:** `@suaveplan/domain-normalizer`, `@suaveplan/cache-browser`, `@suaveplan/mime`
|
||||
- **Note:** `iconz` (npm) is for **build-time app icon generation**, not runtime site icon fetching.
|
||||
|
||||
### 4.4 `@suaveplan/hibp` — Have I Been Pwned Client
|
||||
|
||||
- **Purpose:** Watchtower-style breach monitoring via HIBP k-anonymity API.
|
||||
- **Dependencies:** `@suaveplan/crypto-hash` (SHA-1 prefix), `@suaveplan/retry`
|
||||
|
||||
### 4.5 `@suaveplan/qrcode-scan` — QR Code Decoding / Scanning
|
||||
|
||||
- **Purpose:** Import TOTP secrets by scanning QR codes (camera or image upload).
|
||||
- **Note:** `@suaveplan/qrcode` is **generation-only**. This adds the decode/scan subpath.
|
||||
- **Decision:** Either extend `@suaveplan/qrcode` with `/scan` subpath or create standalone package.
|
||||
|
||||
### 4.6 `@suaveplan/biometrics` — Universal Biometric Auth Abstraction
|
||||
|
||||
- **Purpose:** Abstract Touch ID / Face ID / Windows Hello / Android BiometricPrompt.
|
||||
- **Delegates:** Capacitor plugins (mobile), Electrobun native bridge (desktop), `@suaveplan/macos-keychain` (macOS)
|
||||
- **Estimate:** Thin facade (~300 lines)
|
||||
|
||||
---
|
||||
|
||||
## 5. Medium-Priority New Packages
|
||||
|
||||
### 5.1 `@suaveplan/password-generator`
|
||||
|
||||
- Pronounceable passwords (diceware), memorable passphrases, PINs, charset-constrained generation with entropy estimation.
|
||||
- `@suaveplan/random` has `randomString` but no password-specific logic.
|
||||
|
||||
### 5.2 `@suaveplan/vault-sync` — Domain-Specific Sync & Conflict Resolution
|
||||
|
||||
- Build on top of `@suaveplan/crdt` and `@suaveplan/diff` (JSON Patch).
|
||||
- Vault item merge strategies: last-write-wins for passwords, append-only for history, atomic for moves.
|
||||
|
||||
### 5.3 `@suaveplan/vault` — Vault Orchestration Layer *(see Section 7)*
|
||||
|
||||
---
|
||||
|
||||
## 6. Storage Architecture — Unifying Drive and Storage
|
||||
|
||||
### 6.1 Current Problem
|
||||
|
||||
`@suaveplan/drive` (~7,300 LOC, 127 files) is a mature virtual filesystem with encryption transforms, chunking, deduplication, snapshots, COW, WAL, and journaling. **But it only ships with `MemoryStorage`.**
|
||||
|
||||
`@suaveplan/storage` is a lightweight key-value abstraction with actual persistence backends:
|
||||
|
||||
- `@suaveplan/storage-file` (Node.js/Bun filesystem)
|
||||
- `@suaveplan/storage-browser` (IndexedDB + localStorage)
|
||||
- `@suaveplan/storage-browser-opfs` (Origin Private File System)
|
||||
- `@suaveplan/storage-s3` (S3-compatible object storage)
|
||||
|
||||
**The two ecosystems are disconnected.** Drive has its own `StorageAdapter` interface that is incompatible with `storage`'s `AbstractStorage`.
|
||||
|
||||
### 6.2 The Fix: Layered Architecture
|
||||
|
||||
Do **not** make `storage` subservient to `drive`. They serve different abstraction levels:
|
||||
|
||||
```text
|
||||
Layer 3: @suaveplan/vault (orchestration)
|
||||
Layer 2: @suaveplan/drive (filesystem: encryption, chunking, snapshots, POSIX API)
|
||||
Layer 1: @suaveplan/storage-* (persistence backends: file, S3, IndexedDB, OPFS)
|
||||
```
|
||||
|
||||
Drive sits **on top of** storage backends. Storage is the raw persistence substrate.
|
||||
|
||||
### 6.3 Required Enhancements to Storage Providers
|
||||
|
||||
Drive's `StorageAdapter` needs capabilities that `AbstractStorage` doesn't currently expose:
|
||||
|
||||
| Capability | Status | Implementation |
|
||||
|-----------|--------|----------------|
|
||||
| `get(key)` | ✅ Exists | — |
|
||||
| `set(key, value)` | ✅ Exists | — |
|
||||
| `delete(key)` | ✅ Exists | — |
|
||||
| `exists(key)` | ✅ Exists | — |
|
||||
| `list({ prefix })` | ✅ Exists | — |
|
||||
| `getPartial(key, offset, len)` | ❌ Missing | Add to `AbstractStorage`; emulate via `get` + slice for providers that can't do native partial reads |
|
||||
| `getItemSize(key)` | ❌ Missing | Add to `AbstractStorage`; store size in metadata or compute on demand |
|
||||
| `createReadStream(key)` | ❌ Missing | Add to `AbstractStorage`; emulate for providers without native streaming |
|
||||
| `createWriteStream(key)` | ❌ Missing | Add to `AbstractStorage`; emulate via buffering |
|
||||
|
||||
**Effort:** ~20-50 lines per provider. `file` and `opfs` can implement natively. `indexeddb`, `s3`, `localstorage` can emulate.
|
||||
|
||||
### 6.4 The Universal Drive Adapter
|
||||
|
||||
Create **one** adapter in `drive` that wraps any `storage` provider:
|
||||
|
||||
```typescript
|
||||
// @suaveplan/drive/src/adapters/storage-adapter.ts
|
||||
export class StorageProviderAdapter extends StorageAdapter<StorageProviderConfig> {
|
||||
private storage: AbstractStorage;
|
||||
|
||||
async connect() {
|
||||
this.storage = StorageFactory.create(this.config.provider);
|
||||
this._connected = true;
|
||||
}
|
||||
|
||||
async read(key: string): Promise<Uint8Array> {
|
||||
const data = await this.storage.get(key);
|
||||
if (!data) throw new Error(`Key not found: ${key}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
async readPartial(key: string, offset: number, length: number): Promise<Uint8Array> {
|
||||
if ('getPartial' in this.storage) {
|
||||
return (this.storage as any).getPartial(key, offset, length);
|
||||
}
|
||||
const full = await this.read(key);
|
||||
return full.slice(offset, offset + length);
|
||||
}
|
||||
|
||||
async write(key: string, data: BufferLike): Promise<void> {
|
||||
await this.storage.set(key, toUint8Array(data));
|
||||
}
|
||||
|
||||
async size(key: string): Promise<number> {
|
||||
if ('getItemSize' in this.storage) {
|
||||
return (this.storage as any).getItemSize(key);
|
||||
}
|
||||
const data = await this.storage.get(key);
|
||||
return data?.length ?? 0;
|
||||
}
|
||||
|
||||
// ... exists, delete, list, streaming
|
||||
}
|
||||
```
|
||||
|
||||
Then `configureDrive()` accepts storage provider configs directly:
|
||||
|
||||
```typescript
|
||||
const drive = await configureDrive({
|
||||
storage: {
|
||||
type: 'storage-provider',
|
||||
provider: { type: 'file', config: { basePath: './data' } }
|
||||
},
|
||||
encryption: { keyLength: 256 },
|
||||
});
|
||||
```
|
||||
|
||||
### 6.5 Factory Pattern Unification
|
||||
|
||||
Backport `storage`'s module-augmentation factory pattern into `drive`'s adapter registry:
|
||||
|
||||
```typescript
|
||||
// @suaveplan/storage-file/index.ts
|
||||
import { StorageFactory } from '@suaveplan/storage';
|
||||
import { DriveAdapterFactory } from '@suaveplan/drive';
|
||||
|
||||
StorageFactory.register('file', FileSystem);
|
||||
DriveAdapterFactory.register('file', (config) => new StorageProviderAdapter(config));
|
||||
```
|
||||
|
||||
Installing `@suaveplan/storage-file` automatically makes `type: 'file'` available to **both** `storage` and `drive`.
|
||||
|
||||
### 6.6 Migration Checklist for Storage
|
||||
|
||||
| Step | Action | Effort |
|
||||
|------|--------|--------|
|
||||
| 1 | Add `getPartial()`, `getItemSize()`, `createReadStream()`, `createWriteStream()` to `AbstractStorage` | ~1h |
|
||||
| 2 | Implement enhanced methods in `storage-file`, `storage-browser`, `storage-opfs` | ~2h |
|
||||
| 3 | Create `StorageProviderAdapter` in `drive` | ~1h |
|
||||
| 4 | Wire `StorageFactory` auto-registration into drive's config | ~1h |
|
||||
| 5 | Backport module-augmentation pattern to drive adapter registry | ~1h |
|
||||
| 6 | Deprecate drive's standalone `MemoryStorage` in favor of adapter wrapping `storage` memory provider | ~30m |
|
||||
|
||||
**Total: ~6-7 hours.**
|
||||
|
||||
---
|
||||
|
||||
## 7. `@suaveplan/vault` — The Orchestration Layer
|
||||
|
||||
### 7.1 The Problem
|
||||
|
||||
`@suaveplan/keys` currently wears two hats:
|
||||
|
||||
1. **Key material management** (keypair generation, validation, keychain, chain signing) — correct scope
|
||||
2. **Vault container management** (VaultFactory, VaultManager, packVault, unpackVault, password-based encryption) — scope creep
|
||||
|
||||
### 7.2 Proposed Boundaries
|
||||
|
||||
| Package | Scope |
|
||||
|---------|-------|
|
||||
| `@suaveplan/keys` | Keypair generation, validation, keychain storage, chain signing. **No vault logic.** |
|
||||
| `@suaveplan/crypto-vault` | BIP39 mnemonics, Argon2id HD key derivation, 2SKD (password + Secret Key → AUK), SRP-6a, key hierarchy (AUK → personal keyset → vault key → item key) |
|
||||
| `@suaveplan/vault` *(new)* | Orchestrates `crypto-vault` + `keys` + `db-sqlite` + `drive` + `drive-sharing` into a single "vault" abstraction |
|
||||
|
||||
### 7.3 What Moves Where
|
||||
|
||||
**Stays in `@suaveplan/keys`:**
|
||||
|
||||
- `generateKeypair()`, `validateKeypair()`, `assertValidKeypair()`
|
||||
- `Keychain` class and adapters (`MemoryKeychainAdapter`, `SerializedKeychainAdapter`)
|
||||
- `ChainedKeySigner`
|
||||
- Key-type definitions (`MasterKey`, `EncryptKey`, `SignKey`, `KxKey`)
|
||||
|
||||
**Moves OUT of `@suaveplan/keys` → `@suaveplan/vault`:**
|
||||
|
||||
- `VaultManager` → `Vault`
|
||||
- `VaultFactory` → `VaultFactory`
|
||||
- `packVault()` / `unpackVault()` → `Vault.serialize()` / `Vault.deserialize()`
|
||||
- `createRootKeys()` / `createServerKeys()` → `@suaveplan/crypto-vault` (or `vault`)
|
||||
|
||||
### 7.4 Vault API
|
||||
|
||||
```typescript
|
||||
import { Vault } from '@suaveplan/vault';
|
||||
|
||||
const vault = await Vault.open({
|
||||
path: '/Users/alice/.vaulted/vault',
|
||||
password: '...',
|
||||
secretKey: 'A3-XXXXXX-...',
|
||||
email: 'alice@example.com',
|
||||
});
|
||||
|
||||
await vault.unlock();
|
||||
|
||||
// Item CRUD (delegates to db-sqlite)
|
||||
const login = await vault.items.create({
|
||||
category: 'login',
|
||||
title: 'GitHub',
|
||||
fields: [
|
||||
{ type: 'username', value: 'alice' },
|
||||
{ type: 'password', value: '...', concealed: true },
|
||||
{ type: 'url', value: 'https://github.com' },
|
||||
],
|
||||
});
|
||||
|
||||
// Search (delegates to db-sqlite FTS5)
|
||||
const results = await vault.items.search({ query: 'github' });
|
||||
|
||||
// Attachments (delegates to drive)
|
||||
await vault.attachments.upload(login.id, '/path/to/ssh-key.pem');
|
||||
|
||||
// Sharing (delegates to drive-sharing)
|
||||
const link = await vault.shares.createLink({
|
||||
itemId: login.id,
|
||||
expiresIn: '7d',
|
||||
maxViews: 5,
|
||||
});
|
||||
|
||||
// Sync (delegates to sync engine)
|
||||
await vault.sync.pull();
|
||||
await vault.sync.push();
|
||||
|
||||
// Lock (zeroizes keys, closes DB)
|
||||
await vault.lock();
|
||||
```
|
||||
|
||||
### 7.5 Internal Structure
|
||||
|
||||
```typescript
|
||||
class Vault {
|
||||
private keyVault: MasterKeyVault; // from @suaveplan/crypto-vault
|
||||
private itemRepo: ItemRepository; // uses @suaveplan/db-sqlite
|
||||
private fileManager: FileManager; // uses @suaveplan/drive
|
||||
private shareManager: ShareManager; // uses @suaveplan/drive-sharing
|
||||
private syncEngine: SyncEngine; // new, uses @suaveplan/api + ws
|
||||
|
||||
async unlock(password: string, secretKey: string) {
|
||||
// 1. Derive AUK via crypto-vault (2SKD)
|
||||
// 2. Decrypt personal keyset
|
||||
// 3. Decrypt vault keys
|
||||
// 4. Open SQLite DB with vault key
|
||||
// 5. Open drive with vault key
|
||||
}
|
||||
|
||||
async lock() {
|
||||
// 1. Close SQLite
|
||||
// 2. Destroy drive session keys
|
||||
// 3. Zeroize all key material in crypto-vault
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.6 `@suaveplan/drive-sharing` Complementarity
|
||||
|
||||
`drive-sharing` is **not** replaced by `vault`. They layer:
|
||||
|
||||
| Package | Scope |
|
||||
|---------|-------|
|
||||
| `drive-sharing` | Primitives: envelope encryption, partition stores, grant tokens, share links |
|
||||
| `vault` | Orchestration: "share this login item" → extracts from DB → encrypts via `drive-sharing` → uploads via `drive` |
|
||||
|
||||
`vault` is the password-manager domain layer. `drive-sharing` is the secure-sharing primitive layer.
|
||||
|
||||
---
|
||||
|
||||
## 8. External Dependencies — Keep vs Build
|
||||
|
||||
| Need | Verdict | External | Rationale |
|
||||
|------|---------|----------|-----------|
|
||||
| SRP-6a | **Build** | None good | Critical, well-defined protocol |
|
||||
| TOTP/HOTP | **Build** | `otpauth` pulls own crypto | Small spec, reuse our hash primitives |
|
||||
| WebAuthn client | **Build** | `@simplewebauthn` has deps | Needs our crypto + new CBOR package |
|
||||
| CBOR | **Build** | `cbor-x` is heavy | Small spec, unlocks WebAuthn |
|
||||
| Password strength | **Build** | `zxcvbn` is 800KB | Two-tier: light core + optional dictionaries |
|
||||
| Form detection | **Build** | None standalone | Proprietary in competing products |
|
||||
| Site icons | **Build** | None good | Domain-specific caching + fetch logic |
|
||||
| HIBP client | **Build** | None maintained | Small, privacy-critical wrapper |
|
||||
| QR scan | **Build/extend** | `jsQR` is decent | Extend existing `@suaveplan/qrcode` |
|
||||
| WebExtension polyfill | **Keep external** | `webextension-polyfill` | Mozilla-maintained, 15KB, excellent |
|
||||
| PDF generation | **Wrap external** | `pdf-lib` | Universal, tree-shakeable, ~100KB |
|
||||
| Image compression | **Keep external** | `sharp` (Node), Capacitor plugins | Native bindings are hard to beat |
|
||||
| SQLCipher | **Keep external** | `sqlcipher` native | C library, hard to replace in pure TS |
|
||||
| Time sync (NTP) | **Inline** | None needed | ~50 lines of UDP code |
|
||||
| YubiKey/WebHID | **Defer** | Platform-specific | Hardware integration, post-MVP |
|
||||
|
||||
---
|
||||
|
||||
## 9. Migration Sequencing for Tonight
|
||||
|
||||
### Phase 1: Storage Bridge (6-7 hours)
|
||||
|
||||
1. Enhance `AbstractStorage` with `getPartial()`, `getItemSize()`, streaming
|
||||
2. Implement in `storage-file`, `storage-browser`, `storage-opfs`
|
||||
3. Create `StorageProviderAdapter` in `drive`
|
||||
4. Wire auto-registration so storage providers are available to drive
|
||||
5. Test: `configureDrive({ storage: { type: 'file', ... }})` works end-to-end
|
||||
|
||||
### Phase 2: Vault Rearchitecture (4-5 hours)
|
||||
|
||||
1. Strip vault logic from `@suaveplan/keys` (move to `@suaveplan/vault`)
|
||||
2. Ensure `@suaveplan/crypto-vault` owns key hierarchy + 2SKD
|
||||
3. Scaffold `@suaveplan/vault` with `Vault.open()`, `unlock()`, `lock()`
|
||||
4. Wire `vault.items` → `db-sqlite`, `vault.attachments` → `drive`
|
||||
|
||||
### Phase 3: Critical Crypto Packages (5-6 hours)
|
||||
|
||||
1. `@suaveplan/cbor` — encoder/decoder
|
||||
2. `@suaveplan/srp` — SRP-6a client
|
||||
3. `@suaveplan/otp` — TOTP/HOTP generation
|
||||
4. `@suaveplan/webauthn` — WebAuthn client (depends on CBOR)
|
||||
|
||||
### Phase 4: Extension Infrastructure (3-4 hours)
|
||||
|
||||
1. `@suaveplan/native-messaging` — encrypted transport
|
||||
2. `@suaveplan/form-detection` — DOM heuristics
|
||||
3. `@suaveplan/password-strength` — heuristic + dictionary scorer
|
||||
|
||||
### Phase 5: Utilities (2-3 hours)
|
||||
|
||||
1. `@suaveplan/site-icons` — favicon fetch/cache
|
||||
2. `@suaveplan/hibp` — breach monitoring client
|
||||
3. `@suaveplan/qrcode-scan` — QR decode subpath
|
||||
4. `@suaveplan/biometrics` — universal biometric facade
|
||||
5. `@suaveplan/iconz` — migrate from npm, integrate into build pipelines
|
||||
|
||||
**Total estimate: 20-25 hours across all phases.** Not a single night, but Phase 1+2 (storage bridge + vault rearchitecture) is the critical foundation and can be done in one focused session.
|
||||
|
||||
---
|
||||
|
||||
## 10. Security Invariants
|
||||
|
||||
These must hold across all packages:
|
||||
|
||||
1. **Key material never leaves `@suaveplan/crypto-vault` / `@suaveplan/keys` memory space.** `vault`, `drive`, `db-sqlite` only receive derived encryption keys, not the AUK or mnemonic.
|
||||
2. **All key material is zeroized on `vault.lock()`.** Use `@suaveplan/crypto-utils` `zeroize()` on `Uint8Array` instances.
|
||||
3. **Decryption happens exclusively on the client.** The sync server only ever sees encrypted blobs.
|
||||
4. **Biometric data never leaves the hardware secure enclave.** Platform APIs return boolean only.
|
||||
5. **Extension crypto is delegated to the desktop app.** Browser extensions never hold vault keys.
|
||||
|
||||
---
|
||||
|
||||
## 11. References
|
||||
|
||||
- `../vaulted/REPORT.md` — 1Password architecture analysis
|
||||
- `../vaulted/apps/desktop/REPORT.md` — Electrobun desktop build spec
|
||||
- `../vaulted/apps/mobile/REPORT.md` — Capacitor mobile build spec
|
||||
- `../vaulted/plugins/chrome/REPORT.md` — Chrome MV3 extension spec
|
||||
- `../vaulted/plugins/firefox/REPORT.md` — Firefox MV2 extension spec
|
||||
- `../vaulted/plugins/safari/REPORT.md` — Safari App Extension spec
|
||||
- `../suaveplan/README.md` — Full package inventory (356 packages)
|
||||
- `STATUS.md` — Dashboard table of every package's migration status
|
||||
- `SUMMARY.md` — Per-package summaries (completed / pending / genesis-original)
|
||||
383
MISSING.yml
Normal file
383
MISSING.yml
Normal file
@@ -0,0 +1,383 @@
|
||||
# MISSING — packages not yet created on disk.
|
||||
|
||||
# Auto-generated by scripts/generate-missing.ts — do not edit manually.
|
||||
|
||||
# Re-run after landing a new package to refresh.
|
||||
|
||||
universal:
|
||||
config:
|
||||
- config
|
||||
control-flow:
|
||||
- undo
|
||||
auth:
|
||||
- auth
|
||||
- permissions
|
||||
- jwt-tenant
|
||||
- audit-logging
|
||||
- rate-limiter
|
||||
database:
|
||||
- db
|
||||
- db-sqlite
|
||||
- surrealdb
|
||||
- cache
|
||||
- migration
|
||||
storage:
|
||||
- storage
|
||||
- storage-s3
|
||||
- drive
|
||||
data:
|
||||
- etl
|
||||
- search
|
||||
- crdt
|
||||
compression:
|
||||
# compress-svg → packages/docs/compress-svg (shipped)
|
||||
# font-compress → packages/fonts/font-compress (shipped)
|
||||
# bitpacker → packages/bitpacker (shipped; family hardened 2026-05-03)
|
||||
# packer → packages/bitpacker/packer (shipped)
|
||||
- font-decompress
|
||||
- audio-sprite-packer
|
||||
- emblem-solver
|
||||
network:
|
||||
- network
|
||||
- network-websockets
|
||||
- network-observability
|
||||
- network-standby-transport
|
||||
- network-assignment
|
||||
- network-bootstrap
|
||||
- network-failover
|
||||
- network-governance
|
||||
- network-protocol
|
||||
- network-quality
|
||||
- network-registry
|
||||
- network-testing
|
||||
- service-discovery
|
||||
streams:
|
||||
- streams
|
||||
- streams-metadata
|
||||
- streams-chunk-queue
|
||||
- queue
|
||||
- queue-distribution
|
||||
events:
|
||||
- events-metrics
|
||||
- events-otel
|
||||
- events-schema
|
||||
- events-transport
|
||||
- event-sourcing
|
||||
media:
|
||||
- media
|
||||
- image
|
||||
- image-quantisation
|
||||
- video
|
||||
- video-thumbnails
|
||||
- dmx
|
||||
email:
|
||||
- email-processing
|
||||
- email-templates
|
||||
- campaigns
|
||||
- contacts
|
||||
- delivery-tracking
|
||||
- audience-rules
|
||||
social:
|
||||
- slack-client
|
||||
- discord-client
|
||||
- telegram-client
|
||||
- imessage-adapter
|
||||
- message-bridge
|
||||
messaging:
|
||||
- sms-providers
|
||||
- notification-channel
|
||||
commerce:
|
||||
- payment
|
||||
- payment-providers
|
||||
- booking
|
||||
- license
|
||||
geo:
|
||||
- ports
|
||||
- heightmap-tiles
|
||||
- osm-buildings
|
||||
platform:
|
||||
- electrobun
|
||||
- macos-contacts
|
||||
- profiler
|
||||
docs:
|
||||
- api-docs
|
||||
- docs
|
||||
dev:
|
||||
- wip
|
||||
workflow:
|
||||
- workflow
|
||||
- form-builder
|
||||
- ab-testing
|
||||
- rules
|
||||
browser:
|
||||
audio:
|
||||
- audio-engine
|
||||
- audio-plugins-fx
|
||||
- audio-controllers
|
||||
network:
|
||||
- network-webrtc
|
||||
- network-libp2p
|
||||
- drive-webrtc
|
||||
- network-discovery-crdt
|
||||
- network-discovery-gundb
|
||||
- network-discovery-hybrid
|
||||
- network-discovery-peerjs
|
||||
- network-discovery-trystero
|
||||
- network-discovery-webtorrent
|
||||
storage:
|
||||
- cache-browser
|
||||
- packer-browser
|
||||
- storage-browser
|
||||
- storage-browser-opfs
|
||||
ui:
|
||||
- react-command-palette
|
||||
- react-kanban
|
||||
- react-tour
|
||||
platform:
|
||||
- pwa
|
||||
- clipboard
|
||||
- web-workers
|
||||
- broadcast-channel
|
||||
- analytics-browser
|
||||
dev:
|
||||
- storybook-test-utils
|
||||
- test-vite
|
||||
react:
|
||||
ui:
|
||||
- react-ui
|
||||
- react-data-table
|
||||
- react-virtual
|
||||
- react-skeleton
|
||||
- react-charts
|
||||
- react-timeline
|
||||
- react-modal
|
||||
- react-dnd
|
||||
- react-infinite-scroll
|
||||
- react-tree-view
|
||||
- react-hotkeys
|
||||
- react-loading
|
||||
- react-iphone
|
||||
- react-error-boundary
|
||||
- react-notifications
|
||||
- react-store
|
||||
forms:
|
||||
- react-forms
|
||||
- react-admin-forms
|
||||
- react-wizard
|
||||
- react-rule-builder
|
||||
- react-color-picker
|
||||
- react-calendar
|
||||
auth:
|
||||
- react-auth
|
||||
- react-auth-whitelabel
|
||||
- react-permissions
|
||||
- react-crypto
|
||||
- react-consent-overlay
|
||||
media:
|
||||
- react-media
|
||||
- react-image
|
||||
- react-video
|
||||
- react-audio
|
||||
- react-audio-player
|
||||
- react-audio-stream
|
||||
- react-audio-fingerprint
|
||||
- react-file-upload
|
||||
- react-font-decompress
|
||||
- react-pdf
|
||||
- react-rich-text
|
||||
social:
|
||||
- react-social
|
||||
- react-ui-social
|
||||
platform:
|
||||
- react-docs
|
||||
- react-map
|
||||
- react-drive-sharing
|
||||
- react-campaigns
|
||||
- react-contacts
|
||||
- react-port-tracking
|
||||
- react-translate
|
||||
api:
|
||||
framework:
|
||||
- api
|
||||
- api-shared
|
||||
- api-gateway
|
||||
- websocket-gateway
|
||||
auth:
|
||||
- api-plugin-auth
|
||||
- api-plugin-admin
|
||||
- api-plugin-user
|
||||
- api-plugin-tenant
|
||||
- api-plugin-vault
|
||||
commerce:
|
||||
- api-plugin-payment
|
||||
- api-plugin-banking
|
||||
- api-plugin-store
|
||||
- api-plugin-subscription
|
||||
content:
|
||||
- api-plugin-cms
|
||||
- api-plugin-site
|
||||
- api-plugin-events
|
||||
comms:
|
||||
- api-plugin-messaging
|
||||
- api-plugin-notifications
|
||||
- api-plugin-campaigns
|
||||
- api-plugin-contacts
|
||||
- api-plugin-social
|
||||
- api-plugin-webhook
|
||||
data:
|
||||
- api-plugin-database
|
||||
- api-plugin-search
|
||||
- api-plugin-analytics
|
||||
- api-plugin-export
|
||||
- api-plugin-import
|
||||
- api-plugin-file-upload
|
||||
- api-plugin-sse
|
||||
- api-plugin-rate-limit
|
||||
- api-plugin-audit
|
||||
- api-plugin-graphql
|
||||
logistics:
|
||||
- api-plugin-logistics
|
||||
- api-plugin-customs
|
||||
- api-plugin-procurement
|
||||
- api-plugin-warehouse-ops
|
||||
- api-plugin-asset-register
|
||||
- api-plugin-port-tracking
|
||||
- api-plugin-vatdash-bridge
|
||||
- api-plugin-gtap
|
||||
- api-plugin-inventory
|
||||
r3f:
|
||||
geo:
|
||||
- r3f-earth
|
||||
commerce:
|
||||
- r3f-booking
|
||||
media:
|
||||
- r3f-dmx
|
||||
server:
|
||||
data:
|
||||
- storage-file
|
||||
- cache-redis
|
||||
- cache-memory
|
||||
network:
|
||||
- network-hyperswarm
|
||||
- network-libp2p-server
|
||||
- network-gundb-server
|
||||
- network-trystero-server
|
||||
infra:
|
||||
- caddy-imgproxy
|
||||
- job-scheduler
|
||||
- health-check
|
||||
- seed
|
||||
- server-keys
|
||||
- linter
|
||||
- sandbox
|
||||
services:
|
||||
- pdf
|
||||
- sms
|
||||
- emailer
|
||||
finance:
|
||||
crypto:
|
||||
- cryptocurrency
|
||||
mcp:
|
||||
core:
|
||||
- mcp
|
||||
python:
|
||||
audio:
|
||||
- python-audio-fingerprint
|
||||
gaming:
|
||||
engine:
|
||||
- ecs
|
||||
- game-loop
|
||||
- spatial-hash
|
||||
- game-persistence
|
||||
- game-server
|
||||
- game-state-schemas
|
||||
- controller
|
||||
rigs:
|
||||
- character-physics
|
||||
- vehicle-physics
|
||||
- mixamo
|
||||
- bone-mapping
|
||||
- ik-solver
|
||||
- animation-events
|
||||
- character-vehicle-picker
|
||||
combat:
|
||||
- combat
|
||||
- weapons
|
||||
- hitbox
|
||||
- destructibles
|
||||
ai:
|
||||
- behavior-tree
|
||||
- npc
|
||||
- fauna
|
||||
narrative:
|
||||
- dialogue
|
||||
- quest
|
||||
- crafting
|
||||
- progression
|
||||
- economy
|
||||
world:
|
||||
- building
|
||||
- day-night
|
||||
- weather
|
||||
multiplayer:
|
||||
- lobby
|
||||
- party
|
||||
- chat
|
||||
- emotes
|
||||
- leaderboard
|
||||
server:
|
||||
- api-plugin-bank
|
||||
- api-plugin-game-server
|
||||
- api-plugin-game-persistence
|
||||
- api-plugin-matchmaking
|
||||
- api-plugin-anticheat
|
||||
- api-plugin-virtual-currency
|
||||
react:
|
||||
- react-game-hud
|
||||
- react-game-map
|
||||
- react-game-settings
|
||||
- react-dialogue
|
||||
- react-crafting
|
||||
- react-controller
|
||||
- react-bank
|
||||
- react-social-game
|
||||
scene:
|
||||
- r3f-terrain
|
||||
- r3f-world
|
||||
- r3f-vegetation
|
||||
- r3f-water
|
||||
- r3f-weather
|
||||
- r3f-buildings
|
||||
- r3f-building
|
||||
- r3f-environment
|
||||
character:
|
||||
- r3f-character
|
||||
- r3f-character-vehicle
|
||||
- r3f-character-vehicle-picker
|
||||
- r3f-animation
|
||||
- r3f-controller
|
||||
physics:
|
||||
- r3f-physics-core
|
||||
- r3f-physics-rapier
|
||||
- r3f-physics-cascade
|
||||
effects:
|
||||
- r3f-particles
|
||||
- r3f-shaders
|
||||
- r3f-parallax
|
||||
- r3f-audio
|
||||
- r3f-lod
|
||||
- r3f-performance
|
||||
devices:
|
||||
- r3f-camera
|
||||
- r3f-vehicle
|
||||
- r3f-number-plate
|
||||
- r3f-emulator
|
||||
- r3f-xr
|
||||
gameplay:
|
||||
- r3f-combat
|
||||
- r3f-inventory
|
||||
- r3f-pathfinding
|
||||
- r3f-minimap
|
||||
- r3f-networking
|
||||
- r3f-ui
|
||||
- r3f-npc
|
||||
123
PROPOSED_STRUCTURE.md
Normal file
123
PROPOSED_STRUCTURE.md
Normal file
@@ -0,0 +1,123 @@
|
||||
Poposed top-level structure
|
||||
|
||||
|
||||
────────────────────────────
|
||||
|
||||
┌────────────────┬───────────────────────────────────────────────────────────────────┐
|
||||
│ New Top-Level │ Current Folders / Sub-categories that move inside │
|
||||
├────────────────┼───────────────────────────────────────────────────────────────────┤
|
||||
│ foundation/ │ core, data, random, timing, bitpacker, barcode, flow │
|
||||
│ persistence/ │ database, storage, cache │
|
||||
│ network/ │ network, drive, events │
|
||||
│ communication/ │ messaging, emails (+ webhook-client from the old platform bucket) │
|
||||
│ api/ │ api, gtap, vatdash │
|
||||
│ auth/ │ auth │
|
||||
│ security/ │ security, crypto │
|
||||
│ monitor/ │ monitor │
|
||||
│ media/ │ audio, audio-fx, audio-ir, image, fonts, video │
|
||||
│ react/ │ react, forms │
|
||||
│ r3f/ │ r3f │
|
||||
│ i18n/ │ i18n │
|
||||
│ geo/ │ geo │
|
||||
│ finance/ │ finance, payment │
|
||||
│ commerce/ │ booking, store, contacts, logistics │
|
||||
│ testing/ │ testing │
|
||||
│ tooling/ │ tooling, docs, docker │
|
||||
│ games/ │ games │
|
||||
└────────────────┴───────────────────────────────────────────────────────────────────┘
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Rationale for the groupings
|
||||
───────────────────────────
|
||||
|
||||
1. foundation/
|
||||
──────────────
|
||||
Everything that is runtime-agnostic, zero-dependency (or near-zero), and imported by higher layers. flow
|
||||
fits here because cbor, crdt, retry, streams, rules, queue, etc. are infrastructure primitives, not busin
|
||||
ess logic.
|
||||
|
||||
2. persistence/
|
||||
───────────────
|
||||
All passive data retention: database adapters, migration tools, key-value storage, S3/OPFS/File backends,
|
||||
and cache implementations. These share the concern of "write bytes somewhere, read them back later."
|
||||
|
||||
3. network/
|
||||
───────────
|
||||
The distributed-systems stack. drive is kept here because its value proposition is P2P mesh transport (We
|
||||
bRTC, HTTPS, chunk gossip, signed envelopes); it is not just storage. events is the pub/sub bus that unde
|
||||
rpins both drive and network.
|
||||
|
||||
4. communication/
|
||||
─────────────────
|
||||
User-facing or outbound message delivery: email, SMS, Slack/Discord/Telegram, DMARC, suppression lists, w
|
||||
ebhooks. This is "talk to humans or external systems."
|
||||
|
||||
5. api/
|
||||
───────
|
||||
The Hono-centric server platform and all api-plugin-* packages. gtap and vatdash are just thin bridge plu
|
||||
gins — they belong here.
|
||||
|
||||
6. auth/ & 7. security/
|
||||
───────────────────────
|
||||
Split intentionally. auth is identity, sessions, OAuth, WebAuthn, permissions. security is cryptography,
|
||||
vaults, certificates (x509, JWS), password tools, SRP, TPM. They are related but auth is a consumer of se
|
||||
curity, not the same domain.
|
||||
|
||||
8. monitor/
|
||||
───────────
|
||||
Observability triple: logging, OpenTelemetry SDK wiring, audit-logging. Small but distinct enough to avoi
|
||||
d polluting api or security.
|
||||
|
||||
9. media/
|
||||
─────────
|
||||
The entire audio graph (core, fx plugins, IR engine), plus image processing, video, and font pipelines. T
|
||||
hese are all "create, transform, or render sensory content."
|
||||
|
||||
10. react/ & 11. r3f/
|
||||
─────────────────────
|
||||
Kept separate because react is already ~25 packages and growing; r3f will be ~40+ game/rendering packages
|
||||
. Combining them would recreate the bloat you're trying to solve. forms lives under react because every f
|
||||
orm package is a React surface.
|
||||
|
||||
12. i18n/
|
||||
─────────
|
||||
Large enough to deserve its own top-level: core ICU parser, 5+ backend adapters, 7+ formatters, locale da
|
||||
ta, and React bindings.
|
||||
|
||||
13. geo/
|
||||
────────
|
||||
Astronomy, tides, solar system, star catalogs, and geospatial geometry. Self-contained domain.
|
||||
|
||||
14. finance/ & 15. commerce/
|
||||
────────────────────────────
|
||||
Split by layer. finance is the money stack: money, currency, all currency-provider-*, payment, all paymen
|
||||
t-provider-*. commerce is the business domain on top: bookings, store, inventory, subscriptions, contacts
|
||||
, logistics.
|
||||
|
||||
16. testing/
|
||||
────────────
|
||||
Testcontainers, E2E fixtures, DOM helpers, A/B testing. Cross-cutting but cohesive.
|
||||
|
||||
17. tooling/
|
||||
────────────
|
||||
CLI toolkit, installer, readline, docs generators (sitemap, SVG compress), Docker assets. "Things develop
|
||||
ers run, not things apps import at runtime."
|
||||
|
||||
18. games/
|
||||
──────────
|
||||
The massive upcoming game-engine stack: ECS, physics, combat, progression, quests, NPCs, building, vehicl
|
||||
es, and all their React/R3F UI layers. It deserves its own silo.
|
||||
|
||||
─────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
What this eliminates
|
||||
────────────────────
|
||||
• The platform/ catch-all (its contents are split into foundation, api, communication, and monitor).
|
||||
• The unknown/ bucket in STATUS.md — every package now has a logical home.
|
||||
• The commerce/ mega-category from STATUS.md — split into finance (money/payments) and commerce (business
|
||||
perations).
|
||||
|
||||
This gives you 18 folders at the packages/ root instead of 41, with each current category neatly nested a
|
||||
s a sub-folder. Scanning the repo becomes trivial: "Is it a base primitive? foundation/. Is it a UI compo
|
||||
nent? react/ or r3f/. Is it about money? finance/."
|
||||
45
README.md
Normal file
45
README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# SuavePlan
|
||||
|
||||
An extensive collection of libraries
|
||||
|
||||
## Companion docs
|
||||
|
||||
- [`AGENTS.md`](./AGENTS.md) — agent-execution contract (shared with all subagents).
|
||||
- [`CLAUDE.md`](./CLAUDE.md) — repo rules, conventions, and quality gates.
|
||||
- [`STATUS.md`](./STATUS.md) — migration dashboard (✅ migrated / – pending / ❌ unwritten).
|
||||
- [`SUMMARY.md`](./SUMMARY.md) — long-form per-package summaries.
|
||||
- [`MANIFEST.md`](./MANIFEST.md) — dependency graph + tier overlay (auto-generated).
|
||||
- [`MISSING.yml`](./MISSING.yml) — machine-readable list of unbuilt packages.
|
||||
- [`openspec/`](./openspec/) — capability specs + change proposals + archived changes.
|
||||
|
||||
## Apps
|
||||
|
||||
- [`apps/docs`](./apps/docs/README.md) — Docusaurus 3.10 documentation portal (this site).
|
||||
- [`apps/testbed`](./apps/testbed/README.md) — internal demo + integration testbed (React + Vite + Hono).
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Packages by category
|
||||
|
||||
466 `@suaveplan/*` packages across 18 categories. Each row links to the category's own README, which in turn lists every package and its first-paragraph description.
|
||||
|
||||
| Category | Packages |
|
||||
|---|---:|
|
||||
| [`api-stack`](./packages/api-stack/README.md) | 89 |
|
||||
| [`commerce`](./packages/commerce/README.md) | 5 |
|
||||
| [`communication`](./packages/communication/README.md) | 16 |
|
||||
| [`devtools`](./packages/devtools/README.md) | 16 |
|
||||
| [`foundation`](./packages/foundation/README.md) | 62 |
|
||||
| [`gaming`](./packages/gaming/README.md) | 3 |
|
||||
| [`geospatial`](./packages/geospatial/README.md) | 8 |
|
||||
| [`identity`](./packages/identity/README.md) | 7 |
|
||||
| [`localization`](./packages/localization/README.md) | 19 |
|
||||
| [`media`](./packages/media/README.md) | 75 |
|
||||
| [`money`](./packages/money/README.md) | 20 |
|
||||
| [`networking`](./packages/networking/README.md) | 65 |
|
||||
| [`observability`](./packages/observability/README.md) | 5 |
|
||||
| [`persistence`](./packages/persistence/README.md) | 23 |
|
||||
| [`security-stack`](./packages/security-stack/README.md) | 28 |
|
||||
| [`testkit`](./packages/testkit/README.md) | 13 |
|
||||
| [`threejs`](./packages/threejs/README.md) | 1 |
|
||||
| [`ui`](./packages/ui/README.md) | 11 |
|
||||
<!-- end:auto-docs -->
|
||||
153
STATUS-OPENSPEC-GAPS.md
Normal file
153
STATUS-OPENSPEC-GAPS.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# OpenSpec Coverage Map
|
||||
|
||||
_Generated 2026-05-14. Cross-references `packages/<category>/<pkg>/` against archived openspec proposals/tasks._
|
||||
|
||||
## Packages NOT processed through any archived openspec proposal
|
||||
|
||||
These shipped without going through OpenSpec governance — priority targets for retroactive spec authoring + quality review.
|
||||
|
||||
- `elevenlabs` (active proposal in flight)
|
||||
|
||||
## Packages without a same-named `openspec/specs/<pkg>/` entry but covered by a family proposal
|
||||
|
||||
| Package | Primary archived proposal |
|
||||
|---|---|
|
||||
| `ab-testing` | `2026-04-24-enterprise-ab-testing` |
|
||||
| `api-schemas` | `2026-04-19-enterprise-api-schemas` |
|
||||
| `audit-logging` | `2026-04-24-enterprise-audit-logging` |
|
||||
| `auth` | `2026-04-24-enterprise-auth` |
|
||||
| `barcode` | `2026-04-19-enterprise-barcode` |
|
||||
| `bitpacker-compressed` | `2026-05-02-bitpacker-modular-restructure` |
|
||||
| `bitpacker-delta` | `2026-05-02-bitpacker-modular-restructure` |
|
||||
| `bitpacker-game` | `2026-05-02-bitpacker-modular-restructure` |
|
||||
| `bitpacker-network` | `2026-05-02-bitpacker-modular-restructure` |
|
||||
| `bitpacker-schema` | `2026-05-02-bitpacker-modular-restructure` |
|
||||
| `bitpacker-strategies` | `2026-05-02-bitpacker-canonical-spec` |
|
||||
| `bitpacker-streaming` | `2026-05-02-bitpacker-modular-restructure` |
|
||||
| `bitpacker-validation` | `2026-05-02-bitpacker-canonical-spec` |
|
||||
| `cache` | `2026-04-24-enterprise-cache` |
|
||||
| `cli` | `2026-04-20-enterprise-cli` |
|
||||
| `compress-image` | `2026-04-19-enterprise-compress-image` |
|
||||
| `compress-svg` | `2026-04-25-enterprise-compress-svg` |
|
||||
| `crdt` | `2026-04-24-enterprise-crdt` |
|
||||
| `crypto-browser` | `2026-04-18-enterprise-crypto-family` |
|
||||
| `crypto-cli` | `2026-05-03-restore-crypto-cli-and-allowlist` |
|
||||
| `crypto-core` | `2026-05-03-barrel-explicit-exports` |
|
||||
| `crypto-sodium` | `2026-04-18-enterprise-crypto-family` |
|
||||
| `data-masking` | `2026-04-19-enterprise-data-masking` |
|
||||
| `db` | `2026-04-23-enterprise-db` |
|
||||
| `db-sqlite` | `2026-04-24-enterprise-db-sqlite` |
|
||||
| `dedup` | `2026-04-19-enterprise-dedup` |
|
||||
| `diff` | `2026-04-19-enterprise-diff` |
|
||||
| `dmarc` | `2026-04-19-enterprise-dmarc` |
|
||||
| `domain-normalizer` | `2026-04-19-enterprise-domain-normalizer` |
|
||||
| `drive-transport-https` | `2026-05-11-drive-transport-https-v1` |
|
||||
| `etl` | `2026-04-24-enterprise-etl` |
|
||||
| `event-sourcing` | `2026-04-24-enterprise-event-sourcing` |
|
||||
| `events-metrics` | `2026-04-24-enterprise-events-metrics` |
|
||||
| `events-otel` | `2026-04-24-enterprise-events-otel` |
|
||||
| `events-schema` | `2026-04-24-enterprise-events-schema` |
|
||||
| `events-transport` | `2026-04-24-enterprise-events-transport` |
|
||||
| `exif` | `2026-04-19-enterprise-exif` |
|
||||
| `feature-flags` | `2026-04-19-enterprise-feature-flags` |
|
||||
| `font-compress` | `2026-04-25-enterprise-font-compress-btf` |
|
||||
| `font-convert` | `2026-04-25-enterprise-font-convert` |
|
||||
| `font-subset` | `2026-04-25-enterprise-font-subset` |
|
||||
| `fsm` | `2026-04-19-enterprise-fsm` |
|
||||
| `geo` | `2026-04-19-enterprise-random-family` |
|
||||
| `geo-earth` | `2026-05-05-enterprise-universe-family` |
|
||||
| `geo-planet` | `2026-05-05-enterprise-universe-family` |
|
||||
| `job-scheduler` | `2026-05-12-enterprise-server-job-scheduler` |
|
||||
| `jwt-tenant` | `2026-04-24-enterprise-jwt-tenant` |
|
||||
| `logger` | `2026-04-18-enterprise-logger` |
|
||||
| `metadata` | `2026-04-19-enterprise-metadata` |
|
||||
| `migration` | `2026-04-24-enterprise-migration` |
|
||||
| `network-bootstrap` | `2026-05-11-network-bootstrap` |
|
||||
| `network-discovery-crdt` | `2026-05-11-network-discovery-crdt` |
|
||||
| `network-discovery-gundb` | `2026-05-11-network-discovery-gundb` |
|
||||
| `network-discovery-peerjs` | `2026-05-11-network-discovery-peerjs` |
|
||||
| `network-discovery-trystero` | `2026-05-11-network-discovery-trystero` |
|
||||
| `network-discovery-webtorrent` | `2026-05-11-network-discovery-webtorrent` |
|
||||
| `network-gundb-server` | `2026-05-12-network-gundb-server` |
|
||||
| `network-libp2p-server` | `2026-05-12-network-libp2p-server` |
|
||||
| `network-plugin-assignment` | `2026-05-12-network-suite` |
|
||||
| `network-plugin-audit-mesh` | `2026-05-05-network-plugin-audit-mesh` |
|
||||
| `network-plugin-auth` | `2026-05-12-network-suite` |
|
||||
| `network-plugin-circuit-breaker` | `2026-05-04-network-plugin-topology` |
|
||||
| `network-plugin-content-delivery` | `2026-05-05-network-plugin-content-delivery` |
|
||||
| `network-plugin-crypto-messages` | `2026-05-05-network-plugin-crypto-messages` |
|
||||
| `network-plugin-failover` | `2026-05-12-network-suite` |
|
||||
| `network-plugin-governance` | `2026-05-12-network-suite` |
|
||||
| `network-plugin-intra-relay` | `2026-05-05-network-plugin-intra-relay` |
|
||||
| `network-plugin-middleware` | `2026-05-04-network-plugin-middleware` |
|
||||
| `network-plugin-migration` | `2026-05-12-network-suite` |
|
||||
| `network-plugin-monitor` | `2026-05-04-network-plugin-monitor` |
|
||||
| `network-plugin-partition` | `2026-05-04-network-plugin-topology` |
|
||||
| `network-plugin-protocol` | `2026-05-02-bitpacker-modular-restructure` |
|
||||
| `network-plugin-qos` | `2026-05-12-network-suite` |
|
||||
| `network-plugin-registry` | `2026-05-02-bitpacker-modular-restructure` |
|
||||
| `network-plugin-relay` | `2026-05-04-drive-mesh-protocol-v1` |
|
||||
| `network-plugin-server-bridge` | `2026-05-10-network-plugin-server-bridge` |
|
||||
| `network-plugin-session` | `2026-05-05-network-plugin-session` |
|
||||
| `network-plugin-simulation` | `2026-05-05-network-plugin-simulation` |
|
||||
| `network-plugin-state` | `2026-05-04-network-plugin-state` |
|
||||
| `network-plugin-topology` | `2026-05-04-network-plugin-topology` |
|
||||
| `network-plugin-transport-manager` | `2026-05-04-network-plugin-transport-manager` |
|
||||
| `network-transport-hyperswarm` | `2026-05-12-network-transport-hyperswarm` |
|
||||
| `network-transport-libp2p` | `2026-05-12-network-transport-libp2p` |
|
||||
| `network-transport-standby` | `2026-05-12-network-transport-standby` |
|
||||
| `network-transport-webrtc` | `2026-05-11-network-transport-websockets-adapter` |
|
||||
| `network-transport-websockets` | `2026-05-11-network-transport-websockets-adapter` |
|
||||
| `network-trystero-server` | `2026-05-12-network-trystero-server` |
|
||||
| `noise` | `2026-04-19-enterprise-random-family` |
|
||||
| `oauth-client` | `2026-04-19-enterprise-oauth-client` |
|
||||
| `opentelemetry` | `2026-04-19-enterprise-opentelemetry` |
|
||||
| `password-formats` | `2026-04-19-enterprise-password-formats` |
|
||||
| `permissions` | `2026-04-24-enterprise-permissions` |
|
||||
| `pseudo-random` | `2026-04-19-enterprise-random-family` |
|
||||
| `qrcode` | `2026-04-19-enterprise-qrcode` |
|
||||
| `queue` | `2026-04-24-enterprise-queue` |
|
||||
| `r3f-networking` | `2026-05-12-r3f-networking` |
|
||||
| `random` | `2026-04-19-enterprise-random-family` |
|
||||
| `rate-limiter` | `2026-04-24-enterprise-rate-limiter` |
|
||||
| `react-forms` | `2026-05-05-enterprise-react-i18n` |
|
||||
| `react-network-libp2p-demo` | `2026-05-12-react-network-libp2p-demo` |
|
||||
| `react-ui` | `2026-05-10-react-ui-i18n-deferred-tooling` |
|
||||
| `readline` | `2026-04-20-enterprise-readline` |
|
||||
| `schema-registry` | `2026-04-19-enterprise-schema-registry` |
|
||||
| `search` | `2026-04-24-enterprise-search` |
|
||||
| `security-codes` | `2026-04-19-enterprise-security-codes` |
|
||||
| `serialize` | `2026-04-19-enterprise-serialize` |
|
||||
| `service-discovery` | `2026-05-11-service-discovery` |
|
||||
| `sitemap` | `2026-04-19-enterprise-sitemap` |
|
||||
| `solar-system` | `2026-05-05-enterprise-universe-family` |
|
||||
| `stars` | `2026-05-05-enterprise-universe-family` |
|
||||
| `storage-s3` | `2026-04-24-enterprise-storage-s3` |
|
||||
| `streams` | `2026-04-24-enterprise-streams` |
|
||||
| `suppression` | `2026-04-19-enterprise-suppression` |
|
||||
| `surrealdb` | `2026-04-24-enterprise-surrealdb` |
|
||||
| `testing-integration-step-ca` | `2026-05-02-testing-integration-step-ca` |
|
||||
| `typescript-config` | `2026-04-19-enterprise-typescript-config` |
|
||||
| `universe` | `2026-05-05-enterprise-universe-family` |
|
||||
| `voice` | `2026-05-02-voice` |
|
||||
| `webhook-client` | `2026-04-19-enterprise-webhook-client` |
|
||||
| `websocket-gateway` | `2026-05-12-websocket-gateway` |
|
||||
|
||||
## Orphan specs (no matching package directory)
|
||||
|
||||
Specs named differently from any package; mostly renames or capability-level groupings.
|
||||
|
||||
- database
|
||||
- drive-network-integration
|
||||
- logging
|
||||
- monorepo-structure
|
||||
- network-content-delivery
|
||||
- network-intra-relay
|
||||
- network-session
|
||||
- network-webrtc
|
||||
- noise-functions
|
||||
- random-utilities
|
||||
- react-ui-i18n
|
||||
- seeded-prng
|
||||
- serialization
|
||||
- statistical-distributions
|
||||
831
STATUS.md
Normal file
831
STATUS.md
Normal file
@@ -0,0 +1,831 @@
|
||||
# Status
|
||||
|
||||
> **Single source of truth** for every package planned, in-flight, or shipped on `genesis`. Edit by hand when status changes; regenerate from disk + `SUMMARY.md` whenever the auto-gen sources are refreshed (see [Maintenance](#maintenance) below).
|
||||
>
|
||||
> Companion docs:
|
||||
>
|
||||
> - [`SUMMARY.md`](./SUMMARY.md) — auto-generated package summaries (completed / pending / genesis-original) with full descriptions.
|
||||
> - [`MANIFEST.md`](./MANIFEST.md) — auto-generated dep graph + tier overlay (`bun scripts/folder-reorg/generate-manifest.ts`).
|
||||
> - [`MISSING.yml`](./MISSING.yml) — auto-generated machine-readable list of packages not yet on disk (`bun scripts/generate-missing.ts`).
|
||||
> - [`MIGRATION.md`](./MIGRATION.md) — Vaulted password-manager migration plan (future reference, not active OpenSpec work).
|
||||
|
||||
## Legend
|
||||
|
||||
| Symbol | Meaning |
|
||||
|---|---|
|
||||
| ✅ | **Migrated.** Source is on `genesis` and the package builds. Folder under `packages/` is populated. |
|
||||
| – | **Yet to be migrated.** Source exists in `../suaveplan/` (main branch) but has not been ported, **or** the genesis folder is a placeholder waiting on its OpenSpec change. |
|
||||
| ❌ | **Hasn't been written yet.** Net-new package — not in `suaveplan` and no genesis source. Typically scheduled in `MIGRATION.md`. |
|
||||
|
||||
## Counts
|
||||
|
||||
| Status | Count |
|
||||
|---|---:|
|
||||
| ✅ Migrated | 387 |
|
||||
| – Yet to be migrated | 152 |
|
||||
| ❌ Hasn't been written yet | 7 |
|
||||
| **Total tracked** | **501** |
|
||||
|
||||
## Packages
|
||||
|
||||
### `api`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/api` | ✅ | Hono orchestrator + middleware framework: SuaveApp composition-via-DI, AppBuilder fluent API with auto-wired tenant resolver, 6-state monotonic FSM, multi-source Zod config loader (file→env-file→env-vars), name-addressable MiddlewareChain (fail-fast insert), full SuaveplanError statusCode passthrough, configurable health routes, opt-in single RequestContext ALS, 5 API.* codes |
|
||||
| `@suaveplan/api-gateway` | – | API gateway with routing, load balancing, circuit br... |
|
||||
| `@suaveplan/api-manifest` | ✅ | Stage-1 contract package for the API plugin foundation. Composite Zod `pluginManifestSchema` (`.strict()` at top level) covers every documented `plugin.json` key from design §3 — identity / compat / isolation / permissions / dependsOn / routes / graphql / websockets / sse / sagas / twoPhaseCommit / database / jobs / events / secrets / config / scopes / telemetry / openapi / errors / uninstall. `parseManifest` (throw-form) and `parseManifestSafe` (discriminated `ParseResult`) over the same schema. Four cross-field validators that Zod alone can't express: `validateSagaOwnership`, `validatePermissionsWhenSandboxed`, `validateOpenApiRequiresGraphqlOrRoutes`, `validateErrorStatusMapShape` — each batch-mode, every offender surfaced in one pass. `resolveInstallOrder` is a deterministic Kahn topological sort with lexicographic tie-breaking; cycle path + missing-dep + version-mismatch surface stable error codes. `resolveCapabilities` reports required-missing / version-mismatch as errors and optional-missing as warnings. `hashManifest` returns `sha256:<hex>` over canonical JSON (deep key-sort, cycle/bigint/NaN-reject). `runManifestConformance` harness for downstream parsers. Eleven `MANIFEST.*` codes, every one production-path-reachable (spec-conformance-test enforced). 109 tests, 100/100/100/100 coverage. Deps: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `zod`. |
|
||||
| `@suaveplan/api-migrations` | ✅ | Dialect-agnostic DB migration runner: MigrationAdapter per dialect (SurrealQL/Postgres/SQLite shipped, MySQL slot open), real SHA-256 checksum validation, AbortSignal cancellation, dry-run, OTel emitters, 6 MIGRATION.* codes |
|
||||
| `@suaveplan/api-openapi` | ✅ | OpenAPI 3.1 registry + RouteBuilder with end-to-end Zod inference, widened StandardResponses (400-504 incl. 422/429/503/504), instance-only SchemaRegistry (no global singleton), atomic Hono/registry dual-surface registration via definePluginRoutes, 3 OPENAPI.* codes |
|
||||
| `@suaveplan/api-router-worker-bridge` | ✅ | Stage-3 HTTP marshaller across the worker boundary: `marshalRequest`/`rehydrateRequest` + `marshalResponse`/`rehydrateResponse` (tagged-union body: `none`/`inline`/`stream`), default header deny-list (`authorization`/`cookie`/`set-cookie`/`x-csrf-token`/`proxy-authorization`), 64 KiB inline cap + 16 MiB hard cap, Zod envelope schemas validated both sides; `ReadableStream<Uint8Array>` transport over `api-worker-rpc` `stream-chunk`/`stream-end`/`stream-ack` frames with window=16 backpressure; `createMainBridge(port)` / `createWorkerBridge(port, handler)` over a single `MessagePort` with multiplex by correlation id; worker-side `SuaveplanError` round-trips via `errorToEnvelope`; `createInProcessBridge` + `runBridgeConformance` harness; 4 `ROUTER_BRIDGE.*` codes (`MARSHAL_FAILED`, `BODY_TOO_LARGE`, `STREAM_INTERRUPTED`, `RESPONSE_INVALID`); no raw `Request` reference ever crosses the wire; 72 tests / 100/100/100/100 coverage. |
|
||||
| `@suaveplan/api-worker-rpc` | ✅ | Stage-3 root: MessagePort RPC protocol — Zod-validated `WorkerRpcMessage` discriminated frames (request/response/cancel/progress/stream-*/hello/goodbye), CorrelationIdGenerator + pendingRegistry, `createWorkerRpcClient<T>(port)` Proxy + `serveWorkerRpc(port, handlers)`, streaming via AsyncIterable + backpressure policy, AbortSignal forwarding via cancel frames, SuaveplanError round-trip via ErrorEnvelope codec, cloneable-guard, 10 `WORKER_RPC.*` codes, `runWorkerRpcConformance` harness. 122 tests / 100/100/100/100. |
|
||||
| `@suaveplan/api-worker-host` | ✅ | Stage-3 worker-side runtime hosting exactly one plugin: `bootstrapWorkerPlugin` validates manifest + capability handles via Zod, builds frozen RPC-backed capability proxy table over the capability port, mounts `lifecycle.dispatch`/`host.manifest`/`host.state` on the control port, and posts `worker-ready`; `createLifecycleReceiver` runs the nine-state worker FSM with `errorToEnvelope`-reconstructed `LifecycleAck`; `installCrashDetector` wires `uncaughtException`/`unhandledRejection` → `WORKER_HOST.CRASHED`; 9 `WORKER_HOST.*` codes; `runWorkerHostConformance` in-process simulator. 55 tests / 100/100/100/100. |
|
||||
| `@suaveplan/api-logger-worker-proxy` | ✅ | Stage-3 worker → main logger proxy: `createLoggerProxyClient(port, plugin)` returns `LoggerLike` with plugin pre-tagging + OTel `TraceProvider` + injectable `ClientClock`; `createLoggerProxyServer(port, logger, options?)` with redaction passthrough, level narrowing, child-logger cache, observation hooks. `LogBatcher` with size/idle/error-flush + bounded outstanding-batch budget. `ControlChannelSender`/`mountControlChannelReceiver` for `disable`/`enable`. 5 `LOGGER_PROXY.*` codes; `runLoggerProxyConformance` harness. 66 tests / 100/100/100/100. |
|
||||
| `@suaveplan/api-jobs-worker-proxy` | ✅ | Stage-3 jobs proxy: bidirectional `MessagePort` bridge implementing `capabilities.JobRegistrationHandle`. Worker `registerCron`/`registerQueued` calls store closures in `HandlerRegistry` and marshal `registerJob` RPC to main; main-side validates against manifest whitelist then wires registration into host's real handle. Scheduler ticks dispatch `runJob` back to the worker; `runWithTimeout` enforces per-handler budgets. 5 `JOBS_PROXY.*` codes. 44 tests / 100/100/100/100. |
|
||||
| `@suaveplan/api-events-worker-proxy` | ✅ | Stage-3 worker-tier proxy over a main-process `EventBus`. `createEventBusProxyServer(port, bus)` mounts the bus over `@suaveplan/api-worker-rpc`; `createEventBusProxyClient(port)` returns `EventBus`-shaped surface. Subscriptions are long-lived streaming RPCs; Zod-validated payload codec + structured-clone safety probe reject non-cloneable payloads with `EVENTS_PROXY.UNSERIALISABLE_PAYLOAD`. Per-subscription `highWaterMark` backpressure. 4 `EVENTS_PROXY.*` codes. 77 tests / 100/100/100/100. |
|
||||
<<<<<<< HEAD
|
||||
| `@suaveplan/api-events-in-memory` | ✅ | Stage-5 in-memory `EventsBus` adapter for the API testing kit. `createInMemoryEventsBus(options?)` (alias `createInMemoryEventBus`) wraps a fresh `@suaveplan/events` `EventBus` with a per-topic recording layer: `publish`/`publishAsync`/`emit` append payload to history BEFORE dispatch so `bus.published(topic)` returns every payload since the last `reset()` (or construction) in publish order even when subscribers throw. Subscribers fire through the real production bus (FIFO delivery, `once`, `off`, dispose unsubscribe). `emit(topic, payload)` satisfies the structural `CapabilityEventBusLike` contract from `@suaveplan/api-types/capabilities` so the bus can be handed to plugins as their `events` capability directly. Strict-throw history overflow via cap-bounded `History` ring (`maxHistoryPerTopic` default 1000; positive-integer Zod boundary rejects `0`, negatives, fractions, and unknown keys via `.strict()`). 2 `EVENTS_IN_MEMORY.*` codes (`HISTORY_OVERFLOW` with `{topic, limit}` context, `INVALID_OPTIONS` carrying `z.ZodError` cause). `./testing` subpath ships `createInMemoryEventsBusHarness({maxHistoryPerTopic?})` returning `{bus, waitFor(predicate, {timeoutMs=1000, intervalMs=5}?), dispose}` with fast-path resolve when the predicate already holds and descriptive timeout messages. `dispose()` clears history AND subscribers. 6 subpath exports (`./bus`, `./adapter` alias, `./history`, `./options`, `./testing`, `./error-codes`). 43 tests across 5 files / 100/100/100/100 coverage. Deps: `@suaveplan/api-types`, `@suaveplan/events`, `@suaveplan/error`, `tslib`, `zod`. |
|
||||
=======
|
||||
| `@suaveplan/api-db-sqlite-memory` | ✅ | Stage-5 in-memory SQLite database adapter for the api-host testing kit. `createSqliteMemoryAdapter(opts?)` opens a real `bun:sqlite` `Database` (`:memory:` default or any filename for file-backed fixtures), applies `journal_mode=WAL` + `foreign_keys=ON` by default with full per-pragma override (`{ ...DEFAULT_PRAGMAS, ...opts.pragmas }`), and exposes the canonical `AdminDatabaseLike` surface — real `BEGIN IMMEDIATE`/`COMMIT`/`ROLLBACK`, real `prepare`/`finalize`, real FK enforcement (no shim). `createSqliteMemoryDb(opts?)` composes the adapter with `createTenantScopedSqliteWrapper` to return `{ admin, scoped(tenantId), close }`; the scoped surface mirrors `@suaveplan/api-db-adapter`'s rewriter (WHERE/AND injection on SELECT/UPDATE/DELETE preserving ORDER BY/LIMIT/GROUP BY/RETURNING tails, INSERT auto-inject under `autoInjectInserts: true` or rejection with `DB_SQLITE_MEMORY.TENANT_SCOPE_VIOLATION` under `false`, literal + bound-param tenant-mismatch detection, `$N` positional bind handling, multi-row INSERT/INSERT…SELECT/DDL/record-params all rejected). `registerSqliteMemoryEngine()` declaration-merges `"sqlite-memory"` into the api-db-adapter `DatabaseEngineRegistry` and installs an idempotent disposer-returning factory so `createDbAdapter({ engine: "sqlite-memory", url: ":memory:" })` resolves transparently from any Stage-4 caller. `runSqliteMemoryMigrations({ plugin, admin, migrationsDir, fs })` is a transactional lexicographic forward-only runner with `__plugin_migrations(plugin, version, applied_at)` bookkeeping, idempotent re-run, and `DB_SQLITE_MEMORY.MIGRATION_FAILED` with the offending file on `context.file`. `createInMemorySqliteHarness(opts?)` ships the convenience `beforeEach`/`afterEach` builder with idempotent `cleanup()`. 4-code `DB_SQLITE_MEMORY_*` namespace (`OPEN_FAILED`, `INVALID_OPTIONS`, `MIGRATION_FAILED`, `TENANT_SCOPE_VIOLATION`), each thrown by a real production path. 5 OTel instruments lazily resolved (`api.db.query_duration_ms`, `api.db.execute_duration_ms`, `api.db.errors_total`, `api.db.migrations_applied_total`, `api.db.migration_duration_ms`) — counter + histogram both asserted under an in-memory `MeterProvider`. 9 subpath exports. 73 tests across 10 files, 100/100/100/100 src coverage. Deps: `@suaveplan/api-db-adapter`, `@suaveplan/api-types`, `@suaveplan/error`, `@opentelemetry/api`, `tslib`, `zod`. |
|
||||
>>>>>>> feat/api-db-sqlite-memory
|
||||
| `@suaveplan/api-db-adapter` | ✅ | Stage-4 bridge from `@suaveplan/db` engine adapters to plugin `AdminDatabaseLike` / `TenantScopedDatabaseLike`. Open `DatabaseEngineRegistry` (host calls `registerEngine(name, factory)` per engine — postgres/sqlite/surrealdb declared in `@suaveplan/api-types`, third-party engines via declaration merging). `createDbAdapter({ engine, url, pool?, tenantColumn, autoInjectInserts, migrationsDir? })` returns `{ admin, scoped(tenantId), close }`; Zod-validated options throw `DB_ADAPTER.INVALID_OPTIONS`. `createTenantScopedWrapper(admin, tenantId, opts?)` rewrites SQL: `WHERE`/`AND tenantColumn = ?` on SELECT/UPDATE/DELETE (ORDER BY, LIMIT, RETURNING, GROUP BY tails preserved); INSERT auto-injects column when `autoInjectInserts: true` or rejects with `TENANT_SCOPE_VIOLATION` (also for conflicting literal/bound tenant value); DDL + multi-row INSERT + record-shaped params + named params all rejected at the boundary. `createUnsafeGate({ plugin, capabilities, admin })` exposes `db.unsafe.query/execute` gated by `db.unsafe-queries` capability declaration; permitted calls increment `plugin.unsafe_query_total{plugin}`; absent declaration throws `UNSAFE_NOT_PERMITTED`. `runMigrations({ plugin, admin, migrationsDir, fs })` applies lexicographic `*.sql` files inside engine transaction (when supported), records versions in `__plugin_migrations(plugin, version, applied_at)`, idempotent on re-run, `MIGRATION_FAILED` with cause + file context. `createDbAdapterWorkerProxy({ port, engine })` wires `@suaveplan/api-db-worker-proxy/worker-side` into the same surface for plugins running inside worker hosts (cursor streaming preserved). 5 `DB_ADAPTER.*` codes (`TENANT_SCOPE_VIOLATION`, `UNSAFE_NOT_PERMITTED`, `MIGRATION_FAILED`, `ENGINE_UNSUPPORTED`, `INVALID_OPTIONS`). 3 OTel instruments (`plugin.unsafe_query_total`, `api.db.migrations_applied_total`, `api.db.migration_duration_ms`) all lazily-resolved via `getMeter` for test-MeterProvider compatibility. 63 tests / 100/100/100/100. Deps: `@suaveplan/api-types`, `@suaveplan/api-db-worker-proxy`, `@suaveplan/db`, `@suaveplan/error`, `@opentelemetry/api`, `tslib`, `zod`. |
|
||||
| `@suaveplan/api-db-worker-proxy` | ✅ | Stage-3 worker → main DB adapter proxy. `createDbProxyServer(port, db)` mounts `AdminDatabaseLike` on a `MessagePort`; `createDbProxyClient(port, { engine })` returns proxy + streaming `cursor(sql, params, { signal? })`. Transactions auto-commit/auto-rollback; leaked `tx` handles throw `DB_PROXY.TRANSACTION_LOST`. Prepared statements survive multiple round-trips. Cursor rows stream as `stream-chunk` frames; consumer abort propagates. `wrapTenantScoped(adminProxy, tenantId)` is the worker-side type-level facade. 4 `DB_PROXY.*` codes. 72 tests / 100/100/100/100. |
|
||||
| `@suaveplan/api-router` | ✅ | Stage-1 contract package for the API plugin foundation. Abstract `Router` class (8 verbs incl. `all` with 2- and 3-arg overloads + `mount` prefix composition + `match` resolution + `collidesWith` + `use` middleware chain) with reference in-memory implementation; host-side `RouterRequest`/`RouterResponse` shapes + immutable `ResponseBuilder` (`ok`/`jsonResponse` shortcuts, ETag, Server-Timing, streaming toggle); `MapHeaders` case-insensitive store; Zod-validated `routeOptionsSchema` (`auth`, `summary`, `tags`, `request`/`responses` with structural Zod predicate accepting cross-realm schemas, `runtimeValidation`/`openapi` defaulting true, `streaming` defaulting false, `csrf`, `idempotent`, discriminated `rateLimit: {rpm} | {rps}`); onion-model `Middleware` + `composeMiddleware` with double-`next()` rejection; 11-step `PipelineStep` literal + `PLUGIN_INSERTION_STEP="post-validation"` + `assertPluginInsertionStep` guard; `RouteMatch` + frozen `createRouteMatch`; `BlueGreenSlot` interface + `InMemoryBlueGreenSlot` reference impl (atomic `flip` returning `DrainingHandle`, `acquire`/`release` pending bookkeeping, `dispose` race-with-AbortSignal on timeout); frozen 5-entry `ROUTER_CODES` (`ROUTE_COLLISION`, `INVALID_PREFIX`, `MIDDLEWARE_INSERTION_DENIED`, `SLOT_FLIP_FAILED`, `SLOT_DISPOSE_TIMEOUT`) — every code thrown by a production path + spec-conformance test enumerating reachability; `runRouterConformance(factory)` adapter harness + `makeNoopSpan`/`makeFixtureRequest` helpers. 9 subpath exports. 125 tests across 16 files, 100/100/100/100 coverage. Deps: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `tslib`, `zod`. |
|
||||
| `@suaveplan/api-router-in-memory` | ✅ | Stage-5 in-memory `Router` + `PipelineRouter` adapter. Single concrete `InMemoryRouter` class extends abstract `Router` from `@suaveplan/api-router` (inheriting verb registration, mount/prefix rewriting, collision detection, param parsing) AND exposes the pipeline facet (`registerPipelineRoute`, `unregisterPipelineRoute`, `unregisterPipelinePlugin`, `listPipelineRoutes`, `matchPipeline`) structurally compatible with `@suaveplan/api-host`'s `PipelineRouter`/`PipelineRoute` contracts. `PipelineRouteTable` Map-backed metadata store: cross-plugin collision throws `ROUTER_IN_MEMORY.ROUTE_COLLISION`; same-plugin re-registration overwrites (hot-reload path); URL-decoded `:param` extraction; trailing-slash + case normalisation. `InMemoryBlueGreenSlot` thin extension of `api-router`'s reference impl. Zod-validated constructor options throw `ROUTER_IN_MEMORY.INVALID_PREFIX` on invalid plugin label. `createInMemoryRouterHarness()` bundles a fresh router + pinned slot factory for downstream foundation/plugin tests. 50 tests across 7 files including BOTH conformance suites (`runRouterConformance` from `@suaveplan/api-router/testing` AND `runPipelineRouterConformance` from `@suaveplan/api-host/testing`); 100/100/100/100 coverage. Subpath exports: `./adapter`, `./blue-green-slot`, `./pipeline`, `./testing`, `./error-codes`. No HTTP listener, no socket — production-grade in-memory adapter, not a test double. Deps: `@suaveplan/api-host`, `@suaveplan/api-router`, `@suaveplan/api-types`, `@suaveplan/error`, `tslib`, `zod`. |
|
||||
| `@suaveplan/api-router-hono` | ✅ | Stage-4 concrete Hono `Router` adapter. `HonoRouter` subclasses the abstract `Router` and mirrors every verb registration (`get`/`post`/`put`/`patch`/`delete`/`head`/`options`/`all`) onto a private Hono instance; `getHonoApp()` exposes the underlying app for ops integrations; `createHonoRouter()` factory returns the abstract `Router` surface so `runRouterConformance` accepts it unchanged. `HonoBlueGreenSlot` implements `BlueGreenSlot` with O(1) atomic flip, pending-counter `acquire`/`release`, AbortController per slot, `dispose(timeoutMs)` racing a timer against drain settle and aborting on timeout (`ROUTER.SLOT_DISPOSE_TIMEOUT`), `flip()` without a shadow throws `ROUTER.SLOT_FLIP_FAILED`. `toHonoMiddleware`/`fromHonoMiddleware` bridge the genesis onion-model `Middleware` ↔ Hono `MiddlewareHandler`; bridge wraps Hono-side throws in `RouterHonoError` (`ROUTER_HONO.HONO_INTERNAL_ERROR`). 2-entry `ROUTER_HONO_CODES` (`HONO_INTERNAL_ERROR`, `INVALID_HONO_PEER`) — every code thrown by a production path. 50 tests across 5 files including full `runRouterConformance` invocation, 100/100/100/100 coverage. Deps: `@suaveplan/api-router`, `@suaveplan/api-types`, `@suaveplan/error`, `tslib`, `zod`; `hono` peer ≥4.0.0. |
|
||||
| `@suaveplan/api-plugin` | ✅ | Plugin-system foundation: definePlugin / PluginRegistry with 4-phase lifecycle, deterministic topo-sort, AbortSignal-enforced timeouts, type-safe ServiceRegistry via module augmentation, priority-ordered HookExecutor, 10 PLUGIN.* codes |
|
||||
| `@suaveplan/api-tenant` | ✅ | Multi-tenant resolution: canonical Tenant type, 6 resolver strategies + TenantCache (TTL + negative + request-coalesce), single c.get("tenant") key, fail-closed NullTenantContext, pluggable rate-limit store with LRU bound, opt-in ALS, tenantScopes plugin opt-out, 7 TENANT.* codes |
|
||||
<<<<<<< HEAD
|
||||
| `@suaveplan/api-tenant-resolver-jwt` | ✅ | Stage-4 tenant-resolver adapter: reads `Principal.tenantId` (or a configurable claim from `Principal.claims`) attached by an earlier `api-auth-*` chain step and projects it into a `TenantContext`; returns `null` (chain-advance sentinel) when the claim is absent, throws `TENANT_RESOLVER_JWT.PRINCIPAL_MISSING_TENANT_ID` only under strict-mode (`requirePrincipal: true`); Zod-validated options at construction time (`TENANT_RESOLVER_JWT.INVALID_OPTIONS`); injectable `PrincipalExtractor` (default reads `request.principal`); self-conformance harness covers id stability, return-shape contract, request immutability, concurrent fan-out; 31 tests, 100/100/100/100 coverage. Deps: `@suaveplan/api-types`, `@suaveplan/error`, `zod`. |
|
||||
=======
|
||||
| `@suaveplan/api-tenant-provider-memory` | ✅ | Stage-4 in-memory reference `TenantProvider` adapter. `createMemoryTenantProvider({ tenants? })` returns a `MemoryTenantProvider` (abstract contract + out-of-band `add`/`remove`/`clear` admin helpers) backed by an insertion-ordered `Map<TenantId, TenantRecord>` for deterministic `list()` output and O(1) `lookup`/`count`. Honours the contract's active-only-default filter (`includeSuspended` lifts to active+suspended; `archived` reachable only via `lookup`) and the canonical `encodeCursor`/`decodeCursor` codec from `@suaveplan/api-tenant-provider/pagination`, including `pageSize` clamp + resumable cursor offsets. Every record entering the store passes through `tenantRecordSchema` at the boundary — seed, `add()`, and options shape all validated. 3 `TENANT_PROVIDER_MEMORY.*` codes (`DUPLICATE_TENANT`, `TENANT_NOT_FOUND`, `INVALID_RECORD`), each thrown from at least one production path. Idempotent `dispose()` clears the store. Passes `runTenantProviderConformance({ label: "memory" })` verbatim. 25 tests, 100/100/100/100 coverage. Deps: `@suaveplan/api-tenant-provider`, `@suaveplan/api-types`, `@suaveplan/error`, `tslib`, `zod`. |
|
||||
>>>>>>> feat/api-tenant-provider-memory
|
||||
| `@suaveplan/api-plugin-admin` | – | Admin panel plugin |
|
||||
| `@suaveplan/api-plugin-analytics` | – | Product analytics, funnel/cohort analysis |
|
||||
| `@suaveplan/api-plugin-audit` | ✅ | Hono audit-trail plugin wrapping `@suaveplan/audit-logging`. Global middleware records `{method, path, status, duration_ms, response_size, request_id, principal_id, tenant_id}` per request with severity classification (info<400, warning<500, error otherwise). Configurable sampling (head-end RNG gate), exclude paths (health probes default), case-insensitive header redaction + exact query/body field redaction, optional JSON body capture. `c.get("audit")` exposes handler-level `log()` + `getLogger()` via Hono `ContextVariableMap` augmentation. `auditService` typed slot in `ServiceRegistry`. Sink failures never throw into the request — logged at warn + counted via `audit.sink_failures_total`. 4 `AUDIT_PLUGIN_*` codes; 4 OTel instruments (`requests_recorded_total{outcome,sampled}`, `requests_excluded_total`, `sink_failures_total`, `middleware_duration_ms`). 65 tests inc. 1000-request sampling integration in [400, 600] band. 100/100/100/100 coverage. |
|
||||
| `@suaveplan/api-plugin-auth` | ✅ | Better-Auth-first authentication plugin. Canonical 6-step `onInit` lifecycle (resolve services → apply schema via injected `MigrationRunner` → construct Better-Auth via injectable factory → register `authService` in `ServiceRegistry` → register routes via `definePluginRoutes` → install global session-resolver middleware that sets typed `c.get("user")` + `c.get("session")` via Hono `ContextVariableMap` augmentation). 9 OpenAPI routes (sign-up, sign-in, sign-out, session, forgot-password, reset-password, verify-email, oauth-start, oauth-callback) via `RouteBuilder` PLUS catch-all `app.all("/auth/*", auth.handler)` so Better-Auth's full router surface is reachable while OpenAPI documents the 9 canonical paths. 4 middleware factories: `sessionMiddleware`, `requireAuth`, `requireRole`, `requireEmailVerified`. `Principal` + `mapBetterAuthUserToPrincipal` (canonical genesis identity). 10 `AUTH.*` codes, every one thrown from a real call site. 5 OTel instruments (`sessions_resolved_total`, `session_resolve_duration_ms`, `role_check_total`, `signups_total`, `signins_total`). 122 tests including 5 real-SurrealDB-testcontainer integration tests (full sign-up → sign-in → /session → sign-out roundtrip; OpenAPI doc lists 9 routes; schema-apply idempotency). 100/100/100/100 coverage. |
|
||||
| `@suaveplan/api-plugin-campaigns` | ✅ | Hono campaigns/suppression/webhooks API plugin (18 routes) with OpenAPIHono boundaries, Surreal + in-memory repositories, local CampaignSuppressionStore (channel-scoped) |
|
||||
| `@suaveplan/api-plugin-cms` | ✅ | Content management plugin |
|
||||
| `@suaveplan/api-plugin-database` | ✅ | Database management plugin (Path A: wraps SurrealAdapter). 88 tests, 100% coverage. Shipped parallel session. |
|
||||
| `@suaveplan/api-plugin-event-bus` | ✅ | Genesis-native typed event-bus plugin wrapping `@suaveplan/events`. `createEventBusPlugin(options)` factory. Canonical 6-step `onInit`: resolve `app` + optional `openAPIRegistry` → construct `EventBus` (interceptors + bounded queue + idempotency) → register `eventBusService` slot → install `requestBusMiddleware` setting `c.set("eventBus", scopedHandle)` via `ContextVariableMap` → optionally mount 3 routes (`POST /event-bus/publish`, `GET /event-bus/subscriptions`, `DELETE /event-bus/subscriptions/:id`) gated by `requireAuth()`. Per-tenant scoping via `scopeTopic`/`unscopeTopic` auto-prefixing with `tenant:<id>:` from `c.get("tenant")?.id` (tenant id validated against stricter sub-regex to block prefix smuggling). Topic name validation `^[a-z0-9_\-:.]+$`. 8 `EVENT_BUS_*` codes; 5 OTel instruments (`published_total{outcome,topic_root}`, `dropped_total{policy}`, `duplicates_total`, `subscriptions_active`, `publish_duration_ms`). 135 tests inc. tenant-isolation + http-routes integration suites. 100/100/100/100 coverage. |
|
||||
| `@suaveplan/api-plugin-events` | ✅ | Events management plugin (social/calendar — RSVPs, meetups, ICS export — not yet shipped; distinct from `api-plugin-event-bus`) |
|
||||
| `@suaveplan/api-plugin-export` | ✅ | Data export plugin (CSV, JSON, XLSX) with streaming + RBAC scoping. 77 tests, 100% coverage. Shipped parallel session. |
|
||||
| `@suaveplan/api-plugin-file-upload` | ✅ | Multipart + chunked file uploads with validation. 6 routes under `basePath` (default `/uploads`): `POST /` single-shot (10 MiB default cap), `POST /initiate` + `PUT /:uploadId/chunks/:chunkIndex` + `POST /:uploadId/complete` chunked (1 GiB default cap, 5 MiB default chunk), `DELETE /:uploadId` abort, `GET /:uploadId` status. All `requireAuth()`-gated + scoped to `c.get("user")!.id` + tenant (foreign uploadId → 404 anti-enumeration). MIME validation via `@suaveplan/mime` with magic-bytes sniff cross-check (PNG/JPEG/GIF/WEBP/PDF/ZIP baseline) to detect Content-Type spoofing. Executable-MIME denylist on by default. SHA-256 hash verification on chunked complete; assembler + `verifyHash` helpers. Pluggable `UploadStorage` (`MemoryUploadStorage` + `FileSystemUploadStorage` with path-traversal hardening) + `UploadSessionStore` (memory default). `fileUploadService` typed slot. 11 `UPLOAD_*` codes. 4 counters + 1 histogram. Cleanup scheduler `unref()`'d, cleared on `onClose`. 154 tests inc. real-fs 1 MiB / 256 KiB chunked round-trip with simulated-crash resume + MIME spoof rejection + 200-run fast-check property. 100/100/100/100 coverage. |
|
||||
| `@suaveplan/api-plugin-graphql` | ✅ | GraphQL adapter with schema building. 60 tests, 100% coverage. Shipped parallel session. |
|
||||
| `@suaveplan/api-plugin-import` | ✅ | Data import plugin (CSV/JSON/XLSX) with progress tracking + transform pipeline. Wrapped onto genesis `PluginDescriptor` from `@suaveplan/api-plugin` (upstream's `definePlugin` shape rewritten). 3 format parsers (csv/json/xlsx), transform pipeline (map/filter/validate stages), in-memory `ImportStore` with per-job progress events, RFC 7807 problem+json errors via hand-rolled renderer (genesis `@suaveplan/error` doesn't ship `formatErrorAsProblemJson`). Hono routes auth-gated via `requireAuth()`; foreign-owner reads return `IMPORT_NOT_FOUND` (anti-enumeration). Route errors funnel through `app.onError` so handler returns stay strictly typed against OpenAPI response codes. Removed 7 `@deprecated` aliases + duplicate `import.service.ts` per `enterprise/api-plugin-import.md` §3. 158 tests across 13 files (csv 29, json 7, xlsx 7, transform 24, progress 8, store 8, service 34, problem-json 6, build-routes 14, plugin 11, conformance 4, otel 2, error-codes 4). 100/100/100/100 coverage (statements 593/593, branches 347/347, functions 104/104, lines 554/554). |
|
||||
| `@suaveplan/api-plugin-rate-limit` | ✅ | Hono rate-limit plugin wrapping `@suaveplan/rate-limiter`. `createRateLimitPlugin(options)` factory + standalone `rateLimit({service})` middleware. `RateLimitService` typed slot in `ServiceRegistry`: `check({ip, tenantId?, path, method})`, `reset(key)`, `config`. Default global `app.use("*", rateLimit(...))` (secure-by-default); per-route mount available. Hono-compatible path patterns (exact, `/*`, inline `*`, `:param`). 429 response: RFC 7807 Problem+JSON body + `Retry-After` header. Three header families on every response: `X-RateLimit-*` (default), RFC 9238 `RateLimit-*`, `both`. Tenant multiplier applied to numeric limit. `c.set("rateLimit", decision)` via `ContextVariableMap`. Pluggable `RateLimitStorage` (bundled `MemoryStorage` default; Redis caller-supplied). 5 codes (`INVALID_INPUT`, `SERVICE_NOT_AVAILABLE`, `LIMIT_EXCEEDED`, `STORAGE_FAILURE`, `INVALID_PATH_PATTERN`). 4 OTel instruments. 75 tests, 100/100/100/100. |
|
||||
| `@suaveplan/api-plugin-search` | ✅ | Full-text search plugin with relevance scoring, Typesense-backed via `@suaveplan/testing-integration-typesense`. 125 tests, 100% coverage. Shipped parallel session. |
|
||||
| `@suaveplan/api-plugin-site` | ✅ | Site management plugin |
|
||||
| `@suaveplan/api-plugin-social` | – | Social features plugin |
|
||||
| `@suaveplan/api-plugin-sse` | ✅ | Server-Sent Events plugin via native `ReadableStream<Uint8Array>` (Bun/Node/Workers/Deno portable). `createSsePlugin(options)` factory; `sseService` slot: `broadcast(channel, event)`, `subscribe({channel, lastEventId?, principalId?, signal?})` returning `SubscriptionHandle` with `AsyncIterableIterator<SseEvent>`, `close(channel?)`, `stats()`. Reads `Last-Event-ID` for resume; threads `AbortSignal` on client disconnect; per-connection heartbeat via injectable timer; backpressure via `pull`-driven `drainInto` with `queueHighWaterMark` + `overflowPolicy`. Bounded `RingBuffer` per channel with stale-cursor protection. Pluggable `Fanout` (in-process default; cross-node siblings later). `formatSseFrame`/`parseSseStream` wire-format pair shipped. 8 `SSE_*` codes incl. `SSE_UNAUTHENTICATED` via `requirePrincipal: true`. 4 counters + UpDownCounter + Histogram. 102 tests inc. real-fetch round-trip + heartbeat + `Last-Event-ID` reconnect, 100/100/100/100. |
|
||||
| `@suaveplan/api-plugin-tenant` | ✅ | Hono plugin exposing `@suaveplan/api-tenant`'s resolver chain as a first-class slot. `createTenantPlugin(options)` factory; canonical 6-step `onInit`: resolve host services → build 7-strategy resolver chain (header/subdomain/domain/path/jwt/query/cookie; `Query`+`Cookie` contributed locally since `api-tenant` didn't ship those) → register `tenantService` → install global middleware setting `c.set("tenant", ctx)` → optionally mount 5 CRUD routes when `store` configured → optional rate-limit middleware. `tenantService` slot: `resolveFrom`/`requireFrom`/`current`/`getById`/`listAll`/`createTenant`/`updateTenant`/`deleteTenant`/`getResolver`. Pluggable `TenantStore` + default `MemoryTenantStore` (slug-uniqueness, idempotent soft-delete). Admin RBAC via dynamic import of `@suaveplan/api-plugin-auth`'s `requireRole("admin")` (declared as optional peer; graceful degrade when missing). Optional `TenantCache` wrap with OTel cache-outcome instrumentation. Strict Zod schemas on every surface. 7 `TENANT_PLUGIN_*` codes; 4 OTel instruments. 128 tests inc. header-beats-query priority + NullTenantContext fallback + admin role enforcement. 100/100/100/100 coverage. |
|
||||
| `@suaveplan/api-plugin-user` | ✅ | User profile plugin on top of `api-plugin-auth`. 3 routes (`GET /users/me`, `PATCH /users/me`, `DELETE /users/me`) all gated by `requireAuth()` and scoped to `c.get("user")!.id`. `userService` registered in `ServiceRegistry` (`getById`, `requireById`, `getCurrent`, `updateProfile`) with module-augmentation typed slot. `USER_PROFILE_SCHEMA` DDL renderer + `applyUserProfileSchema` helper supporting both `migrationRunner` and `db` transports. `UserProfileSchema` extends `PrincipalSchema` from auth plugin; `UpdateUserProfileBodySchema` is `.pick(profileFields).partial().strict()` rejecting unknown fields; `buildUpdateBodySchemaFor` mints per-instance schemas. 7 `USER.*` error codes, every one thrown. 3 OTel counters (`profile_reads_total`, `profile_updates_total{outcome}`, `self_deletes_total`). `DELETE /users/me` uses `auth.$context.internalAdapter.deleteUser(userId)` for cascade. 101 tests including real-SurrealDB-testcontainer integration: sign-up via auth → `userService.getById` round-trip → `updateProfile`. 100/100/100/100 coverage. `dependencies: ["auth"]`. |
|
||||
| `@suaveplan/api-plugin-webhook` | ✅ | Outbound signed webhook delivery plugin wrapping `@suaveplan/webhook-client`. `createWebhookPlugin(options?)` Zod-validated PluginDescriptor with 5-step `onInit`. `WebhookService`: `send`/`sendAll`/`register`/`update`/`unregister`/`listEndpoints`/`getEndpoint`/`test`. 6 routes (`GET/POST /webhooks`, `GET/PATCH/DELETE /webhooks/:id`, `POST /webhooks/:id/test`) all `requireAuth()`-gated + tenant-scoped. HMAC-SHA256 signing; canonical headers (`X-Webhook-Id`, `X-Webhook-Event`, `X-Webhook-Timestamp`, `X-Webhook-Signature`). Per-delivery dedup via `@suaveplan/idempotency` (fallback in-process Map). Retry with exponential backoff + jitter (`maxAttempts` default 5); 4xx non-retryable; dead-letter sink on exhaustion. Pluggable `EndpointStore` + `DeadLetterSink` (memory defaults). 10 `WEBHOOK.*` codes. 4 OTel instruments (`deliveries_total`, `dead_letters_total`, `delivery_attempts`, `send_duration_ms`). 77 tests inc. real `node:http` receiver on random port asserting signed payload + 3-attempt retry + dead-letter. 100/100/100/100. |
|
||||
| `@suaveplan/api-auth` | ✅ | Stage-1 abstract session-resolver: `SessionResolver` + `SessionResolverChain` (ordered first-match-wins, fail-closed-on-throw, opt-in `continueOnThrow`), `SecuritySchemeContribution` (Zod-validated OAS-3.1 discriminated union), `CsrfPolicy.requiresCsrf` default, composite `AuthAdapter` (`securityScheme` now optional for anonymous topology), `AUTH_SESSION_CAPABILITY_ID` literal, 4-code `AUTH_CODES` registry, `runAuthSessionConformance` harness with null-only `seedPrincipal` mode for the anonymous tail. 55 tests / 100/100/100/100. |
|
||||
| `@suaveplan/api-auth-anonymous` | ✅ | Stage-4 trivial session resolver — `createAnonymousResolver(opts?)` always returns null `Principal` and `dispose()` is a no-op (idempotent). `ANONYMOUS_AUTH_ADAPTER` constant pairs the resolver with `csrf: false` and omits `securityScheme` (anonymous-only hosts emit OpenAPI with no `security` entry); validates via `validateAuthAdapter`. Strict zod options schema rejects non-object / unknown-key input with the single `AUTH_ANON.INVALID_OPTIONS` code; no per-request failure paths by design. Passes `runAuthSessionConformance` in null-only mode (null + dispose-idempotent scenarios). 18 tests / 100/100/100/100. Deps: `@suaveplan/api-auth`, `@suaveplan/api-types`, `@suaveplan/error`, `tslib`, `zod`. |
|
||||
| `@suaveplan/api-auth-static-principal` | ✅ | Stage-5 fixed-`Principal` session resolver — `createStaticPrincipalResolver(opts)` returns the configured `Principal` verbatim on every request (no token decoding, no I/O, no state). `createStaticPrincipalAdapter(opts)` bundles resolver + omitted `securityScheme` (anonymous-style topology validated by `validateAuthAdapter`) + pinned `csrf: false`. `applyTo(req)` predicate + `nullForPaths` (exact or `/foo/*` suffix-glob) drive selective null-return without rewiring the chain. Defensive clone-and-freeze per `resolve()` so callers cannot mutate one returned Principal and observe the change. Strict Zod boundary with split `AUTH_STATIC_PRINCIPAL.{INVALID_OPTIONS, INVALID_PRINCIPAL}` codes — `principal` issues map to `INVALID_PRINCIPAL`, everything else to `INVALID_OPTIONS`. Passes `runAuthSessionConformance` (`static-principal-direct`) in seeded-principal mode for the recognised scenario and via `nullForPaths` for the null scenario. 43 tests / 100/100/100/100. Stage-5 `api-host/testing` adapter-table registration deferred until that surface lands (currently `STAGE_2_RESERVED`); documented in README. Deps: `@suaveplan/api-auth`, `@suaveplan/api-types`, `@suaveplan/error`, `tslib`, `zod`. |
|
||||
| `@suaveplan/api-auth-provider` | ✅ | Stage-1 abstract account-lifecycle contract (design §2.3). `AuthProviderAdapter<Caps>` with configurable `pathPrefix`, four-hook lifecycle. 9-entry `PROVIDER_ROUTE_SURFACE` (register/login/logout/refresh/oauth.start/oauth.callback/verify-email/forgot-password/reset-password). `PathOwnership` registry + `detectAuthPathCollisions` segment-aware containment. 4 `AUTH_PROVIDER.*` codes. `runAuthProviderConformance` harness. 79 tests / 100/100/100/100. |
|
||||
<<<<<<< HEAD
|
||||
| `@suaveplan/api-auth-bearer-static` | ✅ | Stage-4 service-to-service static-token `SessionResolver` adapter: matches `Authorization: Bearer <token>` against a fixed `token → Principal` table via constant-time compare from `@suaveplan/crypto-utils`; unknown tokens defer to next chain entry. Optional per-entry mutual-TLS gate (`mtlsRequired`) rejects when host did not surface `request.tls.peerCertificate`. Configurable `headerName`/`prefix` (case-insensitive scheme match per RFC 7235). Ships both direct surface (`createBearerStaticResolver`) and worker-proxy surface (`createBearerStaticResolverWorkerProxy` + `serveBearerStaticResolverOnPort`) over `@suaveplan/api-worker-rpc`; both pass `runAuthSessionConformance`. OAS-3.1 HTTP-Bearer `securitySchemeContribution`, 4-code `AUTH_BEARER_STATIC.*` registry (only `INVALID_OPTIONS` thrown; rest surfaced via OTel outcome label), `api.auth.bearer_static_match_total{outcome}` counter. 67 tests / 100/100/100/100. |
|
||||
=======
|
||||
| `@suaveplan/api-auth-jwt` | ✅ | Stage-4 concrete JWT `SessionResolver` for the API plugin foundation. `createJwtSessionResolver(opts)` verifies a Bearer JWT (or cookie token) via `@suaveplan/jws` against either a remote JWKS endpoint (`JwksCache`: positive TTL, 30s negative cache, in-flight dedup, `kid`-miss refresh, stale-on-failure) or a single static JWK; enforces `iss`/`aud`/`exp`/`nbf` with injectable `clock` + `clockToleranceSec`. `defaultClaimsToPrincipal` maps `sub`/`scope`+`scopes`/`tenant_id`+`tid`; overridable via `options.claimsToPrincipal`. `JWT_BEARER_SCHEME` OpenAPI 3.1 `http`/`bearer`/`bearerFormat: "JWT"` contribution (renameable). Per-request soft failures collapse to `null` (chain-friendly); infrastructure failures raise the 3-code `AUTH_JWT.*` registry (`JWKS_FETCH_FAILED`/`INVALID_KEY_FORMAT`/`INVALID_CLAIMS_MAPPER`). Conformance harness `runAuthSessionConformance` passes against the resolver under `jwt-static-key`. 92 tests including real-`node:http` JWKS server with RS256 sign+verify round-trip via `@suaveplan/crypto-rsa`. 100/100/100/100 coverage. |
|
||||
>>>>>>> feat/api-auth-jwt
|
||||
| `@suaveplan/api-schemas` | ✅ | Shared Zod schemas describing Suaveplan API request/response shapes — single so… |
|
||||
| `@suaveplan/api-secrets-env` | ✅ | Stage-4 `SecretsLike` adapter backed by `process.env` / `Bun.env` with per-plugin allowed-keys enforcement. `createEnvSecrets({ allowedKeys, source?, prefix? })` returns a frozen-shaped instance: Zod-validated options (`SECRETS_ENV.INVALID_OPTIONS` on bad input); allow-list checked BEFORE source lookup (`SECRETS_ENV.KEY_NOT_PERMITTED` never reads the snapshot); `get()` resolves `undefined` for permitted-but-absent, `require()` throws `SECRETS_ENV.KEY_NOT_FOUND`; `list()` hands out the frozen `allowedKeys` so the surface cannot enumerate names outside the manifest. Injectable `source` so tests never mutate the real env; default reads `process.env` overlaid with `Bun.env` per call (no constructor cache so rotations are picked up immediately). Optional `prefix` namespaces lookups (`prefix: "BILLING_"` → `get("API_KEY")` reads `BILLING_API_KEY`). Companion `buildEnvSecretDescriptors` / `descriptorAllowedKeys` derive both the manifest `secrets[]` and the adapter's `allowedKeys` from one declaration. Secret VALUES never appear in logs/errors — only NAMES on `error.context.name`. Passes `runSecretsConformance` (`@suaveplan/api-secrets/testing`) via `isNotFound` mapped to `SECRETS_ENV.KEY_NOT_FOUND`. 3 `SECRETS_ENV.*` codes; 31 tests / 100/100/100/100 coverage. Deps: `@suaveplan/api-secrets`, `@suaveplan/api-types`, `@suaveplan/error`, `tslib`, `zod`. |
|
||||
| `@suaveplan/api-ws-broker-in-memory` | ✅ | Stage-5 in-process single-host `WsBroker` implementation of the `@suaveplan/api-ws` contract. `createInMemoryWsBroker(options?)` returns an `InMemoryWsBroker<Payload>` (abstract contract + `subscriberCount(topic)` + `isDisposed()` introspection). Per-topic `Map<string, Set<RegistryEntry>>` registry; `publish(topic, envelope)` snapshots the subscriber set before dispatch so a handler that unsubscribes mid-fan-out does not skip siblings; per-subscriber failures are individually caught so one bad handler never poisons the broadcast. `deliveryMode: "fire-and-forget"` (default) dispatches without awaiting; `"await-all"` `Promise.all`-awaits every handler. Idempotent `dispose()` clears the registry and flips a `disposed` flag — subsequent `publish`/`subscribe` throw `WS_BROKER_IN_MEMORY.BROKER_DISPOSED`; second `dispose()` is a no-op. Strict-mode Zod options gate throws `WS_BROKER_IN_MEMORY.INVALID_OPTIONS` on unknown keys or invalid enum values. `./testing` subpath ships `createInMemoryBrokerHarness(opts?)` with a captured-envelope observer, `publishOne(payload, opts?)` convenience, `extraTopics` probe-registration, and idempotent teardown. 34 tests / 100/100/100/100 (istanbul); zero direct `vitest`/`bun:test` imports (uses `@suaveplan/testing/runner`). Deps: `@suaveplan/api-ws`, `@suaveplan/api-types`, `@suaveplan/error`, `tslib`, `zod`. |
|
||||
| `@suaveplan/api-shared` | – | Shared API utilities and types |
|
||||
| `@suaveplan/api-loader` | ✅ | Stage-1 abstract module resolver + RollbackJournal. `ModuleResolver<M>` types-only contract, `RollbackJournal` ring-buffer (depth 3) + reference `createRollbackJournal`, `bustSpecifier(url, version)` WHATWG-URL cache-bust, persisted Zod schema (sha256 + ISO datetime), `NativeBindingDetector` + `assertNoNativeBindings`, 4-code `LOADER_CODES`, conformance harnesses. 8 subpaths, 75 tests / 100/100/100/100. |
|
||||
| `@suaveplan/api-graphql` | ✅ | Stage-1 abstract GraphQL contract: `createGraphqlRegistry`, `mergeSdl` federation with collision detection + `@graphql.namespace` wrapper, minimal SDL parser, `@auth`/`@rateLimit` directive descriptors, `GraphqlEngine` lifecycle, `graphql-ws` subscription protocol Zod schema, `runGraphqlEngineConformance` harness, 7 `GRAPHQL.*` codes. Zero engine deps. 82 tests / 100/100/100/100. |
|
||||
| `@suaveplan/api-host` | ✅ | Stage-2 keystone for the API plugin foundation. Subpaths: `./error-codes` (44 codes via `defineCodes` across `HOST_*`/`PLUGIN_*`/`RELOAD_*`/`CAPABILITY_*`/`ADMIN_*`/`LEDGER_*` namespaces, including the Chunk-D per-request additions `TENANT_NOT_RESOLVED`/`SESSION_REQUIRED`/`CSRF_TOKEN_INVALID`/`RBAC_DENIED`/`RATE_LIMIT_EXCEEDED`/`REQUEST_VALIDATION_FAILED`/`RESPONSE_VALIDATION_FAILED`/`IDEMPOTENCY_REPLAY`/`ROUTE_NOT_FOUND`/`HANDLER_THREW`/`CLIENT_ABORTED`), `HOST_DEFAULT_STATUS_MAP`, `HostError`; `./define-plugin` author DX (`definePlugin<Caps>` literal-array `requires` threaded into `PluginContext<Caps>` + `RequestContext<Caps>`); `./capability-registry` runtime with semver/scope/duplicate validation + reverse-registration-order disposers; `./lifecycle` `LifecycleDriver` driving every §4 transition with per-plugin mutex + `crashed`/auto-restart + `startForReload` rollback hand-off; `./hot-reload` `ReloadDriver` blue-green primitive (§8.1) + `FileWatcher` debounce + persisted `RollbackJournal` (depth 3) + native-binding guard + 503-window mode; `./plugin-ledger` `PluginLedger` over `bun:sqlite` with forward-only idempotent `HostMigrationRunner`, `asLifecycleLedger()` + `asRollbackPersistence()` adapter views; `./admin-api` Hono sub-router exposing every §8.4 endpoint with `host:admin` scope, per-principal token-bucket rate-limit (60/min reads, 1/min destructive), `Idempotency-Key` enforcement, audit envelope; `./error-boundary` wraps every pipeline step and projects `SuaveplanError → statusMap` / `AbortError → 499` / unknown → 500 + cause-chain log + `api.errors_total`; `./request-pipeline` implements the §6 11-step driver exactly (tenant → session → CSRF → RBAC → rate-limit → idempotency → request-validate → handler → response-validate → audit → telemetry) with Zod request/response gates, `enforce`/`warn` strictness, streaming bypass, full §12.3 counter + histogram emission; `./host` `createHost(config)` composing every internal driver with Zod-validated config, topo-sorted `start()`/reverse-topo `stop(graceMs)`, `host.fetch(request)` driving the pipeline, `/internal/livez` + `/internal/readyz` probes, runtime `installPlugin`/`uninstallPlugin`; `./otel` `HostInstruments` covering every §12.3 counter + histogram; `./testing` populated in Stage 5 with `createTestHost(config?)` — composes the in-memory router/auth/tenant/db/secrets/ws-broker/events/ledger adapters into a running `TestHost` (extends `Host` with `installFixturePlugin`/`fetchAs`/`waitForEvent`/`dispose`), seeds `principal`/`tenants`/`secrets` through override-beats-default slots, exposes `STAGE_2_RESERVED=false` for Stage-2 detection back-compat, alongside the existing `runPipelineRouterConformance` harness; Stage-5 smoke (`e2e/foundation-stack-test-host`) drives every wired slot via Bun.serve + 8 Playwright groups. 287 tests, 100% line coverage on every Chunk-D module + every other src/ module, full pre-publish gate (lint + typecheck + test + build) green. |
|
||||
| `@suaveplan/api-types` | ✅ | Types-only contract package for the API plugin foundation — `Principal`/`TenantId` brands, the `PluginManifest` type-level view, `PluginContext`/`RequestContext` with capability-driven `db` split (admin vs tenant-scoped), 25-member alphabetised `CapabilityId` union + `CapabilityMap`/`RequestCapabilityMap` lookup, `CapabilitySlice<Caps>` context narrowing, 13-state `PluginState` lifecycle union, `ErrorCode` template literal `PLUGIN.<UPPER>.<UPPER>`, `Router`/`Middleware`/`RouteHandler`/`RouteOptions`/`RouterSlot`/`BlueGreenSlot`, `AuthAdapter`/`AuthProviderAdapter`/`SessionResolver`/`SecuritySchemeContribution`, `TenantResolver`/`TenantResolverChain`/`TenantProvider`, `SecretsLike`/`SecretDescriptor`, `AdminDatabaseLike`/`TenantScopedDatabaseLike` with open-extensible `DatabaseEngineRegistry`, `GraphqlRegistry`/`Resolver`/`SubscriptionResolver`/`LoaderFactory`, `WsConnection`/`WsConnectionRegistry`/`SubscriptionBus`/`WsBroker`, `SseConnection`/`SseBroker`, `SagaParticipant`/`SagaInstance`/`SagaStep`/`SagaState`, `TwoPcParticipant`/`TwoPcVote`/`TwoPcTransaction`, `WorkerRpcMessage` discriminated union + `WorkerProxyOf<T>` structural transform with `NotCloneable<T>` marker, `Counter`/`Histogram`/`Gauge`/`InstrumentRegistry`, `RollbackJournalEntry`/`RollbackJournal` ring-buffer interface. 20 subpath exports, zero runtime, zero deps; byte-size + dep-graph + 11 `expectTypeOf` conformance suites pinning every cross-cutting invariant. |
|
||||
|
||||
### `audio`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/audio-controllers` | ✅ | Universal-tier MIDI hardware controller plugins. Ships `KontrolS88Plugin` (88-key keyboard with scale modes, arp, pitch-bend, mod-wheel, RGB key LEDs), `MaschinePlugin` (16-pad grid + per-pad step rows + slot-based sample loader), `TraktorS4Plugin` (4-deck DJ controller with mixer, crossfader, per-channel volume). Shared `MidiControllerPlugin` base — injectable `MidiAccessAdapter` seam (zero `navigator` references), `@suaveplan/fsm`-driven `disconnected/connecting/connected/error` connection state with hot-plug recovery, declarative `CCMapping` DSL with Zod boundary validation, learn mode, batched LED output. Transport-synced `Sequencer` and `Arpeggiator` (sample-accurate via `Transport.scheduleRepeat`, offline-renderable, zero `setInterval`). `bindParameter` / `bindPluginPreset` helpers for `@suaveplan/audio-fx-plugin-base` integration with linear/log/exponential scale mapping + optional smoothing curve. 6 `AUDIO_CONTROLLERS.*` codes (`ACCESS_DENIED`, `INVALID_OPTIONS`, `INVALID_STATE`, `DEVICE_ERROR`, `NOT_CONNECTED`, `UNKNOWN_PARAMETER`). OTel tracer + 4 instruments (`audio_controller_midi_messages_total`, `audio_controller_sequencer_steps_total`, `audio_controller_midi_latency_ms`, `audio_controller_sequencer_jitter_ms`) no-op without exporter, no import-time globals. 104 tests, 100/100/100/100 coverage. |
|
||||
| `@suaveplan/audio-core` | ✅ | Universal audio data model and processing primitives: paged AudioDocument, non-destructive PlanEngine, block-level ProcessPipeline, and typed OpRegistry |
|
||||
| `@suaveplan/audio-decode-wav` | ✅ | Genesis-original universal WAV decoder wrapping `@audio/decode-wav` with uniform `AudioDecoder` contract, `AUDIO_DECODE.WAV.*` codes, OpenTelemetry, and chunked-stream + whole-buffer decode paths. |
|
||||
| `@suaveplan/audio-engine-browser` | ✅ | Browser-tier `TimelineAdapter` implementation: Web Audio rendering, AudioWorklet hosting, and `AudioFxPlugin` `createAudioNode()` integration for the universal `TimelineEngine`. |
|
||||
| `@suaveplan/audio-engine-server` | ✅ | Server-tier `TimelineAdapter` implementation: file source/sink adapters and offline render driver for the universal `TimelineEngine`. |
|
||||
| `@suaveplan/audio-ir-core` | ✅ | Genesis-original foundation for the IR family: `ImpulseResponse`, `IrManifest`/`IrManifestEntry` Zod schemas, `IrRegistry` with collision detection, `MutableUriPluginRegistrar` + `createUriPluginCollector`, `AUDIO_IR.*` codes, OpenTelemetry. |
|
||||
| `@suaveplan/audio-ir-loader` | ✅ | Genesis-original IR pipeline: registry → resolver → decoder → manifest validation → frozen `ImpulseResponse`. LRU cache, bounded preload, `AUDIO_IR_LOADER.*` codes, OpenTelemetry. |
|
||||
| `@suaveplan/audio-ir-room-plugin-club` | ✅ | Genesis-original IR content package: ships two bundled WAV room captures (`club1.wav`, `club2.wav`) via the `bundle://` URI scheme. Canonical example of the bundle-only content pattern. |
|
||||
| `@suaveplan/audio-ir-room-plugin-pool` | ✅ | Genesis-original IR content package: ships one bundled WAV (`outdoor-swimming-pool.wav`) paired with a CDN fallback via `fallback://?u=<bundle>&u=<https>`. Canonical example of the bundle + fallback pattern. |
|
||||
| `@suaveplan/audio-fx-plugin-audio-analyzer` | ✅ | Tap-point FFT analyser plugin (member of audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-audio-player` | ✅ | Playback source plugin (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-auto-filter` | ✅ | LFO-driven filter plugin (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-base` | ✅ | Foundation for the audio-fx-plugin-*amily: AudioFxPlugin contract, PluginChain, ChainContext, AmbientState, BusRegistry, PluginRegistry, ChainSnapshot, ChainEventBus, ParameterSchedule, PresetSnapshot, biquad DSP math, `AUDIO_FX.*` error-code registry. Universal-tier; zero @suaveplan/audio-fx-plugin-* deps. |
|
||||
| `@suaveplan/audio-fx-plugin-chorus` | ✅ | Modulated comb / multi-voice chorus plugin (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-compressor` | ✅ | Feed-forward compressor with sidechain input (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-delay` | ✅ | Stereo delay with ping-pong + BPM sync (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-distortion` | ✅ | Waveshaping + soft/hard clip plugin (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-eq` | ✅ | Parametric / shelving / high-pass / low-pass 8-band equaliser, RBJ Audio EQ Cookbook biquads (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-filter` | ✅ | Multi-mode filter (biquad / IIR / state-variable) (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-flanger` | ✅ | Short-delay modulated comb plugin (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-game-audio` | ✅ | Game-audio orchestration plugin (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-gate` | ✅ | Noise gate with hold + linked envelope (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-glados` | ✅ | Pitch-tracked vocal-style effect plugin (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-limiter` | ✅ | Look-ahead brick-wall limiter plugin (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-microphone` | ✅ | Source plugin simulating mic colour (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-mixer` | ✅ | Bus mixer with per-channel sends (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-phaser` | ✅ | All-pass-bank phaser plugin (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-pitch-shifter` | ✅ | Phase-vocoder pitch-shifter plugin (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-reverb` | ✅ | Engine-pluggable reverb (FDN / Schroeder / convolution) (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-sampler` | ✅ | Sample-playback source plugin (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-spatial` | ✅ | HRTF-based spatial panner plugin (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-synthesizer` | ✅ | Polyphonic subtractive synth plugin (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-tremolo` | ✅ | Amplitude-modulated tremolo plugin (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-fx-plugin-widener` | ✅ | Stereo width / mid-side processor plugin (audio-fx-plugin-* family). |
|
||||
| `@suaveplan/audio-metadata` | ✅ | Universal audio metadata extractor: parses ID3v1/v2, MP4/M4A atoms, FLAC Vorbis… |
|
||||
| `@suaveplan/audio-sprite-packer` | ✅ | Universal audio sprite packer with injectable adapter, Zod-validated boundaries, and OpenTelemetry instrumentation. Zero browser or Node.js dependencies. |
|
||||
| `@suaveplan/audio-timeline` | ✅ | Headless DAW timeline engine with multi-track arrangements, clips, playback, loop regions, selection, zoom, per-track mixer state, undo/redo, and a pluggable TimelineAdapter. |
|
||||
| `@suaveplan/audio-transport` | ✅ | Universal musical clock and transport: sample-accurate scheduling with PPQ, BPM automation, swing, time signatures, and loop regions. Zero browser or Node dependencies. |
|
||||
| `@suaveplan/dmx` | – | DMX512 lighting control engine |
|
||||
| `@suaveplan/python-audio-fingerprint` | – | High-accuracy audio fingerprinting and matching service |
|
||||
| `@suaveplan/r3f-audio` | – | Positional 3D spatial audio (HRTF, culling) |
|
||||
| `@suaveplan/r3f-dmx` | – | DMX512 lighting control visualization |
|
||||
| `@suaveplan/react-audio` | ✅ | React surface for the unified audio architecture: `AudioProvider` (strict-mode-safe lifecycle over `BrowserTimelineAdapter` + `PluginRegistry` + `BusRegistry`), 14 hooks via `useSyncExternalStore` for concurrent-safe + SSR-consistent subscription, 17 components (`<PluginSlot>` generic renderer + 12 composed-plugin UIs + `<MidiControllerSetup>` learn-mode), 2 zustand stores, full keyboard + ARIA a11y, Storybook coverage gate enforcing default + interactive + edge-case stories. `REACT_AUDIO.*` error-code namespace, per-component subpath exports. Shipped across 4 sequential changes (`enterprise-react-audio-{foundation,components,components-extras,midi}`). |
|
||||
| `@suaveplan/react-audio-fingerprint` | – | Audio fingerprinting components |
|
||||
| `@suaveplan/react-audio-player` | – | Modular audio player with 3D viz |
|
||||
| `@suaveplan/react-audio-stream` | – | Audio streaming components |
|
||||
| `@suaveplan/voice` | ✅ | Provider-agnostic voice core: abstract interfaces, shared types, Zod schemas, a… |
|
||||
|
||||
### `auth`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/auth` | ✅ | Provider-agnostic authentication: JWT lifecycle, API key management, session ma… |
|
||||
| `@suaveplan/better-auth-surrealdb` | ✅ | Better-Auth Adapter for SurrealDB v3. Two factories: v1.4 `createBetterAuthSurrealAdapter` returns the 10-method `CustomAdapter` directly; v1.6 `createBetterAuthSurrealAdapterV16` wraps it via `createAdapterFactory` from `@better-auth/core/db` to satisfy v1.6's `DBAdapterInstance` factory shape (re-invoking the factory inside the transaction callback to thread Better-Auth's full transform pipeline). Bundles canonical 17-table `BETTER_AUTH_SURREAL_SCHEMA` (user/session/account/verification/passkey/jwks/sso_provider/oauth_application/oauth_access_token/oauth_consent/organization/member/invitation/team/team_member/two_factor/api_key + 13 indexes) extracted byte-for-byte from upstream `02-better-auth.surql`, plus `applyBetterAuthSchema(adapter)` helper. `DEFAULT_BETTER_AUTH_LINK_FIELDS` auto-prefixes string userIds → `record<user>` for 9 link tables. RecordId construction routes through `createRequire(import.meta.url)("surrealdb")` to dodge the CJS/ESM class-identity bug in the driver's CBOR encoder. 172/172 tests including real-SurrealDB-testcontainer `signUpEmail` end-to-end roundtrip. 19-requirement spec (14 v1.4 + 5 v1.6). |
|
||||
| `@suaveplan/jwt-tenant` | ✅ | JWT-based multi-tenant token issuance and verification with Ed25519/ECDSA signi… |
|
||||
| `@suaveplan/macos-keychain` | ✅ | macOS Keychain bridge via the `security` CLI: shell-injection-safe `Bun.spawn… |
|
||||
| `@suaveplan/oauth-client` | ✅ | Universal OAuth 2.0 / OIDC client: Authorization Code + PKCE, Client Credential… |
|
||||
| `@suaveplan/permissions` | ✅ | Full RBAC/ABAC permissions engine: Ability/AbilityBuilder, RoleManager, PolicyE… |
|
||||
| `@suaveplan/react-auth` | ✅ | Authentication components |
|
||||
| `@suaveplan/react-auth-whitelabel` | ✅ | White-label auth with tenant branding |
|
||||
| `@suaveplan/react-permissions` | – | RBAC/ABAC permission components |
|
||||
| `@suaveplan/webauthn` | ✅ | W3C WebAuthn L3 client: `register` / `authenticate` over injectable `WebAuthnAdapter`, full authenticator-data parser with flags decoder, all 7 attestation-format verifiers (`packed` self+full, `fido-u2f`, `tpm` via `@suaveplan/tpm-struct`, `android-key`, `android-safetynet` via `@suaveplan/jws`, `apple`, `none`) under pluggable registry, COSE_Key parse via `@suaveplan/cbor/cose`, signature verify dispatching to `@suaveplan/crypto-curve` + `@suaveplan/crypto-rsa`, X.509 chain validation via `@suaveplan/x509`, sign-counter regression detection (W3C §6.1.1), PRF + largeBlob extension passthrough, conditional UI, DOMException → typed error mapping with 6 DOM-name → typed-error edges, 17-code error registry, constant-time challenge + rpIdHash compare, 5 OTel instruments. 64 impl + 27 test + 24 sibling `.md` + 8 fixture + 3 benchmark files; 317+ tests; 100% coverage. Side-effects: `decodeOne` re-exported from `@suaveplan/cbor` root barrel (1 line) + `__fixtures__/` namespace anchor + subpath export added to `@suaveplan/x509` (both packages keep 100% coverage). |
|
||||
|
||||
### `barcode`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/barcode` | ✅ | Barcode generation and parsing for common 1D symbologies: Code 128, Code 39, EA… |
|
||||
| `@suaveplan/qrcode` | ✅ | QR code generation producing SVG, pixel matrix, terminal (Unicode blocks), and… |
|
||||
|
||||
### `bitpacker`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/bitpacker` | ✅ | Core bit-level binary packing: BitWriter, BitReader, BitPacker, strategy manager. Hardened: Zod boundary, `./register-schemas`, OTel instruments + spans on pack/unpack, mitata baseline. |
|
||||
| `@suaveplan/bitpacker-compressed` | ✅ | CompressedBitPacker + RLE/dictionary adapters and registry. Hardened: Zod boundary on adapter + config, `./register-schemas`, OTel instruments. |
|
||||
| `@suaveplan/bitpacker-crypto` | ✅ | Bit-packer-aware crypto primitives — SignedBitSchema, EncryptedBitSchema, EnvelopeSchema, MultiSignedBitSchema, HashChainSchema, schemaFingerprint, crc32/adler32. Skipped from Phase 3 OTel (already instrumented upstream). |
|
||||
| `@suaveplan/bitpacker-delta` | ✅ | DeltaManager, calculateStringDiff/applyStringDiff, numberXor — change detection and diff/patch encoding. |
|
||||
| `@suaveplan/bitpacker-game` | ✅ | GameStateManager, vector quantization, PositionPredictor / RotationPredictor (Kalman + 6 polynomial methods), GameInterpolator (9 methods), StateReconciliation. Hardened: Zod boundary on the 6 option bundles, `./register-schemas`, OTel instruments. |
|
||||
| `@suaveplan/bitpacker-network` | ✅ | NetworkSimulator (latency, jitter, packet loss, burstiness). Hardened: Zod boundary with 0–100 percentage bounds + structural Rng/Clock injection, `./register-schemas`, OTel instruments. |
|
||||
| `@suaveplan/bitpacker-schema` | ✅ | Schema registry, validation, and versioning for @suaveplan/bitpacker. |
|
||||
| `@suaveplan/bitpacker-strategies` | ✅ | 22 built-in packing strategies. Hardened: Zod boundary on registerDefaultStrategies, `./register-schemas`, OTel span on bootstrap, fast-check properties on 9 kinds, `_version` forward tolerance via `_tolerance: "strict"\|"forward"` + `_allowedVersions`. |
|
||||
| `@suaveplan/bitpacker-streaming` | ✅ | packToChunks, unpackFromChunks, FramedPack/UnpackStream — Web Streams + length-prefix framing. Hardened: Zod boundary on 9 option bundles + FramingConfig, `./register-schemas`, OTel instruments. |
|
||||
| `@suaveplan/bitpacker-validation` | ✅ | SafetyLimits, SafetyChecker, RecursionGuard, ValidationReport. Hardened: Zod boundary on safety + validator options, `./register-schemas`, OTel instruments. |
|
||||
|
||||
### `commerce`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/api-plugin-asset-register` | – | Asset register — fixed assets, depreciation, mainten... |
|
||||
| `@suaveplan/api-plugin-bank` | – | Game bank with virtual currency + interest |
|
||||
| `@suaveplan/api-plugin-banking` | – | Banking/payment plugin |
|
||||
| `@suaveplan/api-plugin-payment` | ✅ | Payment intent lifecycle plugin wrapping `@suaveplan/payment` + provider siblings (`payment-provider-{stripe,adyen,paypal,square}`). `createPaymentPlugin(options)` factory — caller injects `PaymentProvider` (no default). 7 OpenAPI routes all `requireAuth()`-gated + scoped to `c.get("user").id` with 404 anti-enumeration: `POST/GET /payments/intents`, `GET /payments/intents/:id`, `POST /payments/intents/:id/{confirm,capture,cancel,refund}`. Webhook receiver `POST /payments/webhook` mounted as separate sub-router (NOT through `OpenAPIRegistry` so generated SDKs don't expose it) that reads raw body BEFORE Zod parsing for HMAC verification (SHA-256/384/512, 5-min replay window default). `Idempotency-Key` middleware wired to `POST /intents` + `POST .../refund` via `@suaveplan/idempotency` (`MemoryIdempotencyStore` default). Pluggable `PaymentIntentStore` (in-memory default) maps provider IDs back to `(ownerId, tenantId?)`. `FakePaymentProvider implements PaymentProvider` for tests (real interface, in-memory backing). 15 `PAYMENT_PLUGIN_*` codes; 5 counters + 1 histogram (all `provider`-labeled). BigInt amounts serialized as decimal strings. `app.onError` translator converts `PaymentPluginError` → JSON. 153 tests inc. full createIntent → confirm → capture → refund lifecycle + idempotency replay + webhook signature verify + cross-user isolation. 100/100/100/100 coverage. |
|
||||
| `@suaveplan/booking` | ✅ | Universal-tier JSON-configurable booking engine with pluggable strategies — `BookingEngine<TBookableMeta, TBookingMeta>` pipeline (availability → conflict → pricing → rules → state machine → events), in-package `Hooks<TMap>` transform pipeline, configurable `StateMachine` for status transitions, typed `EventBus` for 15 lifecycle events, six domain presets (seats / accommodation / appointments / resources / activities / transport), four built-in conflict detectors (exact_match / range_overlap / slot_capacity / provider_overlap), three availability strategies (exactMatch / rangeOverlap / capacity), eight built-in rule-condition factories, rule evaluator with 11 operators, pricing calculator + seasonal + dynamic + tiered + modifier pipeline with three rounding modes, Zod schemas for bookable / booking / config validation, temporal helpers (windows / recurrence / slots), and typed `BookingError extends SuaveplanError<BookingErrorCode>` with 7-code registry (`ENGINE_NOT_CONFIGURED`, `BOOKING_NOT_FOUND`, `BOOKING_CONFLICT`, `TRANSITION_NOT_ALLOWED`, `UNKNOWN_CONFLICT_MODE`, `UNKNOWN_AVAILABILITY_STRATEGY`, `HOOK_RETURNED_PROMISE_IN_SYNC`) and optional `bookingId` / `holdId` / `mode` / `strategy` discriminants. 29 subpath exports, 150 tests at 100/100/100/100 coverage. Deps: `@suaveplan/error`, `zod`. |
|
||||
| `@suaveplan/currency` | ✅ | Universal-tier currency conversion engine — pluggable `CurrencyProvider` interface, TTL-bounded `RateCache` with direct / inverse / triangulated lookup, primary + fallback chain with `AbortSignal` cancellation, Zod-validated public boundary, typed `CurrencyError` hierarchy with 7 stable `CURRENCY.*` codes, virtual-currency seeding with post-construction `setVirtualRate` / `updateVirtualRate` mutators, enriched `ConversionResult` carrying `providerName` and `triangulatedThrough` diagnostics. 6 subpath exports, 107 tests at 100/100/100/100 coverage. Built on `@suaveplan/money`. |
|
||||
| `@suaveplan/currency-provider-binance` | ✅ | Universal-tier Binance sibling of the `@suaveplan/currency-providers` slim core (follow-up wave, 2026-05-17). Ships `BinanceProvider implements CurrencyProvider` (`name = "binance"`, `supportsCrypto = true`, `supportsFiat = false`) plus the stable `BINANCE_PROVIDER_NAME` / `BINANCE_DEFAULT_BASE_URL` constants. No API key required — fetches the public Binance spot ticker (`GET /api/v3/ticker/price`) through the slim core's shared `httpRequest()` helper, suffix-decodes pairs (e.g. `BTCUSDT` → `BTC` when base is `USDT`) into the converter's "1 base = N units of asset" form via `1 / price`, sanitises zero / negative / non-finite prices, defends against the `USDTUSDT`-style suffix-equals-symbol edge case, and exposes typed `CurrencyProviderError` for every failure: `HTTP_ERROR` (non-2xx via `httpRequest`), `PARSE_ERROR` (non-JSON body, with `cause` set only when the parser threw an `Error`), `UPSTREAM_ERROR` (payload shape is not `Array<{ symbol: string; price: string }>` — non-array, null row, wrong-typed row, missing field), `UNKNOWN_CURRENCY` (requested `options.targets` entry absent from upstream listing, with the offending symbol surfaced in `upstreamMessage`). Constructor takes optional `NoKeyConfig & { timeoutMs?: number }` — injectable `FetchLike` for tests / proxies / edge runtimes, custom `baseUrl` for regional mirrors or staging, optional `timeoutMs` that flows through to `httpRequest` and composes with the caller's `AbortSignal`. 1 subpath export (`./binance/binance`), 21 tests at 100/100/100/100 coverage. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-kraken` | ✅ | Universal-tier Kraken sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Ships `KrakenProvider implements CurrencyProvider` (`name = "kraken"`, `supportsCrypto = true`, `supportsFiat = false`) wrapping Kraken's free, no-API-key `/0/public/Ticker?pair=<csv>` endpoint — accepts the slim core's shared `NoKeyConfig` (optional `baseUrl` defaulting to `https://api.kraken.com/0/public`, optional injectable `FetchLike` defaulting to `globalThis.fetch`) plus an optional `extraSymbols` map that merges on top of the canonical 12-symbol table at construction time (single-round-trip batch fetch via the comma-separated `pair=` query; bidirectional `KRAKEN_SYMBOL_MAP` / `KRAKEN_PAIR_TO_SYMBOL` frozen maps normalise Kraken's legacy `X<base>Z<quote>` asset-prefix names — `XXBTZUSD`, `XETHZUSD`, `XDGUSD`, `XLTCZUSD`, `XXRPZUSD` — and the flat `<base>USD` newer listings — `SOLUSD`, `USDTZUSD`, `DOTUSD`, `ADAUSD`, `LINKUSD`, `AVAXUSD`, `ATOMUSD`). Routes every request through the shared `httpRequest()` helper from the slim core so HTTP failures + caller `RateFetchOptions.signal` propagation come for free, reads the `c[0]` last-trade-closed price for each mapped pair and inverts it (`rates[symbol] = 1 / price`) into the converter's `1 USD = N crypto` convention, silently skips entries whose price doesn't parse to a positive finite number (Kraken occasionally returns `"0.00"` for freshly-listed pairs), filters the rate map by `options.targets`, defaults `options.base` to `"USD"`. Typed error mapping: `HTTP_ERROR` (non-2xx via `httpRequest`, carries `status`), `UPSTREAM_ERROR` (non-empty `data.error[]` array, carries first message on `upstreamMessage`; falls back to `"unknown upstream error"` when the array contains a `null`), `PARSE_ERROR` (malformed JSON or shape-violating body — non-object, non-array `error`, missing or non-object `result`, JSON `null`), `UNKNOWN_CURRENCY` (caller-supplied `targets` array is non-empty but no requested symbol resolves to a mapped Kraken pair). 2 subpath exports (`./symbols/symbols`, `./provider/provider`), 34 tests at 100/100/100/100 coverage. Universal-tier, `sideEffects: false`. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-cryptocompare` | ✅ | Universal-tier CryptoCompare sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Ships `CryptoCompareProvider implements CurrencyProvider` (`name = "cryptocompare"`, `supportsCrypto = true`, `supportsFiat = false`) with truly-optional `apiKey` config — free public tier works fully keyless, supplying `apiKey` attaches the `authorization: Apikey <key>` header on every outbound request (free → keyed is a single-field config change; whitespace-only strings are treated as absent so `process.env.CRYPTOCOMPARE_API_KEY` can be passed directly without a guard). Routes every request through the shared `httpRequest()` helper from the slim core (abort-signal composition + per-call timeouts + typed `HTTP_ERROR` come for free), calls `GET <baseUrl>/pricemulti?fsyms=<targets-or-default-15>&tsyms=<base>`, inverts upstream "1 crypto = X fiat" prices into the converter's "1 base = X crypto" convention, skips non-finite/non-positive quotes, defaults base to `"USD"`, and raises typed `CurrencyProviderError` on every failure path (`HTTP_ERROR` via `httpRequest`, `PARSE_ERROR` for invalid JSON or non-object payload, `UPSTREAM_ERROR` when the payload carries `Response: "Error"` (with `upstreamMessage` populated from `Message` or a default), `UNKNOWN_CURRENCY` when no requested symbol produces a positive quote). 15 default symbols when `options.targets` is omitted or empty (BTC, ETH, SOL, USDT, USDC, BNB, XRP, ADA, DOGE, DOT, AVAX, LINK, UNI, ATOM, LTC). 1 subpath export (`./cryptocompare/cryptocompare`), 15 tests at 100/100/100/100 coverage covering both keyless and keyed branches. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-coingecko` | ✅ | Universal-tier CoinGecko sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Ships `CoinGeckoProvider implements CurrencyProvider` (`name = "coingecko"`, `supportsCrypto = true`, `supportsFiat = true`) with truly-optional `apiKey` config — free public tier needs no key; supplying `apiKey` attaches the `x-cg-pro-api-key` header on every outbound request (free → Pro is a single-field config change). Routes every request through the shared `httpRequest()` helper from the slim core (abort-signal composition + per-call timeouts + typed `HTTP_ERROR` come for free), proactively gates `options.targets` against the curated `COIN_IDS` map (16 cryptos: BTC, ETH, SOL, USDT, USDC, BNB, XRP, ADA, DOGE, DOT, AVAX, MATIC, LINK, UNI, ATOM, LTC) so unknown targets reject with `UNKNOWN_CURRENCY` before any network round trip, inverts upstream "1 crypto = X fiat" prices into the converter's "1 base = X crypto" convention, and raises typed `CurrencyProviderError` on every failure path (`HTTP_ERROR` via `httpRequest`, `PARSE_ERROR` for invalid JSON, `UPSTREAM_ERROR` for wrong response shape, `UNKNOWN_CURRENCY` for unmapped targets). Also exposes `getCoinGeckoId(symbol)` for case-insensitive symbol lookup. 2 subpath exports (`./coingecko/coingecko`, `./coin-ids/coin-ids`), 23 tests at 100/100/100/100 coverage covering both keyless and Pro-key branches. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-open-exchange-rates` | ✅ | Universal-tier Open Exchange Rates sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Ships `OpenExchangeRatesProvider implements CurrencyProvider` (`name = "open-exchange-rates"`, `supportsFiat = true`, `supportsCrypto = false`) wrapping `/latest.json?app_id=<key>&base=<base>`. API-key (`app_id`) required — empty string raises `MISSING_CONFIG` synchronously from the constructor. Free-tier safe by default: non-USD `RateFetchOptions.base` raises `UNSUPPORTED_BASE` before any network round trip; paid plans opt in via `OpenExchangeRatesProviderConfig.allowNonUsdBase: true`. Routes through the slim-core `httpRequest()` helper so non-2xx surfaces typed `HTTP_ERROR` with `providerName` / `status`, and the caller `RateFetchOptions.signal` is forwarded. Validates the upstream `{ base, rates, timestamp }` shape — every malformed branch (non-object body, non-string `base`, non-number `timestamp`, non-object `rates`, invalid JSON, error payload missing `status` / `message`) raises typed `PARSE_ERROR`. Detects the OXR 2xx error-payload shape (`{ error: true, status, message, description? }`) and raises `UPSTREAM_ERROR` with `upstreamMessage` populated. Filters the rates map to `options.targets`; missing codes raise `UNKNOWN_CURRENCY` with the offending symbol in the message. Converts the upstream Unix-seconds timestamp to milliseconds. `apiKey` is `encodeURIComponent`-wrapped to prevent injection. 1 subpath export (`./provider/provider`), 22 tests at 100/100/100/100 coverage covering every error path including non-Error rejection from `Response.json()`. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-coinmarketcap` | ✅ | Universal-tier CoinMarketCap sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Ships `CoinMarketCapProvider implements CurrencyProvider` (`name = "coinmarketcap"`, `supportsCrypto = true`, `supportsFiat = false`) constructed from `ApiKeyConfig` (`apiKey`, optional `baseUrl` defaulting to `https://pro-api.coinmarketcap.com/v1`, optional injectable `FetchLike`); empty / whitespace `apiKey` raises `CurrencyProviderError(MISSING_CONFIG)` at construction time so misconfiguration surfaces synchronously instead of on first fetch. Routes every request through the shared `httpRequest()` helper from the slim core (abort-signal composition + per-call timeouts + typed `HTTP_ERROR` come for free), calls `GET <baseUrl>/cryptocurrency/quotes/latest?symbol=<csv>&convert=<base>` with the `X-CMC_PRO_API_KEY` + `Accept: application/json` headers, forwards `options.signal`, defaults `base` to `USD` and `targets` to the top-10 crypto-by-market-cap (`BTC,ETH,SOL,USDT,USDC,BNB,XRP,ADA,DOGE,DOT`), inverts each returned price (`1 / price`) into the converter's expected `1 fiat = X crypto` form, and silently drops non-finite / non-positive / wrong-base quotes before checking for empty result. Typed error mapping: `HTTP_ERROR` (non-2xx, via `httpRequest`), `PARSE_ERROR` (invalid JSON / non-object body / missing or non-object `data` field with no usable upstream signal), `UPSTREAM_ERROR` (`status.error_code` populated and non-zero, carries `upstreamMessage` when `error_message` is a string; treats `error_code === 0` as success), `UNKNOWN_CURRENCY` (no requested symbol resolved to a positive quote), `MISSING_CONFIG` (constructor); every error carries `providerName: "coinmarketcap"`. 2 subpath exports (`./coinmarketcap/coinmarketcap`), 22 tests at 100/100/100/100 coverage. Universal-tier, `sideEffects: false`. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-fixer` | ✅ | Universal-tier [Fixer.io](https://fixer.io/) sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Ships `FixerProvider implements CurrencyProvider` (`name = "fixer"`, `supportsFiat = true`, `supportsCrypto = false`) — API-key required (free tier restricts `base` to `EUR`, paid plans unlock arbitrary base + higher quotas; 170+ fiat currencies). Constructor takes the slim core's shared `ApiKeyConfig` and raises `CURRENCY_PROVIDER.MISSING_CONFIG` synchronously when `apiKey === ""` so a missing `process.env.FIXER_KEY` fails before burning a single request against the monthly quota. Routes every request through the shared `httpRequest()` helper from the slim core (abort-signal composition + per-call timeouts + typed `HTTP_ERROR` come for free), builds `?access_key=…&base=…[&symbols=…]` (default `base = "EUR"`, `targets` comma-joined into `symbols` when non-empty), and maps the upstream `{ success, error: { code, info } }` envelope into typed `CurrencyProviderError` codes: code `105` (`base_currency_access_restricted`, free-tier base lock) → `UNSUPPORTED_BASE`, codes `201` (`invalid_base_currency`) / `202` (`invalid_currency_codes`) → `UNKNOWN_CURRENCY`, any other `success: false` (or missing `error` field) → `UPSTREAM_ERROR` carrying the upstream `error.info` (or `"<unknown>"`) on `upstreamMessage`. Distinguishes parse failures (malformed JSON, non-object JSON, success envelope missing `base`/`rates`/`timestamp`) under a single `PARSE_ERROR` code. Forwards caller-supplied `RateFetchOptions.signal` to the underlying fetch via `httpRequest` (upstream skeleton did not). Injectable `FetchLike` from `@suaveplan/types` so tests pass a recording closure instead of `vi.mock`. No top-level side effect — `sideEffects: false` honoured; the package never registers an instance with any global registry at import time. 1 subpath export (`./fixer/fixer`), 22 tests across one file at 100/100/100/100 coverage. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-virtual` | ✅ | Universal-tier in-memory programmable virtual / game currency sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Ships `VirtualCurrencyProvider implements CurrencyProvider` (`name = "virtual-currency"`, `supportsCrypto = false`, `supportsFiat = false`, `supportsVirtual = true`) backed by a nested `Map<from, Map<to, number>>` rate table — zero HTTP, zero `FetchLike`, zero API key, zero `@suaveplan/money` `registerCurrency` coupling (kept `sideEffects: false` honest, unlike the upstream form). Programmable runtime API: `setRate(from, to, rate)` (upsert — replaces an existing pair without throwing), `removeRate(from, to): boolean` (idempotent, auto-cleans empty inner maps so `listCurrencies()` stays accurate, never throws), `clear()`, `hasRate(from, to)`, `getRate(from, to): number \| undefined`, `listCurrencies(): readonly string[]` (lexicographic snapshot). `fetchRates(options?)` returns the directed rates stored under `options.base` (default `defaultBase`, `"USD"`), optionally filtered by an `options.targets` inclusion list; `RateFetchResult.timestamp` is sourced from an injectable `Clock` (`@suaveplan/types`, default `() => Date.now()`) so `FakeClock`-driven tests get deterministic timestamps without monkey-patching `Date`. Triangulation is deliberately absent — the provider returns only the rates explicitly stored under the requested base; callers wire it through `CurrencyConverter` (which triangulates from its cache) when they need cross-rates. Raises `CurrencyProviderError(CURRENCY_PROVIDER.UNKNOWN_CURRENCY)` with `providerName: "virtual-currency"` when the requested base has no rates stored (table can legitimately be empty — no triangulation). Surfaces a tiny `makeUnknownCurrencyError(base)` helper + `VIRTUAL_PROVIDER_NAME` / `VIRTUAL_CURRENCY_PROVIDER_CODES` re-exports for ergonomic error construction. 2 subpath exports (`./error-codes/error-codes`, `./provider/provider`), 25 tests at 100/100/100/100 coverage. Universal-tier, `sideEffects: false`. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-ecb` | ✅ | Universal-tier European Central Bank sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Ships `EcbProvider implements CurrencyProvider` (`name = "ecb"`, `supportsFiat = true`, `supportsCrypto = false`) — free, no API key, consumes the ECB daily reference-rates XML feed (`https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml`). Routes every request through the shared `httpRequest()` helper from the slim core (abort-signal composition + per-call timeouts + typed `HTTP_ERROR` come for free), parses the EUR-quoted rate table via the same zero-dep regex the upstream `@suaveplan/currency-providers` shipped pre-slim, mathematically inverts EUR rates to any caller-requested non-EUR `base` (`result.EUR = 1 / baseRate`, `result.<other> = <eurRate> / baseRate`), honours `RateFetchOptions.targets` filtering and `RateFetchOptions.signal` cancellation, and raises typed `CurrencyProviderError` on every failure path (`HTTP_ERROR` via `httpRequest`, `PARSE_ERROR` when the upstream body contains zero currency tags so an empty rate map is never silently cached, `UNSUPPORTED_BASE` when the requested non-EUR base is missing from the feed, `UNKNOWN_CURRENCY` when every requested target is absent from the resolved map). Injectable `FetchLike` from `@suaveplan/types` (`NoKeyConfig.fetch`) so tests pass a recording closure instead of `vi.mock`. 1 subpath export (`./ecb/ecb`), 16 tests at 100/100/100/100 coverage. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-providers` | ✅ | Universal-tier **slim plugin core** for `@suaveplan/currency` rate providers (slimmed 2026-05-17 from the original 9-provider bundle to a foundation-with-sibling-plugins shape matching `@suaveplan/network` and `@suaveplan/drive`). Ships `CurrencyProviderRegistry` (name-keyed `register` / `get` / `has` / `list` / `unregister`, typed `PROVIDER_NOT_REGISTERED` / `PROVIDER_ALREADY_REGISTERED` errors, sorted-snapshot `list()` for deterministic downstream tests), `defineCurrencyProvider` factory + `DefineCurrencyProviderInput` (returns an `Object.freeze`-d `CurrencyProvider` from a plain object literal), shared `httpRequest` / `composeAbortSignal` helper (used by every sibling provider — abort-signal composition via `AbortSignal.any`, `AbortSignal.timeout`-derived timeouts, non-2xx → typed `HTTP_ERROR`, returns raw `Response`), typed `CurrencyProviderError extends SuaveplanError<CurrencyProviderErrorCode>` with the 8-code `CURRENCY_PROVIDER_*` registry (`HTTP_ERROR`, `PARSE_ERROR`, `UPSTREAM_ERROR`, `UNSUPPORTED_BASE`, `MISSING_CONFIG`, `UNKNOWN_CURRENCY`, `PROVIDER_NOT_REGISTERED`, `PROVIDER_ALREADY_REGISTERED`) + optional `providerName` / `status` / `upstreamMessage` discriminants, and the four shared config types (`ApiKeyConfig`, `NoKeyConfig`, `HttpFetchOptions`, `ProviderConfigBase`) every sibling extends. Concrete providers (ECB, Fixer, Open Exchange Rates, Binance, CoinGecko, CoinMarketCap, CryptoCompare, Kraken, Virtual) ship as separate `@suaveplan/currency-provider-<name>` sibling packages (follow-up wave). 5 subpath exports, 32 tests at 100/100/100/100 coverage. Deps: `@suaveplan/currency`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/money` | ✅ | Universal-tier precise currency arithmetic — `bigint` minor-unit `Money` class with full arithmetic + comparison + conversion + lossless JSON round-trip, 30 ISO 4217 fiat + 5 cryptocurrency built-ins, custom and virtual currency registration, locale-aware `Intl.NumberFormat` formatting (`formatMoney`, `formatMoneyCompact`), fair allocators preserving the input total (`allocateEvenly`, `allocateByRatios`, `allocateByPercentages`), aggregators (`sumMoney`, `minMoney`, `maxMoney`), typed `MoneyError` hierarchy with stable `MONEY.*` code registry. 6 subpath exports, 90 tests at 100/100/100/100 coverage. |
|
||||
| `@suaveplan/payment` | ✅ | Universal-tier payment intent lifecycle, state-machine enforcement, webhook signature verification (HMAC via `@suaveplan/crypto-hash` + constant-time compare via `@suaveplan/crypto-utils`), receipt generation, fraud scoring, refund-balance ledger, jurisdiction-aware tax calculator, retry orchestration with exponential backoff + jitter, reconciliation matcher, and PCI-conscious card tokenization. Provider-agnostic; ships Stripe + PayPal + in-memory Test adapters. Typed `PaymentError` extending `SuaveplanError<PaymentErrorCode>` with 13 codes. 16 subpath exports, 180 tests at 100/100/100/100 coverage. |
|
||||
| `@suaveplan/payment-providers` | ✅ | Universal-tier **slim plugin core** for `@suaveplan/payment` provider adapters (slimmed 2026-05-17 from the original 5-adapter bundle to a foundation-with-sibling-plugins shape matching `@suaveplan/network`, `@suaveplan/drive`, and `@suaveplan/currency-providers`). Ships `ProviderRegistry` (name-keyed `register` / `get` / `has` / `list` / `unregister`, typed `PROVIDER_NOT_REGISTERED` / `PROVIDER_ALREADY_REGISTERED` errors, insertion-ordered frozen `list()` snapshot), `definePaymentProvider<TName>` factory (returns an `Object.freeze`-d literal-typed `PaymentProvider` for sibling packages that prefer a closure to a class), shared `httpRequest` + the now-public `composeAbortSignal` helper (used by every sibling for caller-signal + per-call timeout composition; JSON serialisation, string-body passthrough, typed errors, empty-body handling), typed `PaymentProviderError extends SuaveplanError<PaymentProviderErrorCode>` with the 6-code `PAYMENT_PROVIDER_*` registry (`HTTP_ERROR`, `TIMEOUT`, `PARSE_ERROR`, `OAUTH_TOKEN_FAILURE`, `PROVIDER_NOT_REGISTERED`, `PROVIDER_ALREADY_REGISTERED`) + optional `provider` / `httpStatus` / `url` / `bodyExcerpt` discriminants, and the four shared config shapes (`ApiKeyConfig`, `OAuthConfig`, `MerchantKeyConfig`, `ProviderCommonOptions`) plus the `HttpOptions` / `ProviderResponse` / `NowFn` / `RandomIdFn` / `Base64EncodeFn` / `FetchLike` injectable seams every sibling consumes. Concrete providers (Stripe, PayPal, Square, Mollie, Adyen) ship as separate `@suaveplan/payment-provider-<name>` sibling packages (follow-up wave). 5 subpath exports, 45 tests at 100/100/100/100 coverage. Deps: `@suaveplan/payment`, `@suaveplan/error`, `tslib`. |
|
||||
| `@suaveplan/payment-provider-adyen` | ✅ | Universal-tier Adyen Checkout API v71 adapter — sibling provider package on the slim `@suaveplan/payment-providers` core. `AdyenProvider` implements the `PaymentProvider` contract from `@suaveplan/payment` with literal name `"adyen"`, `AdyenConfig` (alias of the shared `MerchantKeyConfig`) constructor requiring `apiKey` + `merchantAccount` and accepting optional `baseUrl` (defaults `https://checkout-test.adyen.com/v71`), `timeoutMs`, `signal`, `fetch`, `randomId`, and `webhookHmacKey`. `X-API-Key: <apiKey>` header stamped on every call (read + write); caller-supplied `idempotencyKey` forwards verbatim as the standard `Idempotency-Key` header on `createIntent` and `refund` AND as the request body's `reference` field on `createIntent`. When `idempotencyKey` is omitted on `createIntent`, the default `reference` is generated via the injectable `RandomIdFn` (falls back to `globalThis.crypto.randomUUID()` at call time when available; throws a typed `PaymentProviderError(PARSE_ERROR)` otherwise so the package stays universal-tier with no `node:crypto` import). Full lifecycle: `createIntent` → POST `/payments` (sends `merchantAccount`, `amount`, `reference`, optional `captureDelayHours: 0` for `manual` capture, `shopperStatement`, `returnUrl`, `paymentMethod.storedPaymentMethodId`, `metadata`), `confirmIntent` → POST `/payments/{id}/details`, `captureIntent` → POST `/payments/{id}/captures`, `cancelIntent` → POST `/payments/{id}/cancels`, `refund` → POST `/payments/{id}/refunds` (with optional `amount`), `getIntent` → GET `/payments/{id}`. The configured `merchantAccount` is injected into every modification body (`captures`, `cancels`, `refunds`) per Adyen's contract. Exhaustive Adyen → domain mappings: 10-row result-code status table (`Authorised` → `authorized`, `Refused`/`Error` → `failed`, `Cancelled` → `canceled`, `Pending`/`Received`/`RedirectShopper`/`IdentifyShopper`/`ChallengeShopper`/`PresentToShopper` → `pending`, unknown → `failed`), 1-row refund-status table (`received` → `pending`, else → `failed`), payment-method normalisation (`scheme` → `card`, anything else → `wallet`; brand lower-cased; `lastFour` → `last4`). Every HTTP call routes through slim-core `httpRequest`; raised `PaymentProviderError`s carry `provider: "adyen"` discriminant. 2 subpath exports (`./adyen/adyen`, `./types/adyen`), 36 tests at 100/100/100/100 coverage. Deps: `@suaveplan/payment`, `@suaveplan/payment-providers`, `tslib`. |
|
||||
| `@suaveplan/payment-provider-mollie` | ✅ | Universal-tier Mollie Payments API v2 adapter — sibling provider package on the slim `@suaveplan/payment-providers` core. `MollieProvider` implements the `PaymentProvider` contract from `@suaveplan/payment` with name `"mollie"`, `ApiKeyConfig` constructor, `Authorization: Bearer <apiKey>` header auto-applied on every call, and the full lifecycle (`createIntent` → POST `/payments`, read-through `confirmIntent`/`captureIntent` for Mollie's auto-capture model, `cancelIntent` → DELETE, `refund` → POST `/payments/{id}/refunds`, `getIntent` → GET). Marshals genesis Money (`bigint` minor-units) to / from Mollie's `{ currency, value: "X.YY" }` wire shape via the exported pure helpers `formatMollieAmount` / `parseMollieAmount` (lossless round-trip via `bigint` — covered with 1-cent, exact-major, large-value, and over-precision edges). Exported total status maps (`mapMollieStatus`, `mapMollieRefundStatus`) with documented unknown-status fallback to `"failed"`. All failures surface as `PaymentProviderError` from the slim core; raw `Error` is never thrown. 2 subpath exports, 34 tests at 100/100/100/100 coverage. Deps: `@suaveplan/payment`, `@suaveplan/payment-providers`, `@suaveplan/error`, `tslib`. |
|
||||
| `@suaveplan/payment-provider-paypal` | ✅ | Universal-tier PayPal Orders v2 + Payments Captures Refund adapter — sibling provider package on the slim `@suaveplan/payment-providers` core. `PayPalProvider` implements the `PaymentProvider` contract from `@suaveplan/payment` with literal name `"paypal"`, `PayPalConfig` constructor extending the shared `OAuthConfig` with `sandbox?: boolean` (flips the default base URL to `https://api-m.sandbox.paypal.com`; explicit `baseUrl` always wins) plus injectable `now: NowFn` (token-cache TTL clock) and `base64Encode: Base64EncodeFn` (Basic-header encoder; defaults to `globalThis.btoa` — runtimes without `btoa` supply their own without forking the package). OAuth2 client-credentials token cache: POST `/v1/oauth2/token` with `Authorization: Basic <base64(clientId:clientSecret)>` + `Content-Type: application/x-www-form-urlencoded` + `grant_type=client_credentials`; cached in-memory with `tokenAcquiredAt + (expires_in * 1000) - 60_000` ms TTL (60 s clock-skew buffer); reused across every method call until expiry; refreshed transparently. Token-fetch failures (non-2xx, transport error, malformed JSON) raise `PaymentProviderError(OAUTH_TOKEN_FAILURE)` with `provider: "paypal"`, the token URL on the `url` discriminant, and the underlying cause attached — raised before any subsequent Orders-v2 call is attempted. Full lifecycle: `createIntent` → POST `/v2/checkout/orders` (intent `AUTHORIZE` for `captureMethod: "manual"`, `CAPTURE` otherwise; sends `purchase_units[0].amount` + optional `description`; caller-supplied `idempotencyKey` forwards as `PayPal-Request-Id` header), `confirmIntent` → POST `/v2/checkout/orders/{id}/authorize`, `captureIntent` → POST `/v2/checkout/orders/{id}/capture` (sums every nested `purchase_units[].payments.captures[].amount.value` into `capturedAmount`), `cancelIntent` → POST `/v2/checkout/orders/{id}/void` (returns synthesised `canceled` intent whose timestamps come from `new Date(now()).toISOString()` so tests pin them deterministically), `refund` → POST `/v2/payments/captures/{paymentIntentId}/refund` (sends optional `amount` + `PayPal-Request-Id` from caller's `idempotencyKey`; empty-object body when neither is supplied), `getIntent` → GET `/v2/checkout/orders/{id}`. Exhaustive PayPal → domain mappings: 6-row order-status table (`CREATED`/`SAVED` → `created`, `APPROVED` → `authorized`, `VOIDED` → `canceled`, `COMPLETED` → `captured`, `PAYER_ACTION_REQUIRED` → `pending`, unknown → `failed`), 3-row refund-status table (`COMPLETED` → `succeeded`, `PENDING` → `pending`, else → `failed`), `MoneyLike` bigint minor-unit ↔ PayPal decimal-string major-unit marshalling (`5000n` ↔ `"50.00"`; handles PayPal's zero-decimal currencies like JPY where no `.` separator is sent). Every HTTP call routes through slim-core `httpRequest`; `Authorization: Bearer <token>` stamped on every call from the cache; raised `PaymentProviderError`s carry `provider: "paypal"` discriminant. 3 subpath exports (`.`, `./paypal/paypal`, `./paypal/mapping`), 25 tests at 100/100/100/100 coverage. Deps: `@suaveplan/payment`, `@suaveplan/payment-providers`, `@suaveplan/error`, `tslib`. |
|
||||
| `@suaveplan/payment-provider-stripe` | ✅ | Universal-tier Stripe REST API adapter — sibling provider package on the slim `@suaveplan/payment-providers` core. `StripeProvider` implements the `PaymentProvider` contract from `@suaveplan/payment` with literal name `"stripe"`, `StripeConfig` (alias of the shared `ApiKeyConfig`) constructor, `Authorization: Bearer <apiKey>` on every call (read + write), and the full payment-intent + refund lifecycle (`createIntent` → POST `/payment_intents`, `confirmIntent` / `captureIntent` / `cancelIntent` → POST `/payment_intents/:id/{confirm,capture,cancel}`, `refund` → POST `/refunds`, `getIntent` → GET). Caller-supplied `idempotencyKey` forwards verbatim as the standard Stripe `Idempotency-Key` header on `createIntent` and `refund`; omitted on read calls and when the caller did not supply one. Exhaustive Stripe → domain mappings: 7-row payment-intent status table (`requires_payment_method`/`requires_confirmation` → `created`, `requires_action`/`processing` → `pending`, `requires_capture` → `authorized`, `succeeded` → `captured`, `canceled` → `canceled`, unknown → `failed`), 3-row refund-status table (`succeeded`/`pending`/else → `failed`), 4-row refund-reason table (`duplicate`/`fraudulent`/`requested_by_customer`/else → `other`, absent → `other`), payment-method normalisation (`card` → `card` with `brand`+`last4`, any other → `wallet`, absent → `undefined`). Every HTTP call flows through the slim-core `httpRequest` so caller-supplied `fetch` / `signal` / `timeoutMs` from `ProviderCommonOptions` propagate end-to-end; raised `PaymentProviderError`s carry `provider: "stripe"` discriminant. 2 subpath exports, 42 tests at 100/100/100/100 coverage. Deps: `@suaveplan/payment`, `@suaveplan/payment-providers`, `tslib`. |
|
||||
| `@suaveplan/payment-provider-square` | ✅ | Universal-tier Square Payments API adapter — sibling provider package on the slim `@suaveplan/payment-providers` core. `SquareProvider` implements the `PaymentProvider` contract from `@suaveplan/payment` with literal name `"square"`, `SquareConfig` constructor extending the shared `ApiKeyConfig` with required `locationId: string` (Square multi-location support) plus optional `sandbox?: boolean` (flips the default base URL to `https://connect.squareupsandbox.com/v2`), `apiVersion?: string` (pins the `Square-Version` header — defaults to `"2024-01-18"`), and `randomId?: RandomIdFn` (deterministic idempotency-key generation; defaults to `crypto.randomUUID()` at call time). Full lifecycle: `createIntent` → POST `/payments` (sends `amount_money`, `location_id`, `autocomplete`, `idempotency_key` plus optional `customer_id`/`note`/`source_id`), `confirmIntent` → no-op delegating to `getIntent` (Square auto-confirms), `captureIntent` → POST `/payments/{id}/complete`, `cancelIntent` → POST `/payments/{id}/cancel`, `refund` → POST `/refunds` (with optional `amount_money`/`reason` and `idempotency_key`), `getIntent` → GET `/payments/{id}`. Exhaustive Square → domain mappings: 5-row payment-status table (`APPROVED` → `authorized`, `COMPLETED` → `captured`, `CANCELED` → `canceled`, `FAILED` → `failed`, `PENDING` → `pending`, unknown → `failed`) and 3-row refund-status table (`COMPLETED` → `succeeded`, `PENDING` → `pending`, anything else including `REJECTED`/`FAILED` → `failed`); payment-method normalisation lower-cases Square's upper-case `card_brand` and falls back to `"unknown"` for non-canonical brands; refund-reason maps any non-empty Square reason to `"requested_by_customer"` and empty/absent to `"other"`. `Authorization: Bearer <apiKey>` and `Square-Version: <apiVersion>` headers stamped on every request via the slim-core `httpRequest`; caller-supplied `fetch` / `signal` / `timeoutMs` from `ProviderCommonOptions` propagate end-to-end; raised `PaymentProviderError`s carry `provider: "square"` discriminant. 5 subpath exports, 37 tests at 100/100/100/100 coverage. Deps: `@suaveplan/payment`, `@suaveplan/payment-providers`, `tslib`. |
|
||||
| `@suaveplan/react-bank` | – | Game banking components and hooks |
|
||||
|
||||
### `contacts`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/api-plugin-contacts` | – | Contacts and groups API plugin |
|
||||
| `@suaveplan/contacts` | ✅ | Universal contact + n-level group management with materialised-path tree, audience resolution, dedup, CSV/JSON import-export, Zod boundaries, OTel instrumentation, typed `CONTACTS.*` error registry. |
|
||||
| `@suaveplan/macos-contacts` | ✅ | macOS Contacts.app bridge via AppleScript with `Contact`↔`Person` mapping, dedup via `@suaveplan/contacts`, push/pull/sync, vCard import-export. |
|
||||
|
||||
### `core`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/config` | ✅ | Layered configuration management: defaults, file, object, env, CLI layers merge… |
|
||||
| `@suaveplan/env` | ✅ | Type-safe environment variable loading: schema-first validation, field types, d… |
|
||||
| `@suaveplan/error` | ✅ | Foundation error substrate — SuaveplanError base class, stable code registry, c… |
|
||||
| `@suaveplan/schema-registry` | ✅ | Versioned schema registry with forward/backward compatibility checking for evol… |
|
||||
| `@suaveplan/types` | ✅ | Types-only foundation — canonical shared interfaces (Clock, IdGenerator, Rng, M… |
|
||||
| `@suaveplan/typescript-config` | ✅ | Shared TypeScript configuration presets for the Suaveplan monorepo |
|
||||
| `@suaveplan/utils` | ✅ | Universal utility library: debug, object, platform, number, random, string, ema… |
|
||||
|
||||
### `crypto`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/api-plugin-vault` | ✅ | Per-principal encrypted vault plugin wrapping `@suaveplan/vault`. 5 routes (`GET /vault/items`, `POST /vault/items`, `GET/PATCH/DELETE /vault/items/:id`) all gated by `requireAuth()` and scoped to `c.get("user")!.id` — foreign items return `VAULT_PLUGIN_ITEM_NOT_FOUND` 404 (NOT 403, anti-enumeration). `UpdateVaultItemBodySchema` is strict mode; `maxItemSizeBytes` default 10 MiB → 413 `VAULT_PLUGIN_ITEM_TOO_LARGE`. `VAULT_OWNERSHIP_SCHEMA` (vault_item + vault_share tables + indexes) applied via injected `MigrationRunner` or `db` fallback. Default unlock extractor `headerExtractor("X-Vault-Unlock")` — never logs the password. Per-principal Vault config: `email` ← `principal.email ?? principal.id`, `secretKey` ← `principal.id` (server-known stable 2nd secret), `argon2idSalt` ← `HKDF-SHA256(serverPepper, principalId, "suaveplan-vault-plugin-argon2id-salt", 16)`. `vaultService` registered with `VaultHandle` opaque type. 9 `VAULT_PLUGIN_*` codes, every one thrown. 4 counters (`items.{created,read,updated,deleted}_total{outcome}`) + 1 histogram (`item.size_bytes`). 116 tests including real-SurrealDB-testcontainer integration: full sign-up via auth → vault CRUD roundtrip with X-Vault-Unlock + cross-user isolation + served `/openapi.json` lists 5 routes + fast-check 200-run property test on size invariant. 100/100/100/100 coverage. `dependencies: ["auth"]`. |
|
||||
| `@suaveplan/crypto` | ✅ | Universal cryptographic primitives — AEAD (AES-256-GCM, ChaCha20-Poly1305, XCha… |
|
||||
| `@suaveplan/crypto-bin` | ✅ | Suaveplan crypto bins: suaveplan-hash-password (Argon2id / blake2b password has… |
|
||||
| `@suaveplan/crypto-browser` | ✅ | WebCrypto-backed engine implementations for @suaveplan/crypto — AES-256-GCM, Ed… |
|
||||
| `@suaveplan/crypto-cipher` | ✅ | AEAD cipher primitives with per-primitive selector layout: aes-256-gcm, chacha2… |
|
||||
| `@suaveplan/crypto-cli` | ✅ | Suaveplan keychain CLI — root + server identity lifecycle (init, roll, revoke,… |
|
||||
| `@suaveplan/crypto-core` | ✅ | Universal cryptographic primitives (formerly @suaveplan/crypto) — AEAD (AES-256… |
|
||||
| `@suaveplan/crypto-curve` | ✅ | Elliptic-curve primitives with per-primitive selector layout: secp256k1, ed2551… |
|
||||
| `@suaveplan/crypto-hash` | ✅ | Pure-TypeScript hashing substrate — SHA-2 digests, HMAC, HKDF, PBKDF2, Argon2id… |
|
||||
| `@suaveplan/crypto-rsa` | ✅ | RSA primitives — RFC 8017 PKCS #1 v2.2 (PKCS1v15 + PSS sign/verify, OAEP + PKCS1v15 encrypt/decrypt), RFC 5208/5958 PKCS#8, RFC 5280 SPKI, RFC 7517/7518 JWK, RFC 7468 PEM, RFC 7638 JWK Thumbprint + RFC 9278 thumbprint URI. Multi-prime keygen with NIST FIPS 186-5 Miller-Rabin (64 rounds), AbortSignal throughout, blinding (RFC 8017 §5.1.2 / Kocher 1996) wired by default into every sign/decrypt path with `{ blinding: false }` opt-out emitting a counter, hand-rolled ASN.1 reader/writer, SPKI fingerprint with hex/base64/base64url/colon-hex output, `RsaCryptoEngine` adapter (noble + WebCrypto subtle + auto-selector), `RsaKeyHandle` HSM/PKCS#11 bridge with reference `SoftwareRsaKeyHandleProvider`, FIPS 186-5 profile (modulusBits ∈ {2048,3072,4096}; e ∈ [2¹⁶+1, 2²⁵⁶-1] odd; PSS saltLength === hLen; SHA-1 banned for sign/verify), JWK `key_ops`/`use`/`alg` enforcement (RFC 7517 §4.2-4.4), Bleichenbacher-resistant combined-error decrypt, constant-time PKCS1v15 verify (Wycheproof-grade). 17-code error registry, 438 tests at 100/100/100/100 coverage. Gates `@suaveplan/x509` + `@suaveplan/jws` + `@suaveplan/webauthn`. |
|
||||
| `@suaveplan/crypto-sodium` | ✅ | libsodium-backed engine implementations for @suaveplan/crypto — AEAD (XChaCha20… |
|
||||
| `@suaveplan/crypto-utils` | ✅ | Shared cryptographic utilities: encoding (hex/base64/base64url/base58), byte he… |
|
||||
| `@suaveplan/crypto-vault` | ✅ | Vaulted cryptographic foundation: BIP39 mnemonics, OWASP-hardened Argon2id, 2… |
|
||||
| `@suaveplan/crypto-keys` | ✅ | Hierarchical keychain chain-signing: pluggable ChainHasher + ChainCodec interf… |
|
||||
| `@suaveplan/cryptocurrency` | ✅ | Universal-tier crypto utilities: 15-chain address validation with EIP-55 checksumming, EIP-1559 gas estimation (bigint), async chain registry with `LoggerLike` injection + typed `UNKNOWN_CHAIN` errors, ERC-20 `TokenRegistry` + `COMMON_TOKENS`, calldata / 4-byte selector decoder, bigint-pure unit conversion across ETH / BTC / SOL, `CryptocurrencyError` hierarchy with 5-code `CRYPTOCURRENCY.*` registry. 11 subpath exports; 183 tests at 100/100/100/100 (Istanbul). |
|
||||
| `@suaveplan/keys` | ➖ | Superseded: chain-signing → `@suaveplan/crypto-keys`; vault orchestration → `@suaveplan/vault`; chunked vault I/O → compose `@suaveplan/packer/chunked` + `@suaveplan/streams` + `@suaveplan/vault`. Bitpacker keychain schemas are obsolete (v1 derivation grammar replaces them). |
|
||||
| `@suaveplan/react-crypto` | – | Client-side cryptography components |
|
||||
|
||||
### `data`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/bytes` | ✅ | Low-level byte utilities: hex/base64/utf8 encoding, Uint8Array helpers, UUID co… |
|
||||
| `@suaveplan/data-masking` | ✅ | PII/sensitive data masking for objects and strings: field-level masking rules,… |
|
||||
| `@suaveplan/dedup` | ✅ | Entity deduplication engine with pluggable match keys, scoring rules, Jaro-Wink… |
|
||||
| `@suaveplan/diff` | ✅ | Zero-dependency RFC 6902 JSON Patch engine with structural diffing, patch appli… |
|
||||
| `@suaveplan/json` | ✅ | Safe JSON parse/stringify with prototype-pollution protection, circular-ref det… |
|
||||
| `@suaveplan/mime` | ✅ | Structured MIME-type detection, parsing, and registry: magic-byte sniffing, RFC… |
|
||||
| `@suaveplan/packer` | ✅ | Binary packing/unpacking: whole-file (gzip + AEAD + ed25519) and chunked (locke… |
|
||||
| `@suaveplan/packer-browser` | ✅ | Browser-tier runtime for `@suaveplan/packer` archives — `PackedResourceManager` (LRU + ref-count + Blob-URL bookkeeping), unit-tested Service Worker interceptor + hand-rolled SW-scope shim, `verifyArchiveSignature` (Ed25519 via `@suaveplan/crypto`), OTel metrics (`packed_resource_fetch_total`, `_cache_hits_total`, `_cache_miss_total`, `_decode_ms`), injected `Clock`/`Rng`, `AbortSignal` on every async entrypoint, `@suaveplan/retry` on the source-pull path, `@suaveplan/feature-flags` slot, 6-code error hierarchy, i18n MessageResolver catalog. Five subpath exports (`.`, `/manager`, `/service-worker`, `/utils`, `/singleton`). No React runtime dep. 153 tests at 100/100/100/100 coverage. |
|
||||
| `@suaveplan/react-packer` | ✅ | React 19 companion for `@suaveplan/packer-browser` — `usePackedResource` generic hook + `usePackedImage` / `usePackedAudio` / `usePackedVideo` / `usePackedFont` variants (font hook injects `@font-face` and gates on `document.fonts.load`) + `PackedResourceProvider` React context with `MessageResolver` propagation and `@suaveplan/packer-browser/singleton` fallback. `AbortSignal` on every load (pre-aborted, mid-flight abort, abort-on-unmount, abort-on-URL-change). 3-code error hierarchy (`LOAD_FAILED`, `FONT_LOAD_FAILED`, `ABORTED`) via branded `ReactPackerError`. Seven subpath exports (`.`, `/hooks/*` × 5, `/provider`). React is a peer dep — non-React consumers of `packer-browser` pay nothing. 48 tests at 100/100/100/100 coverage. |
|
||||
| `@suaveplan/pagination` | ✅ | Universal pagination primitives: cursor, offset, keyset strategies with adapter… |
|
||||
| `@suaveplan/serialize` | ✅ | Round-trip-preserving structured serialization — tagged JSON, MessagePack binar… |
|
||||
| `@suaveplan/templates` | ✅ | Plugin-based template engine with type-safe contexts, variable interpolation, t… |
|
||||
| `@suaveplan/validation` | ✅ | Runtime schema validation with Zod-compatible API — self-contained parse engine… |
|
||||
|
||||
### `database`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/db` | ✅ | Universal relational database adapter interface: DatabaseAdapter (extends Datab… |
|
||||
| `@suaveplan/db-sqlite` | ✅ | Universal SQLite adapter for the genesis monorepo: bun:sqlite on the Bun server… |
|
||||
| `@suaveplan/migration` | ✅ | Database-agnostic migration runner with ledger tracking, dry-run, checksums, lo… |
|
||||
| `@suaveplan/seed` | ✅ | Universal-tier declarative seeding & fixtures: SeedBuilder + SeedRegistry + Sed… |
|
||||
| `@suaveplan/surrealdb` | ✅ | Universal SurrealDB client wrapper implementing DatabaseAdapter: connection lif… |
|
||||
|
||||
### `docs`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/api-docs` | – | API documentation generator |
|
||||
| `@suaveplan/compress-svg` | ✅ | SVG optimisation and minification: whitespace removal, redundant attribute stri… |
|
||||
| `@suaveplan/docs` | ✅ | Server-tier markdown docs toolkit: monorepo-wide discovery with `AbortSignal` + IO-error counter, typed mdast parsing (no `any`), bidirectional reference graph, orphan finder, hierarchy builder with configurable `maxDepth`, scored search with snippet context, eight `DOCS.*` codes, eight OTel instruments (4 counters + 4 histograms), Zod-validated `DiscoverOptions`, eighteen-category `DocCategory`. 95 tests, 100/100/100/100, 9 docs at 100%. |
|
||||
| `@suaveplan/pdf` | ✅ | Server-tier PDF 1.4 generation toolkit (Chunks A–E): typed `PdfError` registry (13 `PDF.*` codes), Zod boundary schemas, six OTel instruments (4 counters + 2 histograms), unit + hex/RGB colour utilities, page-size + margin types, hand-rolled PDF 1.4 binary writer with FlateDecode + AbortSignal + injectable `Clock`, chainable `ContentStreamEncoder`, four document-element primitives (text/table/image/divider) with Zod validation, `StyleResolver` cascade + `DEFAULT_THEME`, block-flow `LayoutEngine` with header/footer slots, fluent `DocumentBuilder`, orchestrating `renderDocument` returning real PDF 1.4 bytes round-trippable via `pdf-parse`, and three high-level templates — `createReceiptTemplate`, `createReportTemplate`, `createInvoiceTemplate` — each Zod-validated and shipping their own pdf-parse round-trip. 395 tests, 100/100/100/100, 21 docs at 100%. |
|
||||
| `@suaveplan/react-docs` | – | React components for rendering Suaveplan documentati... |
|
||||
| `@suaveplan/react-pdf` | – | PDF viewer with zoom and navigation |
|
||||
| `@suaveplan/react-rich-text` | – | Rich text WYSIWYG editor (Tiptap-based) |
|
||||
| `@suaveplan/sitemap` | ✅ | Sitemap generation and parsing: sitemap.xml, sitemap index, image sitemaps, vid… |
|
||||
|
||||
### `drive`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/drive` | ✅ | Modular virtual filesystem core for the Suaveplan monorepo. File-level operatio… |
|
||||
| `@suaveplan/drive-mesh` | ✅ | P2P mesh orchestrator over `drive-plugin-chunks`. `DriveMesh` class with `start`/`stop`/`requestChunk`/`advertise`/`unadvertise`/`peers`/`registry`/`tickGossip` + typed event emitter (`chunk:*`/`peer:*`/`gossip:*`). `PeerRegistry` overlay (availability bitsets per fileId, EMA latency α=0.3, score-ranked `rank(fileId, chunkId)`, session/persistent denylist). `GossipEngine` (periodic advertisement broadcast over compressed bitsets). `ParallelPuller` (top-K racing window default 250 ms, shared `AbortController`, mandatory `verifyChunkSig` gate, `maxParallelRequests` cap). `ServerFallback` (equal-jitter exponential backoff, signature verification on fallback bytes). 10-code `DriveMeshError`. Zod `DriveMeshOptionsSchema`/`ServerFallbackConfigSchema`/`AdvertisementSchema`. Consumes pluggable `DriveTransport` list (HTTPS, WebRTC) — does NOT import `network-plugin-registry` (boundary per `drive-network-integration` spec). 115 tests at 100/100/100/100 coverage; fixtures use real `ed25519` keypair + real `buildSignedChunk` from `drive-plugin-chunks` + in-memory `Broker`/`FakeDriveTransport` — no `vi.mock`/`vi.stubGlobal`. Shipped 2026-05-12 (`drive-mesh-port` archived). |
|
||||
| `@suaveplan/drive-mesh-protocol` | ✅ | Wire protocol for chunk discovery, request, and delivery across the mesh. Three… |
|
||||
| `@suaveplan/drive-plugin-chunks` | ✅ | Chunk-addressing plugin for @suaveplan/drive. Defines the version 0x0001 binary… |
|
||||
| `@suaveplan/drive-react-sharing` | ✅ | React bindings for `@suaveplan/drive-sharing`: `useSharing` hook (live `recipients` list, async `addRecipient`/`removeRecipient`/`updatePermission`/`refresh`, AbortSignal threading, OTel counters, real `GrantManager`/`PartitionManager`/`TokenManager` — no mocks). Components: `<ShareDialog>` (modal, ESC-dismiss, focus trap, `role="dialog"`/`aria-modal`), `<PartitionBadge>` (`role="status"` + composed `aria-label`), `<FileAccessList>` (semantic `<ul>`/`<li>`, Delete/Backspace keyboard revoke). 7-code `DriveReactSharingError`. 55 tests at 100% coverage, accessibility audited (ARIA roles, keyboard nav, screen-reader text). Stories per component. Shipped 2026-05-12 (`enterprise-react-drive-sharing` archived). |
|
||||
| `@suaveplan/drive-sharing` | ✅ | Partitioned drive with envelope encryption: AEAD envelope (AES-GCM/XChaCha) wi… |
|
||||
| `@suaveplan/drive-template` | ✅ | Server-signed drive templates: pre-configured folder structure and per-file met… |
|
||||
| `@suaveplan/drive-transport-https` | ✅ | HTTPS-relay-backed `TransportLike<MessageEnvelope>` for the drive mesh. Stateless `POST /send` + long-poll `GET /recv` with cursor-tracked at-least-once delivery, explicit `start()`/`stop()` lifecycle, equal-jitter exponential backoff on transient failures, auth-failure short-circuit exposing `lastAuthError` for credential refresh, 256-msg batch cap with yield between batches, injectable `fetchFn`/`clock`/`random`, Zod boundaries, 9 frozen `HTTPS_TRANSPORT_CODES`. 100% coverage. Shipped 2026-05-11 (`drive-transport-https-v1` archived). |
|
||||
| `@suaveplan/drive-plugin-cache` | ✅ | Read-through cache middleware for `@suaveplan/drive`. `driveCachePlugin(options): DrivePlugin<DriveCacheHandle>` registers a `DriveMiddleware` that intercepts `Drive.read(path)` against a caller-supplied `AbstractCache<Uint8Array>` (memory / OPFS / file / Redis / any `cache-storage` backend). Read-through populates on miss when `shouldCache(path, bytes)` returns true (default ceiling 16 MiB matching `drive-mesh-protocol`'s `MAX_FRAME_BYTES`). Write/delete invalidate post-success; stat/list passthrough. 3-code `DRIVE_CACHE.*` registry. Cache failures swallow + counter-record (perf optimisation, not correctness boundary). AbortSignal pre-abort short-circuit. Multi-namespace isolation on a shared cache. Full OTel surface (hits/misses/populations/invalidations/errors counters, `entry_bytes` histogram, outcome-tagged spans). Universal tier. Real `Drive` + `MemoryCache`/`StorageCache` in integration tests (no mocks). |
|
||||
| `@suaveplan/drive-uri` | ✅ | Drive/+mesh/+template URI plugins for `StorageUriResolver`. Three `UriPlugin` factories — `driveUriPlugin`, `driveMeshUriPlugin`, `driveTemplateUriPlugin` — wrap caller-supplied `Drive` instances in a `StorageLike` shim and plug into the existing `storage-uri` resolver. `drive+template://` plugin fetches templates via injected `templateProvider`, hydrates the drive via `Drive.hydrateFrom`, caches `(templateId → in-flight hydration)` so concurrent reads share one round-trip. 7-code `DRIVE_URI.*` registry (`INVALID_URI` / `DRIVE_ID_MISMATCH` / `TEMPLATE_NOT_AVAILABLE` / `TEMPLATE_INVALID` / `WRITE_UNSUPPORTED` / `CAPABILITY_MISSING` / `ABORTED`). Universal tier, no chunk-codec / peer-discovery / template-validation duplication — Drive handles all of that through its installed middleware. Real `Drive` + `chunksPlugin` + `storage-file` in integration tests (no mocks). |
|
||||
| `@suaveplan/drive-transport-webrtc` | ✅ | WebRTC datachannel implementation of the `DriveTransport` interface. Composes `WebRTCClient` + `DataChannelManager` from `@suaveplan/drive-webrtc`. Length-prefixed binary wire framing (`[4B frameLen][1B frameType][payload]`), 3 frame types (`chunk-request` / `chunk-response` / `advertisement`), `MAX_FRAME_BYTES = 16 MiB`. `CompressionStream`/`DecompressionStream`-backed gzipped-bitset advertisement payloads with injectable `StreamCtorResolver`. AbortSignal threading, idempotent `dispose`, 7-code `DriveTransportWebRtcError` (`FRAME_INVALID`/`FRAME_TOO_LARGE`/`REQUEST_TIMEOUT`/`PEER_NOT_CONNECTED`/`DATA_CHANNEL_CLOSED`/`BITSET_COMPRESSION_FAILED`/`ABORTED`), Zod payload schemas, OTel surface (2 counters + 2 histograms). 67 tests at 100% coverage. Tests reuse `FakeRTCPeerConnection`/`FakeRTCDataChannel` from drive-webrtc — no `vi.mock`/`vi.stubGlobal`. Shipped 2026-05-12 (`drive-transport-webrtc-port` archived). |
|
||||
| `@suaveplan/drive-webrtc` | ✅ | Browser-tier WebRTC P2P mesh plugin for `@suaveplan/drive`. 11 subsystems: `SignalingClient` (Zod-validated, reconnect with backoff), `WebRTCClient` (offer/answer/ICE/auto-restart, injectable RTCPeerConnection), `PeerManager` + `PeerBandwidthTracker` + 3 topology strategies (FullMesh/SuperPeer/HubAndSpoke), `DataChannelManager` (bufferedAmount throttle), `ChunkIntegrityManager` (real Ed25519 via `@suaveplan/crypto-curve` + SHA-256 hash), `ChunkDiscoveryManager` (multi-peer racing, integrity-gated fallback), `WebRTCChunkSource`, `WebRTCDrivePlugin` orchestrator, `LRUCache`, `RateLimiter`, `SubChunkSplitter`/`Reassembler`, security validators. 13-code error registry, Zod boundaries, OTel surface (counters + 3 histograms), 419 tests at 100% coverage, 11 bundle-size snapshots. Shipped 2026-05-12 (`enterprise-browser-drive-webrtc` archived). |
|
||||
|
||||
### `emails`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/dmarc` | ✅ | DMARC report parser — parses aggregate (RUA) and forensic (RUF) DMARC XML repor… |
|
||||
| `@suaveplan/domain-normalizer` | ✅ | Normalisation, matching, and alias-canonicalisation for internet domain names |
|
||||
| `@suaveplan/email-processing` | ✅ | postal-mime + libmime wrappers; parser, attachments, CID resolver, calendar, data extractor, DKIM canon/verify, DMARC, SPF, spam scorer, thread grouper, hooks pipeline |
|
||||
| `@suaveplan/email-templates` | ✅ | Universal-tier React-free email template engine: VNode factories, nine reusable components, five built-in templates, typed registry, fluent builder, theme system, structured errors, OTel instruments |
|
||||
| `@suaveplan/emailer` | ✅ | Email sending with plugins |
|
||||
| `@suaveplan/suppression` | ✅ | Email suppression list management: in-memory store, bulk import/export (CSV/JSO… |
|
||||
|
||||
### `events`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/broadcast-channel` | ✅ | Cross-tab BroadcastChannel communication |
|
||||
| `@suaveplan/event-sourcing` | ✅ | Event sourcing patterns: aggregate root, typed event store, snapshot support, p… |
|
||||
| `@suaveplan/events` | ✅ | Typed event-bus substrate: pub/sub with deterministic FIFO, interceptor API, bo… |
|
||||
| `@suaveplan/events-metrics` | ✅ | OTel metrics companion for @suaveplan/events: dispatch counters, latency histog… |
|
||||
| `@suaveplan/events-otel` | ✅ | OpenTelemetry tracing companion for @suaveplan/events: wraps each dispatch in a… |
|
||||
| `@suaveplan/events-schema` | ✅ | Runtime schema-validation companion for @suaveplan/events: validates event payl… |
|
||||
| `@suaveplan/events-transport` | ✅ | Cross-context event bridges for @suaveplan/events: MessageChannel (at-least-onc… |
|
||||
|
||||
### `flow`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/cache` | ✅ | Type-safe caching abstraction: LRU/LFU/FIFO/TTL MemoryCache, NamespacedCache, C… |
|
||||
| `@suaveplan/cbor` | ✅ | CBOR (RFC 8949) + RFC 8742 sequences + RFC 9052 COSE + RFC 8392 CWT + RFC 8610 CDDL (Vaulted) |
|
||||
| `@suaveplan/crdt` | ✅ | Conflict-free replicated data types: G-Counter, PN-Counter, OR-Set, LWW-Registe… |
|
||||
| `@suaveplan/idempotency` | ✅ | Idempotency key guard and dedup: store adapters (memory, database, redis), dist… |
|
||||
| `@suaveplan/job-scheduler` | ✅ | Background job scheduling with cron |
|
||||
| `@suaveplan/queue` | ✅ | Enterprise-grade queue management: memory/priority/delayed queues, concurrent w… |
|
||||
| `@suaveplan/queue-distribution` | ✅ | Worker-pool distribution layer: load-balancing strategies (round-robin / least-busy / weighted), worker pool, manager with retry + health checks, in-process LocalWorker. Transport plugins ship separately. |
|
||||
| `@suaveplan/queue-distribution-http` | ✅ | HTTP transport plugin: HttpWorkerCommunicator (fetch-backed, Zod-validated envelope) + RemoteWorker with @suaveplan/retry-driven connect/reconnect. |
|
||||
| `@suaveplan/queue-distribution-worker-thread` | ✅ | Web-Worker / worker_threads transport plugin: WorkerThreadCommunicator (Zod envelope, bounded outstanding-request map, AbortSignal) + WorkerThreadWorker. |
|
||||
| `@suaveplan/rate-limiter` | ✅ | Enterprise rate limiter: TokenBucket, SlidingWindow, FixedWindow algorithms; te… |
|
||||
| `@suaveplan/retry` | ✅ | Retry strategies with exponential/linear/constant backoff, jitter, circuit brea… |
|
||||
| `@suaveplan/rules` | ✅ | Rule engine + builder + 33 operators + 3 validators + serializer + optimizer + actions + events |
|
||||
| `@suaveplan/rules-hono` | ✅ | Hono middleware suite for `@suaveplan/rules` — 6 factories + lazy default engine |
|
||||
| `@suaveplan/streams` | ✅ | WHATWG-Streams-based primitives: backpressure-first transforms, zero-copy Uint8… |
|
||||
| `@suaveplan/undo` | ✅ | Enterprise-grade undo/redo engine with branching history, async commands, histo… |
|
||||
|
||||
### `fonts`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/font-compress` | ✅ | BTF (Binary TypeFace) binary packing — packer-only since the 2026-05-17 split (unpacker now in `@suaveplan/font-decompress`). |
|
||||
| `@suaveplan/font-convert` | ✅ | Convert TTF/OTF/WOFF/WOFF2 font files to typeface.js JSON format. Defines canon… |
|
||||
| `@suaveplan/font-decompress` | ✅ | BTF binary unpacking — reverses `@suaveplan/font-compress::BinaryFontPacker` output back to typeface.js JSON for Three.js / R3F / game-font rendering. |
|
||||
| `@suaveplan/font-subset` | ✅ | Font subsetting pipeline: Unicode range subsetting, WOFF2/WOFF encoding, TTF-to… |
|
||||
| `@suaveplan/react-font-decompress` | – | React hooks for font loading |
|
||||
|
||||
### `forms`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/form-builder` | ✅ | Dynamic form schema builder with validation, conditi... |
|
||||
| `@suaveplan/react-admin-forms` | – | Admin form components |
|
||||
| `@suaveplan/react-calendar` | – | Calendar and date picker components |
|
||||
| `@suaveplan/react-color-picker` | – | Color picker component |
|
||||
| `@suaveplan/react-file-upload` | – | Drag-and-drop file upload with chunked/resumable upl... |
|
||||
| `@suaveplan/react-forms` | ✅ | React form-validation surface: schema-driven `<Form>`, field bindings, validators, error reporting; 88 src files, 28 test files, all gates green. |
|
||||
| `@suaveplan/react-wizard` | – | Multi-step form wizard with validation |
|
||||
|
||||
### `games`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/animation-events` | – | Animation event dispatching, foot contact detection, procedural modifiers |
|
||||
| `@suaveplan/api-plugin-anticheat` | – | Server-side anti-cheat with speed, damage, rate, and teleport validation |
|
||||
| `@suaveplan/api-plugin-game-persistence` | – | Game save/load with slots, auto-save, and size limits |
|
||||
| `@suaveplan/api-plugin-game-server` | – | Game server session management with tick rate and heartbeats |
|
||||
| `@suaveplan/api-plugin-matchmaking` | – | Skill-based matchmaking with queue management and gap expansion |
|
||||
| `@suaveplan/behavior-tree` | – | Behavior tree engine with composite nodes, decorators, blackboard, and fluent builder |
|
||||
| `@suaveplan/bone-mapping` | – | Skeleton detection, bone matching, retargeting math |
|
||||
| `@suaveplan/building` | – | Player building system with snap placement, structural integrity, and blueprints |
|
||||
| `@suaveplan/character-physics` | – | Engine-agnostic character controller physics (floating capsule, plugins) |
|
||||
| `@suaveplan/character-vehicle-picker` | – | Character/vehicle picker schemas, catalogs, and state management with BitPacker |
|
||||
| `@suaveplan/chat` | – | In-game chat with channels, whispers, rate limiting, and profanity filter |
|
||||
| `@suaveplan/combat` | – | Core combat mechanics with damage types, health, armor, shields, and status effects |
|
||||
| `@suaveplan/controller` | – | Controller abstractions |
|
||||
| `@suaveplan/crafting` | – | Recipe-based crafting with stations, material requirements, quality tiers, and queues |
|
||||
| `@suaveplan/day-night` | – | Day/night cycle with configurable time, light schedules, and gameplay modifiers |
|
||||
| `@suaveplan/destructibles` | – | Destructible environment system with fracture patterns, debris, and loot |
|
||||
| `@suaveplan/dialogue` | – | Branching dialogue trees with conditional choices, bark system, and conversation management |
|
||||
| `@suaveplan/economy` | – | In-game economy with wallets, trading, marketplace, and auction system |
|
||||
| `@suaveplan/ecs` | – | Archetype-based Entity Component System with bitset queries |
|
||||
| `@suaveplan/emotes` | – | Emote/expression system with registry, cooldowns, and level-gated unlocks |
|
||||
| `@suaveplan/fauna` | – | Wildlife AI with herding, predator-prey, territorial zones, and taming |
|
||||
| `@suaveplan/fruit-machine` | – | Nested state-machine engine for fruit-machine / AWP games (Adders & Ladders reference) |
|
||||
| `@suaveplan/game-loop` | – | Fixed-timestep game loop with tick scheduling |
|
||||
| `@suaveplan/game-persistence` | – | Save/load game state with versioned migrations |
|
||||
| `@suaveplan/game-server` | – | Universal game server framework with sessions, networking, matchmaking, and game queue |
|
||||
| `@suaveplan/game-state-schemas` | – | Bit-level quantization schemas for multiplayer state compression |
|
||||
| `@suaveplan/heightmap-tiles` | – | Delta-encoded heightmap tile pyramid with adaptive bit depth |
|
||||
| `@suaveplan/hitbox` | – | Hierarchical hit detection with per-bone hitboxes, raycasting, and sweep tests |
|
||||
| `@suaveplan/ik-solver` | – | Pure math inverse kinematics solvers (two-bone, look-at, multi-chain) |
|
||||
| `@suaveplan/leaderboard` | – | Leaderboard system with rankings, seasonal resets, and around-player queries |
|
||||
| `@suaveplan/lobby` | – | Multiplayer lobby management with matchmaking and countdown |
|
||||
| `@suaveplan/mixamo` | – | Mixamo character utilities |
|
||||
| `@suaveplan/npc` | – | NPC lifecycle management with spawn pools, population control, and daily schedules |
|
||||
| `@suaveplan/osm-buildings` | – | OSM building/road geometry extraction and mesh generation |
|
||||
| `@suaveplan/party` | – | Party/group system with invites, leadership transfer, and kick/promote |
|
||||
| `@suaveplan/progression` | – | Character progression with XP curves, skill trees, perks, and achievements |
|
||||
| `@suaveplan/quest` | – | Quest/mission system with multi-objective tracking, chains, rewards, and event-driven triggers |
|
||||
| `@suaveplan/react-controller` | – | Game controller components |
|
||||
| `@suaveplan/react-crafting` | – | Crafting UI with recipe filtering, station selection, and craft queues |
|
||||
| `@suaveplan/react-dialogue` | – | Dialogue UI with typewriter effect, choices, and speaker portraits |
|
||||
| `@suaveplan/react-game-hud` | – | Game HUD state for health, ammo, compass, minimap, notifications |
|
||||
| `@suaveplan/react-game-map` | – | World map with viewport, markers, fog of war, and zoom controls |
|
||||
| `@suaveplan/react-game-settings` | – | Game settings for graphics, audio, controls, and accessibility |
|
||||
| `@suaveplan/react-social-game` | – | Social game UI with friends list, requests, blocking, and status |
|
||||
| `@suaveplan/vehicle-physics` | – | Engine-agnostic vehicle physics (Pacejka tires, suspension, drivetrain) |
|
||||
| `@suaveplan/weapons` | – | Weapon system with projectile ballistics, melee, recoil, ammo, and attachments |
|
||||
| `@suaveplan/weather` | – | Dynamic weather simulation with transitions, zones, and gameplay effects |
|
||||
|
||||
### `geo`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/geo` | ✅ | Abstract geospatial geometry: ellipsoids, spherical coordinates, transforms, distance, bearing, angular separation, bounds, polygons, units, and Zod schemas |
|
||||
| `@suaveplan/geo-earth` | ✅ | Earth configuration and legacy geo utilities — WGS84 constants, geocoding, geohash, coordinate parsing/formatting |
|
||||
| `@suaveplan/geo-planet` | ✅ | Configurable planet surface geometry from PlanetConfig — ellipsoid, atmosphere, satellites, and orbital elements |
|
||||
| `@suaveplan/solar-system` | ✅ | Solar system provider with injectable astronomy engine — body positions, seasons, equinoxes, solstices, sunrise/sunset, lunar phases |
|
||||
| `@suaveplan/stars` | ✅ | Star catalog provider with in-memory sources, cone queries, magnitude filtering, and atmospheric extinction |
|
||||
| `@suaveplan/tides` | ✅ | Tidal prediction providers — TICON harmonic engine, ADMIRALTY, and physics-based with multi-satellite superposition |
|
||||
| `@suaveplan/universe` | ✅ | Observer model, coordinate frames, wire protocol packets, and provider registry for solar, star, tide, and weather data |
|
||||
|
||||
### `gtap`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/api-plugin-gtap` | – | GTAP integration plugin |
|
||||
|
||||
### `i18n`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/i18n` | ✅ | Slim integration core (Wave 6 modular split, archived 2026-05-09). Owns the `MessageCatalog` (typed `t<K extends DotPaths<TMessages>>`, ergonomic options bag, nested `$t()` refs), `createI18n` / `I18nInstance` plugin slot, locale negotiation/resolver, server middleware, observability state (detector + backend metrics), schemas, key utilities, post-processors (pseudo-localize, missing-key reporter), and `defaultFormatBridge` wiring the per-formatter packages into the universal-tier ICU parser. Constants / errors / parser / pluralization / interpolation / formatters / detector / backends now live in 14 sibling packages and are re-exported here via the root barrel for back-compat. Four kept subpaths (`./detector`, `./parser`, `./plugin`, `./errors`). 320 tests in the slim core, 100/100/100/100 coverage; downstream sweep 40 react-* tasks green (1238 tests in `react-ui` alone). Bundle 592 KB / 15 modules (was ~3.5 MB / ~120 modules pre-split). |
|
||||
| `@suaveplan/i18n-backend-chained` | ✅ | Universal-tier chained backend orchestrator: `ChainedBackend` class + `chainedBackend` factory, capabilities matrix derived from union of children (`canRead: true`; `canSave/canTrackModification/canWatch` follow union), read-fallthrough on `BACKEND_LOCALE_NOT_AVAILABLE` with immediate propagation of `BACKEND_READ_FAILED`, write-through cache after successful read from index N to indices 0..N-1 (best-effort; per-backend save failures swallowed), `readMulti` concurrent fan-out with `first-wins` (default) or `last-wins` merge strategies (gaps filled regardless), `save` fan-out to every save-capable child surfacing failures as `BACKEND_WRITE_FAILED`, `lastModified` returns first defined timestamp, `init` forward / `dispose` reverse-order, optional `AbortSignal` on `read`/`readMulti`/`save` throwing `BACKEND_TIMEOUT` on pre-aborted signals + threaded into children, defensive `CHAINED_BACKEND_ALL_FAILED` runtime guard for empty-chain bypasses past the schema, Zod boundary validation throwing `INVALID_OPTIONS`, OTel instruments (`i18n.backend.chained.read_duration_ms{backend,locale,namespace,outcome}` histogram, `read_total{backend,outcome}` counter — `outcome ∈ hit/miss/error/cache_hit/cache_miss/primary` extending the base set with chained-cache classification, `read_errors_total{backend,code}` counter, `write_duration_ms`/`write_total{backend,outcome}`, `fallthrough_total{from_backend,to_backend}` counter for per-link fallthrough rate). Tested with shared `runBackendConformance` harness from `@suaveplan/i18n-shared/testing`. |
|
||||
| `@suaveplan/i18n-backend-fs-multifile` | ✅ | **Server-tier** Node `fs/promises`-backed translation backend: `MultiFileFsBackend` class + `multiFileFsBackend` factory, 5-flag capability matrix (`canRead: true`, `canReadMulti: false`, `canSave: true`, `canTrackModification: true`, `canWatch` follows the `watch` option), three wire layouts (`nested` default reads `<root>/<locale>/<namespace>.<ext>`, `flat` reads `<root>/<locale>.<namespace>.<ext>`, `glob` substitutes `{locale}` / `{namespace}` placeholders into a caller-supplied pattern joined to root). JSON built-in encode/decode rejecting non-object payloads as `BACKEND_READ_FAILED`; YAML/YML and JSON5 auto-detected via dynamic `import()` (`yaml` / `json5` are optional peer deps, never direct deps); missing peers surface as `BACKEND_READ_FAILED` with `peer` in the error context. Optional `fs.watch` loop emits `loaded` events through `BackendInitContext.emitLoaded` for both `nested` and `flat` layouts (HMR / Suspense refresh integration). ENOENT / ENOTDIR map to `BACKEND_LOCALE_NOT_AVAILABLE` (chained-backend fallthrough); other errno codes (`EACCES`, `EBUSY`, …) map to `BACKEND_READ_FAILED`; `writeFile` failure → `BACKEND_WRITE_FAILED` with `cause` preserved; unsupported encode extension → `BACKEND_WRITE_FAILED`. Optional `AbortSignal` on `read`/`save` throws `BACKEND_TIMEOUT` on pre-aborted signals before any filesystem call. Injectable `MultiFileFsLike` shim defaulting to `node:fs/promises` via dynamic import — tests run against a real `node:fs.mkdtemp` tmpdir per CLAUDE.md "no mocks" rule. Zod boundary validation throwing `INVALID_OPTIONS` (rejects bad `layout`, empty `name`, `glob` without `globPattern`). OTel instruments (`i18n.backend.fs_multifile.read_duration_ms{backend,locale,namespace,outcome,layout}` histogram, `read_total{backend,outcome,layout}` counter, `read_errors_total{backend,code,layout}` counter, `write_duration_ms{backend,outcome,layout}` histogram, `write_total{backend,outcome,layout}` counter — `layout` 3-value low-cardinality enum). `package.json` declares `engines.node >=20`, omits `browser` field; leading `// Server-tier package` comment in `src/backend/backend.ts`; NOT re-exported from slim `@suaveplan/i18n` core (Wave 6). Tested with shared `runBackendConformance` harness from `@suaveplan/i18n-shared/testing`. |
|
||||
| `@suaveplan/i18n-backend-http` | ✅ | Universal-tier `fetch`-based translation backend: `HttpBackend` class + `httpBackend` factory, read-only 5-flag capability matrix (`canRead=true`, all writes/watches false), URL templating via `{locale}`/`{namespace}` placeholders with `encodeURIComponent`, pluggable `parsePayload` (default JSON-object parser rejecting arrays/null/primitives as `BACKEND_READ_FAILED`), exported `defaultParsePayload` + `buildUrl` helpers, pluggable `customHeaders` builder, `crossDomain: true` shorthand for `credentials: "include"` + `mode: "cors"`, structurally-compatible `retry?: HttpBackendRetry` (consumes `@suaveplan/retry`'s `RetryPolicy` without a hard dep), optional `AbortSignal` on `read` throwing `BACKEND_TIMEOUT` on pre-aborted signals + threading into `RequestInit` for mid-flight cancellation (AbortError / `ABORT_ERR` code → `BACKEND_TIMEOUT`), `defaultFetch()` fallback throwing `BACKEND_READ_FAILED` when `globalThis.fetch` is unavailable, Zod boundary validation throwing `INVALID_OPTIONS`, error semantics (404 → `BACKEND_LOCALE_NOT_AVAILABLE` for chained-backend fallthrough; 5xx / network / parse → `BACKEND_READ_FAILED` chain-stopping signal), OTel instruments (`i18n.backend.http.read_duration_ms{locale,namespace,outcome}` histogram, `read_total{outcome}` counter, `read_errors_total{code,status?}` counter — `status` present on HTTP-classified errors). Tested with shared `runBackendConformance` harness from `@suaveplan/i18n-shared/testing`. |
|
||||
| `@suaveplan/i18n-backend-memory` | ✅ | Universal-tier in-memory translation backend: `MemoryBackend` class + `memoryBackend` factory, full 5-flag capability matrix (`canRead/canReadMulti/canSave/canTrackModification`, `canWatch=false`), `Map<locale, Map<namespace, TranslationRecord>>` store + modification ledger, injectable `now` clock, optional `AbortSignal` on `read`/`readMulti`/`save` throwing `BACKEND_TIMEOUT`, Zod boundary validation throwing `INVALID_OPTIONS`, OTel instruments (`i18n.backend.memory.read_duration_ms`, `read_total{outcome}`, `read_errors_total{code}`, `write_duration_ms`, `write_total{outcome}`). Tested with shared `runBackendConformance` harness from `@suaveplan/i18n-shared/testing`. |
|
||||
| `@suaveplan/i18n-backend-storage` | ✅ | Universal-tier `AbstractStorage`-backed translation backend: `StorageBackend` class + `storageBackend` factory, full 5-flag capability matrix (`canRead/canReadMulti/canSave/canTrackModification`, `canWatch=false`), three wire layouts (`single-key-per-namespace` default, `single-key-per-locale`, `flat-keys`), pluggable codec (default JSON via `TextEncoder`/`TextDecoder` rejecting non-object payloads as `BACKEND_READ_FAILED`), exported `defaultCodec`, optional `AbortSignal` on `read`/`readMulti`/`save` throwing `BACKEND_TIMEOUT`, Zod boundary validation throwing `INVALID_OPTIONS`, `lastModified` consults `AbstractStorage.getMetadata().modifiedAt` (max across leaves for `flat-keys`), OTel instruments (`i18n.backend.storage.read_duration_ms`, `read_total{outcome,layout}`, `read_errors_total{code,layout}`, `write_duration_ms`, `write_total{outcome,layout}` — `layout` is a 3-value low-cardinality enum). Tested via `runBackendConformance` against all three layouts. |
|
||||
| `@suaveplan/i18n-detector` | ✅ | Universal-tier locale-detection chain: `createDetectorChain` orchestrator, `bcp47Negotiate` default negotiator, `getTextDirection` / `isRtl` helpers, 9 detectors (querystring, userPreference, cookie, localStorage, htmlLang, acceptLanguage, navigator, cloudflareIpGeo, fallback), `defaultDetectorChain` factory, OTel instruments (`i18n.detector.match_total{detector}`, `i18n.detector.detected_total{detector,locale}`, `i18n.detector.duration_ms`, `i18n.detector.chain.exhausted_total`, `i18n.detect` span). |
|
||||
| `@suaveplan/i18n-formatter-collator` | ✅ | Universal-tier `Intl.Collator` adapter exposing locale-aware string comparison + sort helpers, plugged into `defaultFormatBridge`. |
|
||||
| `@suaveplan/i18n-formatter-currency` | ✅ | Universal-tier `Intl.NumberFormat` currency adapter. Currency-code-aware formatting with sign display + accounting modes, plugged into `defaultFormatBridge`. |
|
||||
| `@suaveplan/i18n-formatter-date` | ✅ | Universal-tier `Intl.DateTimeFormat` adapter. Date / time / datetime formatting with caller-supplied options and cache, plugged into `defaultFormatBridge`. |
|
||||
| `@suaveplan/i18n-formatter-display-name` | ✅ | Universal-tier `Intl.DisplayNames` adapter for language / region / script / currency name lookup, plugged into `defaultFormatBridge`. |
|
||||
| `@suaveplan/i18n-formatter-list` | ✅ | Universal-tier `Intl.ListFormat` adapter. Conjunction / disjunction / unit list formatting plugged into `defaultFormatBridge`. |
|
||||
| `@suaveplan/i18n-formatter-number` | ✅ | Universal-tier `Intl.NumberFormat` adapter for the i18n family. Locale-aware decimal / percent / scientific / compact formatting with caching, plugged into `defaultFormatBridge` by `@suaveplan/i18n`. |
|
||||
| `@suaveplan/i18n-formatter-relative-time` | ✅ | Universal-tier `Intl.RelativeTimeFormat` adapter. Locale-aware relative-time strings ("3 days ago", "in 2 hours"), plugged into `defaultFormatBridge`. |
|
||||
| `@suaveplan/i18n-formatter-segmenter` | ✅ | Universal-tier `Intl.Segmenter` adapter exposing grapheme / word / sentence iteration, plugged into `defaultFormatBridge`. |
|
||||
| `@suaveplan/i18n-icu-parser` | ✅ | Universal-tier ICU MessageFormat parser carved out of the i18n core (Wave 6). Tokenizer + AST builder for ICU syntax (plural, select, selectordinal, number/date/time formatters, nested placeholders), pluralization helpers, interpolation engine, integrates with the per-formatter packages via the `defaultFormatBridge`. |
|
||||
| `@suaveplan/i18n-shared` | ✅ | Shared runtime atoms for the i18n family — locale constants, BCP-47 normalisation map, RTL set, locale descriptors, interpolation defaults, the `I18N_CODES` registry, the `I18nError` base class, AND (Wave 5a) the `runBackendConformance(...)` shared harness under the `/testing` subpath consumed by every `@suaveplan/i18n-backend-*` package. |
|
||||
| `@suaveplan/locale-data` | ✅ | Static ISO 3166-1 country data and ISO 639-1 language data with typed lookup fu… |
|
||||
| `@suaveplan/react-i18n` | ✅ | React bindings for `@suaveplan/i18n`: `<TranslationProvider>`, 14 hooks (incl. `useFormatter`, `useLocaleAdapter`), 4 components, `withTranslation` HOC, 9-detector chain + `useLocaleDetection`, branded `TranslationKey` typing, `tDynamic`, `REACT_I18N_CODES` (12 codes), OTel telemetry. Plugin substrate `createReactI18nRegistry` + `<RegistryProvider>`: four plugin kinds (detector / formatter / post-processor / locale-adapter) with idempotent registration, schema validation, disposable lifecycle. Six built-in formatter plugins auto-register; detector chain accepts plugin splice by priority; post-processor pipeline runs in `<Trans>` after ICU. 408 tests, 100% coverage, 1238-test downstream sweep green. Plugin substrate shipped 2026-05-10 (`react-i18n-plugin-registry-and-ui-primitives` archived). |
|
||||
|
||||
### `image`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/caddy-imgproxy` | ✅ | Universal-tier TypeScript port of the Caddy imgproxy URL rewriter — compact-grammar parser, aspect / pixel dimension resolver, gravity + operation translator, HMAC-SHA256 URL signer, full handler with Zod-validated config and OTel telemetry. |
|
||||
| `@suaveplan/compress-image` | ✅ | Runtime-agnostic image compression utilities: resize, quality reduction, format… |
|
||||
| `@suaveplan/emblem-solver` | ✅ | Minimal-rectangle SVG converter for pixelated sprite... |
|
||||
| `@suaveplan/exif` | ✅ | Dependency-free, runtime-agnostic EXIF/XMP/IPTC metadata reader, writer, and st… |
|
||||
| `@suaveplan/image` | ✅ | Universal-tier image metadata extraction, format detection, resize math, crop validation, and transform pipeline planning. |
|
||||
| `@suaveplan/colours` | ✅ | Universal-tier colour manipulation: parsing, conversion, analysis, mixing, blending, and palette generation. |
|
||||
| `@suaveplan/colour-convert-hsluv` | ✅ | HSLuv perceptually uniform colour space with gamut mapping and uniform harmonies. |
|
||||
| `@suaveplan/colour-convert-cieluv` | ✅ | CIELUV, xyY, LMS cone response, and Bradford chromatic adaptation with CIE standard illuminants. |
|
||||
| `@suaveplan/colour-filter-cmc` | ✅ | CMC l:c Delta-E colour difference metric for textile and print quality control. |
|
||||
| `@suaveplan/colour-filter-cct` | ✅ | Correlated Colour Temperature (CCT) calculation and approximation from RGB. |
|
||||
| `@suaveplan/image-quantisation` | ✅ | Image complexity analysis and adaptive colour quantisation with pool detection, spatial-aware AA cleanup, and multiple quantisation algorithms. |
|
||||
| `@suaveplan/react-image` | – | Image components with lazy loading |
|
||||
|
||||
### `logistics`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/api-plugin-customs` | – | Cross-border customs — declarations, HS codes, borde... |
|
||||
| `@suaveplan/api-plugin-logistics` | – | Enterprise logistics — shipments, carriers, pallets,... |
|
||||
| `@suaveplan/api-plugin-port-tracking` | – | Port tracking — vessel positions, port calls, shippi... |
|
||||
| `@suaveplan/api-plugin-procurement` | – | Procurement — purchase orders, goods receipt notes, ... |
|
||||
| `@suaveplan/api-plugin-warehouse-ops` | – | Warehouse operations — pick/pack/ship, cycle counts,... |
|
||||
| `@suaveplan/delivery-tracking` | ✅ | Channel-agnostic delivery event tracking |
|
||||
| `@suaveplan/ports` | ✅ | Universal-tier global port database with shipping-lane routing and ETA estimation. Curated `Port` registry (~100 sea ports across 5 regions + ~30 cargo airports + ~30 rail freight terminals), UN/LOCODE lookups (`findPort` / `findPortStrict`), country / type / name filters, proximity search (`findNearestPort`, `findPortsInRadius`) via a 5°-bucketed `PortSpatialIndex`, curated `shippingLanes` waypoint graph, lane distance via `@suaveplan/geo` great-circle (`estimateLaneDistance`, `cumulativeDistances`), lane projection (`findPositionOnLane`), ETA estimation (`estimateETA`) with injectable `Clock` and three-tier confidence band. Eight `PORTS.*` error codes + 8 typed `PortsError` subclasses, Zod schemas for every public type, 3 OTel counters + 3 histograms, 99 tests at 100/100/100/100 coverage. Deps: `@suaveplan/error`, `@suaveplan/geo`, `@suaveplan/types`, `@opentelemetry/api`, `zod`. |
|
||||
| `@suaveplan/react-port-tracking` | – | Port tracking components and hooks |
|
||||
|
||||
### `messaging`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/api-plugin-messaging` | – | Messaging plugin |
|
||||
| `@suaveplan/api-plugin-notifications` | ✅ | Notifications plugin |
|
||||
| `@suaveplan/discord-client` | ✅ | Generic Discord API client for webhooks, bot messaging, and embeds. 62 tests, 100% coverage. Shipped parallel session. |
|
||||
| `@suaveplan/imessage-adapter` | ✅ | macOS iMessage SQLite chat.db watcher + `MessageSourceAdapter` projection. Zod-validated config boundary, AbortSignal-aware polling, monotonic `lastRowId` cursor (settable + readable for resume), injectable `SqliteReader` (production `bunSqliteReader` via lazy `createRequire(import.meta.url)("bun:sqlite")`, tests via real chat.db fixture — no mocks), typed `EventBus<IMessageWatcherEvents>` surface beside legacy `on/off`, 10 `IMESSAGE.*` codes (`DB_OPEN_FAILED`, `DB_QUERY_FAILED`, `WATCHER_NOT_RUNNING`, `WATCHER_ALREADY_RUNNING`, `ADAPTER_NOT_INITIALIZED`, `INVALID_CONFIG`, `POLL_ABORTED`, `POLL_FAILED`, `ATTACHMENT_PARSE_FAILED`, `APPLE_DATE_PARSE_FAILED`), 4 OTel instruments (`messages_emitted_total{chat_type,from_me}`, `attachments_emitted_total{mime_family}`, `poll_errors_total{code}`, `poll_duration_ms{outcome}`). 89 tests, 100% line coverage on every package file. |
|
||||
| `@suaveplan/react-notifications` | – | Toast/notification system |
|
||||
| `@suaveplan/slack-client` | ✅ | Generic Slack API client for messaging, channels, and webhooks |
|
||||
| `@suaveplan/sms` | ✅ | SMS send/receive, delivery status tracking, template... |
|
||||
| `@suaveplan/sms-providers` | ✅ | Universal-tier SMS provider implementations for `@suaveplan/sms`: Twilio (HTTP Basic + form + X-Twilio-Signature HMAC-SHA1), Vonage (form auth + HMAC-SHA256), MessageBird (AccessKey + JWT HS256 with iat/exp/nbf window), Plivo (Basic + X-Plivo-Signature-V3 HMAC-SHA256), AWS SNS (inline SigV4 + RSA-SHA1 SNS verifier with injectable callback). `ProviderRegistry`, `httpRequest` over injectable `FetchLike`, `composeAbortSignal`, `defineCodes("SMS_PROVIDERS", [8])` + `SmsProvidersError`, per-provider `mapStatus`/`parseInbound`/`verifyWebhookSignature`, `parseOptOutKeyword` (CTIA/TCPA STOP/UNSTOP/HELP), five OTel instruments. 236 tests inc. real `node:http` round-trip per provider + real `node:crypto` HMAC + RFC 4231/2202 test vectors + RFC SHA-256 fixtures + targeted branch-coverage suites for conditional-spread arms and default-config paths. 100/100/100/100 across statements/branches/functions/lines. |
|
||||
| `@suaveplan/telegram-client` | ✅ | Generic Telegram Bot API client for messaging, groups, and media |
|
||||
|
||||
### `monitor`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/audit-logging` | ✅ | Tamper-proof, GDPR-compliant audit logging: cryptographic chain-integrity, plug… |
|
||||
| `@suaveplan/logger` | ✅ | Foundation logging package — structured JSON output, level filtering, redaction… |
|
||||
| `@suaveplan/opentelemetry` | ✅ | OpenTelemetry SDK setup and configuration helpers: tracer/meter/logger provider… |
|
||||
|
||||
### `network`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/network` | ✅ | Core network interfaces: NetworkNode, NetworkPlugin, NetworkBus, MessageEnvelop… |
|
||||
| `@suaveplan/network-bootstrap` | ✅ | Pluggable peer-discovery chain + TTL peer cache + `BootstrapPlugin`/`BootstrapMiddleware`; injectable `Clock`, `@suaveplan/retry` backoff+jitter, NetworkBus events, opt-in `PeerCacheStorageLike`, NET_BOOTSTRAP error registry, Zod boundaries, OTel surface (span + 2 counters + duration histogram), AbortSignal everywhere. 67 tests, 100/100/100/100. |
|
||||
| `@suaveplan/network-discovery-crdt` | ✅ | Browser-tier CRDT-awareness `DiscoveryStrategy` for `@suaveplan/network-bootstrap`. Construction-time provider selection (Yjs OR Automerge) via single `CRDTProviderAdapter` — no runtime swap. Injectable `yjsDocFactory` / `automergeRepoFactory`; `signaling` URL list REQUIRED (no `wss://signaling.yjs.dev` default). 7-code `NET_DISC_CRDT` registry, Zod boundaries, OTel surface (start span + per-provider sync span + counter + histogram), AbortSignal everywhere. 92 tests (16 Yjs + 13 Automerge + 32 orchestrator + property suite), 100/100/100/100; FakeYjsBroker/FakeAutomergeBroker real impls — no mocks. |
|
||||
| `@suaveplan/network-discovery-gundb` | ✅ | Browser-tier GunDB gossip `DiscoveryStrategy` with **enterprise security uplift over upstream's flagged-fragile audit**: mandatory `peers` list, Ed25519-signed peer-announces (unsigned/tampered silently dropped), Zod wire validation, constant-time peer-ID compare. **+ ships `Transport` + `NetworkPlugin` via `./transport` + `./plugin` subpaths** (Wave 3.5 fold) — every `transport.send()` flows through `signEnvelope`; every inbound payload gated by Zod parse → verify → senderPeerId match → recipient match BEFORE caller handler. Same `gundb.invalid_messages_total` + `gundb.signature_rejected_total` counters cover transport tier. 11-code `NET_DISC_GUNDB` registry (8 discovery + 3 transport). 114 tests (67 discovery + 47 transport/signing/plugin), 100/100/100/100. |
|
||||
| `@suaveplan/network-discovery-hybrid` | ✅ | Browser-tier multi-strategy `DiscoveryStrategy` orchestrator over the five Wave-2 siblings (peerjs/crdt/trystero/webtorrent/gundb), all declared as optional peer-deps with graceful degradation via `STRATEGY_LOAD_FAILED`. Five-module composer: `HybridDiscoveryManager` + `StrategyManager` (health/priority) + `Lifecycle` (start/stop FSM) + `PeerTracker` (deduped peer set with `PeerSource` provenance) + `StrategyLoader` (dynamic `import()`-based lazy resolution). **Banned-mock removal**: upstream `__mocks__/strategy-mock.ts` explicitly dropped; in-package `TestStrategy` deterministic real fixture under `tests/setup/`. Zero `vi.mock`/`jest.mock`/`__mocks__/`/`: any` anywhere. 6-code `NET_DISC_HYBRID` registry, Zod boundaries, OTel surface incl. `hybrid.fallback_total{from,to}` + `hybrid.active_strategies` gauge. 89 tests across 7 files (4 integration), 100/100/100/100. |
|
||||
| `@suaveplan/network-discovery-peerjs` | ✅ | Browser-tier PeerJS `DiscoveryStrategy` for `@suaveplan/network-bootstrap`. Mandatory `host` + injectable `peerFactory` (no public-`peerjs.com` fallback). **+ ships `Transport` + `NetworkPlugin` via `./transport` + `./plugin` subpaths** (Wave 3.2 fold) — lazy peer init, per-peer `DataConnectionLike` registry over wire frame `{kind: "suaveplan.transport.v1", envelope}` that discovery's Zod schema rejects so transport + discovery share one PeerJS unit cleanly. 10-code `NET_DISC_PEERJS` registry (5 discovery + 5 transport), OTel adds `peerjs_transport.*` instruments. 145 tests (101 discovery + 44 transport/plugin/otel), 100/100/100/100. |
|
||||
| `@suaveplan/network-discovery-trystero` | ✅ | Browser-tier Trystero `DiscoveryStrategy` with 4 transports (BitTorrent / Nostr / MQTT / IPFS) behind single `TrysteroTransportAdapter` interface. Mandatory non-empty relay lists; auto-fallback via `@suaveplan/retry`. **+ ships `Transport` + `NetworkPlugin` via `./transport` + `./plugin` subpaths** (Wave 3.3 fold) — `TrysteroNodeTransport` (renamed to avoid string-union collision) over `"envelope"` action namespace, factory resolution for all 4 transport kinds, room-leave auto-prune. 14-code `NET_DISC_TRYSTERO` registry (10 discovery + 4 transport). 150 tests (118 discovery + 32 transport/plugin), 100/100/100/100. |
|
||||
| `@suaveplan/network-discovery-webtorrent` | ✅ | Browser-tier WebTorrent-tracker `DiscoveryStrategy` via `p2pt` for `@suaveplan/network-bootstrap`. Mandatory `.min(1)` tracker list (no public-tracker fallback survives). Injectable `p2ptFactory` + `FakeP2PT` real impl (no mocks). **+ ships `Transport` + `NetworkPlugin` via `./transport` + `./plugin` subpaths** (Wave 3.4 fold) — per-peer registry, lazy p2pt instantiation, AbortSignal-aware, 30 s default `CONNECT_TIMEOUT`. 13-code `NET_DISC_WEBTORRENT` registry (5 discovery + 8 transport), OTel adds `webtorrent_transport.*` instruments. 96 tests (46 discovery + 50 transport/plugin/otel), 100/100/100/100. |
|
||||
| `@suaveplan/network-gundb-server` | ✅ | Server-tier (Node-only) GunDB relay daemon paired with `@suaveplan/network-discovery-gundb` ✅ clients. Idempotent `start({signal?})` / `stop()` lifecycle; injectable `gunFactory` + `FakeGun` real impl (no mocks). Inline `/health` `/ready` `/live` JSON endpoints. Honours client's wire-format security model: Zod-validates inbound announces against shared `GunDBPeerAnnounceSchema` (single source of truth) and silently drops malformed/wrong-kind/missing-sig/missing-key with `gundb_server.invalid_messages_total{reason}`. Signature verification deliberately at client (gossip-security model). Real-server-boot integration test on port 0. 6-code `NET_GUNDB_SERVER` registry, OTel surface. 56 tests, 100/100/100/100. |
|
||||
| `@suaveplan/network-libp2p-server` | ✅ | Server-tier libp2p bootstrap + circuit-relay-v2 daemon paired with `@suaveplan/network-transport-libp2p` ✅ clients. Idempotent `start({signal?}) → {peerId, multiaddrs[]}` / `stop()` lifecycle; required injectable `libp2pFactory` (no native deps on test surface). Inline `/health`/`/ready`/`/live` HTTP probes. TCP + WebSockets listen, Noise + Yamux + Identify + Bootstrap, autoRelay enabled. 6-code `NET_LIBP2P_SERVER` registry, OTel (span + 3 counters + gauge + histogram). All discipline metrics clean — 299 LOC max source, 384 LOC max test, zero `:any`/mocks/React. 4-scenario in-process integration (health probes + client dial + 2-client autoRelay stream + graceful stop). 59 tests, 100/100/100/100. |
|
||||
| `@suaveplan/network-plugin-assignment` | ✅ | Mesh auto-placement: MeshCapacityTracker (live peer-event consumption), AssignmentEngine (weighted multi-criteria scoring across latency/capacity/load/region/tier-fit), MeshBalancer (rebalance planning), AdmissionController (policy-gated admission). |
|
||||
| `@suaveplan/network-plugin-audit-mesh` | ✅ | Signed append-only audit logs across the mesh: hash-chained event records, host-signed entries, cross-host replication for durability, server reconciliation on reconnection, compaction and conflict resolution. |
|
||||
| `@suaveplan/network-plugin-auth` | ✅ | Mesh membership auth + capability tokens: MeshMembershipVerifier (join authorization via auth provider), CapabilityTokenIssuer (tier-bound Ed25519-signed tokens), CrossMeshTrustValidator (relay trust relations). |
|
||||
| `@suaveplan/network-plugin-circuit-breaker` | ✅ | Mesh-boundary circuit breaking: per-path MeshCircuitBreaker FSM (CLOSED→OPEN→HALF_OPEN), fleet-wide registry, half-open probe with bounded test traffic, relay-integration middleware. |
|
||||
| `@suaveplan/network-plugin-content-delivery` | ✅ | P2P file-chunk distribution over the mesh: chunk redundancy planner, integrity verifier, latency-aware routing, plugin wrapper bridging `@suaveplan/drive` chunks across WebRTC peers. (Sub-changes for packer/prefetch/store/throttle remain open.) |
|
||||
| `@suaveplan/network-plugin-crypto-messages` | ✅ | Cryptographic message integrity: per-message Ed25519 signing/verify, peer identity verification via challenge-response, server-signed session/mesh tokens, key rotation, hash-chained log integrity. |
|
||||
| `@suaveplan/network-plugin-devtools` | ✅ | Developer-tools plugin: NetworkVisualizer (hierarchy/flow/heat IR + injectable Renderer + bundled SVG renderer), StateInspector (per-path ring + RFC-6902 diff via @suaveplan/diff + scrubber + composable search), SimulationConfigTool (programmatic peer/link/condition/event editor with deterministic recording, atMs-grouped playback, Zod-validated JSON round-trip), PerformanceProfiler (p50/p95/p99 latency distributions + throughput + plugin overhead + memory tracking + named baselines + Z-score regression detection + categorised optimisation suggestions), and DevToolsPlugin facade with tier gate and `requireActive()` PLUGIN_DEV_DISABLED guard. 111 tests, 100/100/100/100 coverage, OTel counters across every subsystem. |
|
||||
| `@suaveplan/network-plugin-failover` | ✅ | Mesh failover orchestration: HeartbeatManager, HealthMonitor (alive→suspect→dead), debounced PromoterManager, FailoverCoordinator failure-transition, FailoverPlugin wrapper. |
|
||||
| `@suaveplan/network-plugin-governance` | ✅ | Host elections + reputation: ElectionEngine (metric/direct/hybrid modes), CandidateScorer (weighted multi-criteria), ReputationTracker, VotingManager, TransitionManager handover FSM, GovernancePlugin wrapper. |
|
||||
| `@suaveplan/network-plugin-intra-relay` | ✅ | Intra-mesh relay routing: connection manager, peer discovery, envelope encoding, loop guard, relay monitor, peer selector, plugin wrapper. (Security extension proposal still open.) |
|
||||
| `@suaveplan/network-plugin-middleware` | ✅ | Pluggable middleware pipeline for the network bus: 12 modules covering throttling, validation, observability, error handling, transformation, and lifecycle hooks; 85 conformance tests. |
|
||||
| `@suaveplan/network-plugin-migration` | ✅ | Peer state migration between meshes: MigrationCoordinator (initiate→pack→transfer→drain), StateTransferProtocol (snapshot serialize/deserialize), ForwardingTunnel (buffer + drain). Adapters intentionally deferred to dedicated `migration-adapter-*` packages. |
|
||||
| `@suaveplan/network-plugin-monitor` | ✅ | Network health + telemetry monitoring: peer-quality scorers, RTT/loss collectors, alert thresholds, OTel histogram + counter wiring. **+ Wave 6.1 fold of `@suaveplan/network-observability`** — universal-tier Prometheus exposition emitter (hand-rolled `0.0.4` text format from `MetricCollector.scrape()`, sanitised metric/label names + escaped label values, counter/gauge/histogram/summary support with configurable quantiles, optional timestamps, pinned `Content-Type`; no `prom-client` dep) and Grafana dashboard JSON generator (auto-layout, injectable `Rng` for deterministic UIDs, Zod-validated panel options, pinned Prometheus datasource binding, `schemaVersion: 38`). **+ Wave 6.2 fold of `@suaveplan/network-quality`** — universal-tier quality-class classification + adaptive degradation: `BandwidthMonitor` (single-pole EMA over bandwidth/RTT/loss, bounded ring buffer with configurable `maxSamples` + `smoothingFactor` validation, injectable `clock`, listener subscription returning unsubscribe fn), `DegradationManager` (4-rung ladder `MINIMAL=0 < LOW=1 < MEDIUM=2 < HIGH=3` with one-step transitions, candidate-must-persist `hysteresisMs` debounce — `0` commits same-call — over windowed averages, `forceLevel`/`reset` overrides with `"forced"`/`"reset"`/`"conditions-changed"` change-reasons, injectable `clock`), `createQualityMiddleware<M>(dm, options?)` (outgoing-message gate with per-`source` counters, HIGH/MEDIUM/LOW/MINIMAL drop policies, customisable critical-type allowlist + position-update set, identity-preserving pass-or-`null`-drop API), full `QualityLevel`/`BandwidthSample`/`QualityThresholds`/`LevelThreshold`/`QualityChangeEvent` Zod-validated types. 166 tests, 100/100/100/100. |
|
||||
| `@suaveplan/network-plugin-partition` | ✅ | Split-brain detection + healing: PartitionDetector (peer reachability matrix), QuorumResolver (majority/weighted quorum policies), MergeProtocol (post-partition reconciliation). |
|
||||
| `@suaveplan/network-plugin-protocol` | ✅ | Network protocol plugin: 13-byte binary header, message encoder/decoder, pipeli… |
|
||||
| `@suaveplan/network-plugin-qos` | ✅ | Per-mesh QoS: BandwidthQuotaEnforcer, MessagePriorityClassifier, CongestionMonitor, qosRouter prioritising control-plane traffic over bulk. |
|
||||
| `@suaveplan/network-plugin-registry` | ✅ | Fixed-capacity peer registry with bitpacker-based binary message encoding |
|
||||
| `@suaveplan/network-plugin-relay` | ✅ | Cross-mesh federation: RelayRouter, HostBackboneTransport, CrossMeshEnvelope, MeshDirectory, LoopGuard, RelayManager wiring. |
|
||||
| `@suaveplan/network-plugin-server-bridge` | ✅ | Tier-0 host ↔ central-server bridge: ServerGateway (transport-agnostic, 2× heartbeat disconnect detection), GracefulDegradationFsm (online → degrading → offline → recovering → online), OfflineModeCoordinator (autonomous mode + token-grace extension), LogBuffer (circular signed-log replay queue with hash-dedup), ReconciliationProtocol (server-watermark differential with LWW conflict resolution), CommandDispatcher (Ed25519 + nonce-replay protection), HeartbeatMultiplexer (Tier-0 batch forwarding), four hosting-model coordinators (PURE_CLIENT Byzantine quorum, SERVER_BASED, SERVER_TO_CLIENT_FAILOVER, CLIENT_WITH_SERVER_BACKUP), and ServerBridgePlugin facade with tier gate. 156 tests, 100/100/100/100 coverage. |
|
||||
| `@suaveplan/network-plugin-session` | ✅ | Hierarchical session management: SessionTree, SessionManager, SessionRouter, per-session host election, transition FSM, parent-state bridge, capacity enforcer, mesh-wide registry, and SessionPlugin wrapper. |
|
||||
| `@suaveplan/network-plugin-simulation` | ✅ | Mesh simulation harness: latency/loss/jitter injection, deterministic clock + RNG, scripted peer scenarios for failover/governance/partition tests across 11 modules. |
|
||||
| `@suaveplan/network-plugin-state` | ✅ | Mesh-state coordination: shared key-value state with vector-clock conflict resolution, snapshot + delta sync, persistent backing, observable state subscriptions across 14 modules. |
|
||||
| `@suaveplan/network-plugin-topology` | ✅ | Mesh topology plugin: tiered hierarchy (Tier 0 Host → Tier 1 SubHost → Tier 2 Leaf), latency-grouped neighbours, group manager, plugin wrapper. |
|
||||
| `@suaveplan/network-plugin-transport-manager` | ✅ | Multi-transport orchestration: pool registry, capability negotiation, 5-tier priority routing (CRITICAL/HIGH/NORMAL/LOW/BULK) with redundant CRITICAL delivery, round-robin balancing, health monitor with auto-pause/resume, EMA quality-trend analyzer + pre-emptive selector with smooth handoff, session persistence buffer, prioritized connection recovery, continuity guard, battery-aware scheduler, bandwidth conservator. |
|
||||
| `@suaveplan/network-testing` | ✅ | Network testing utilities |
|
||||
| `@suaveplan/network-transport-hyperswarm` | ✅ | Server-tier (Node-only) `Transport` + `NetworkPlugin` over Hyperswarm DHT. Required injectable `hyperswarmFactory` (never imports `hyperswarm` directly — keeps native deps off the unit-test surface). Per-peer connection registry, hole-punched DHT discovery, Ed25519-signed peer announces with Zod wire-payload validation + defensive drop. Reputation store with monotonicity property test. `@suaveplan/serialize` length-prefix wire format. 10-code `NET_HYPERSWARM` registry, OTel (2 counters + 2 histograms + active-peers gauge), AbortSignal everywhere. 3-peer in-process integration test (`FakeSwarmHub`) green; all-to-all envelope exchange + disconnect propagation verified. 94 tests effectively 100% net-new (upstream was 8.98%). 100/100/100/100. |
|
||||
| `@suaveplan/network-transport-libp2p` | ✅ | Universal-tier `Transport` + `NetworkPlugin` over the libp2p stack (websockets + webrtc sub-transports; tcp/mdns deferred to a future server-tier sibling). Required injectable `libp2pFactory` — package never imports `libp2p` at module-eval (keeps 12 subpackage native deps off the unit-test surface). **Mandatory `bootstrap` list** — no public-bootstrap fallback survives. 11-code `NET_LIBP2P` registry, OTel (3 counters + 2 histograms + active-peers gauge), AbortSignal everywhere. 9-module architecture, every file ≤300 LOC (max 290), every test file ≤500 LOC. **Zero React imports / `:any` / mocks** in the package. 2-peer integration tests for both websockets and webrtc sub-transports via `FakeLibp2pHub`. 749-LOC upstream demo extracted to Wave 7 (`react-network-libp2p-demo`). 74 tests, 100/100/100/100. |
|
||||
| `@suaveplan/network-transport-standby` | ✅ | Pre-emptive WebRTC standby pool implementing `Transport`+`NetworkPlugin` from `@suaveplan/network`. Min/max slot pool + EMA health scoring (RTT/jitter/loss) + auto-reconnect FSM via `@suaveplan/retry` exponential backoff + equal jitter. Warm-up via `@suaveplan/timing` Scheduler + injectable `Clock`. Injectable `RtcConnectionFactory` + `FakeRtcConnection` real impl (no mocks). 6-code `NET_STANDBY` registry, Zod cross-field invariants, OTel (gauge + 2 counters + histogram). 102 tests; warm-to-active < 100 ms integration test green. fast-check verifies `warmCount + activeCount ≤ maxSlots`, eviction monotonicity, promotion atomicity, health bounds. (Renamed from upstream `network-standby-transport` per network-transport-family proposal.) 100/100/100/100. |
|
||||
| `@suaveplan/network-transport-webrtc` | ✅ | WebRTC transport plugin for mesh networking: RTCPeerConnection data-channel tra… |
|
||||
| `@suaveplan/network-transport-websockets` | ✅ | WebSocket `Transport` + `NetworkPlugin` per `@suaveplan/network` contract on top of existing `WebSocketClient` (typed framing, heartbeat, backpressure queue, injectable adapter). Per-peer registry with duplicate-id rejection, `AbortSignal` on connect, fan-in `onMessage`, idempotent close. Extended `defineCodes("NET_WS", [...])` with `PEER_ALREADY_CONNECTED`/`PEER_UNKNOWN`/`TRANSPORT_CLOSED`/`MAX_PEERS_EXCEEDED`. OTel spans + counters + active-peers gauge. 88 tests (60 baseline + 28 new conformance/edge), 100% coverage. |
|
||||
| `@suaveplan/network-trystero-server` | ✅ | Server-tier Trystero signaling daemon paired with `@suaveplan/network-discovery-trystero` ✅ clients. Construction-time `mode: "mqtt"\|"nostr"` via single `BrokerLike` adapter (no runtime branching). Required injectable `mqttBrokerFactory`/`nostrRelayFactory` — production passes real WS broker shims, tests bind `FakeBroker` real impl. Idempotent `start({signal?})/stop()` lifecycle, inline `/health`/`/ready`/`/live`. 6-code `NET_TRYSTERO_SERVER` registry, OTel mode-labelled (span + 2 counters + gauge + histogram). Both modes integration-tested end-to-end (boot → health → published relayUrl round-trip → 2-client connect/frame/disconnect). 298 LOC max source, 413 LOC max test, zero `:any`/mocks. Concrete production broker shims deferred to future `network-trystero-server-broker-{mqtt,nostr}`. 69 tests, 100/100/100/100. |
|
||||
| `@suaveplan/react-network-libp2p-demo` | ✅ | **Net-new** React demo for `@suaveplan/network-transport-libp2p` ✅, extracted from upstream's 749-LOC `libp2p-demo.tsx` per the tier-isolation rule. 6 subcomponents (`<Libp2pDemo>` root composer, `<NodeStatusPanel>`, `<NodeControls>`, `<PeerInteractionPanel>`, `<PeerList>`, `<LogPanel>`), each with sibling `.tsx`/`.md`/`.test.tsx`/`.stories.tsx`. `useLibp2pDemo` hook + `wirePeerEvents` helper. Bootstrap list is a **required** React prop (no hardcoded defaults). Consumes structural `Libp2pDemoNodeFactory` adapter — package never imports `libp2p`. 5-code `REACT_LIBP2P_DEMO_CODES` registry, Zod boundaries on props. Discipline: max source 263 LOC, max test 495 LOC, zero `:any`/mocks. 72 tests across 10 files, 100/100/100/100. |
|
||||
| `@suaveplan/service-discovery` | ✅ | Universal `ServiceRegistry` with TTL/expiry, AbortSignal-aware `watch()`, optional storage persistence + `EventBusLike` fan-out; 100% coverage. |
|
||||
| `@suaveplan/websocket-gateway` | ✅ | Server-tier WebSocket gateway hosting rooms + presence + channels. Idempotent `start({signal?}) → {address, port}` / `stop()` lifecycle. Required injectable `wsServerFactory` (production → real `ws.WebSocketServer`, tests → `FakeWsServer`/`RealWsServer` adapters; no mocks). 4 callback hooks for unmigrated deps (`authVerifier`/`authorizationCheck`/`rateLimitGate`/`auditLogger`) — default no-op, README documents replacement paths to `jwt-tenant`/`permissions`/`rate-limiter`/`audit-logging` when shipped. Direct integrations: `@suaveplan/idempotency` (dedup on mutating events), `@suaveplan/retry`, `@suaveplan/webhook-client` (signed presence-event delivery). 9-code `WS_GATEWAY` registry, OTel surface. **3-client real-`ws` integration test** verifies room broadcast + health endpoints + shutdown. 125 tests, 100/100/100/100. (Note: `gateway/gateway.ts` is 332 LOC — 32 over CLAUDE.md hard cap as a documented facade-pattern exception; 7 subsystems already factored out cleanly.) |
|
||||
|
||||
### `platform`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/analytics-browser` | ✅ | Browser-tier client-side analytics: `AnalyticsTracker` + `BatchTransport` (AbortController + timeoutMs + 4xx short-circuit + exponential backoff) + `BeaconTransport` (sendBeacon + fetch keepalive) + `SessionManager`. Consent gate (`AllowAllPolicy`/`DoNotTrackPolicy`), `HeadSampler` with per-event overrides, PII scrubber (redact/hash/drop on dotted paths), `MemoryStorageBackend` + `WebStorageBackend` (maxBytes watermark). Zod-validated `AnalyticsEventSchema`. Eight-code `ANALYTICS_BROWSER.*` registry. Five OTel instruments. Typed `EventBus<AnalyticsEventMap>` lifecycle bus. 114 tests, 100/100/100/100. |
|
||||
| `@suaveplan/audience-rules` | ✅ | Dynamic audience segmentation: predicate library, segment evaluator + refresher, Zod-validated store, typed errors, OTel. |
|
||||
| `@suaveplan/campaigns` | ✅ | Campaign orchestration: 9-transition state machine, batch planner, 6-step workflow runner, CampaignStore + InMemory impl, audience+template+channel+schedule |
|
||||
| `@suaveplan/etl` | ✅ | Extract-Transform-Load pipeline engine with typed stages, error handling, obser… |
|
||||
| `@suaveplan/feature-flags` | ✅ | Provider-agnostic feature flag evaluation with typed flag values, strategy chai… |
|
||||
| `@suaveplan/health-check` | ✅ | /health, /ready, /live with dependency checks (HTTP, TCP, custom); injectable fetch/connect; OTel duration histogram + status counters |
|
||||
| `@suaveplan/notification-channel` | ✅ | Multi-channel notification routing with templates, r... |
|
||||
| `@suaveplan/pwa` | ✅ | Browser-tier PWA: SW lifecycle manager, caching strategies + router, sync queue (IndexedDB/localStorage/memory) with retry + schema migration, push subscriptions, install-prompt, manifest builder, cross-tab update controller, OTel counters/histograms |
|
||||
| `@suaveplan/search` | ✅ | Full-text and fuzzy search primitives with in-memory BM25 engine, trigram fuzzy… |
|
||||
| `@suaveplan/webhook-client` | ✅ | Webhook delivery client with retry logic, HMAC signature generation/verificatio… |
|
||||
|
||||
### `r3f`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/r3f-animation` | – | Animation state machine, blend trees, layers |
|
||||
| `@suaveplan/r3f-booking` | – | Interactive 3D venue maps for booking |
|
||||
| `@suaveplan/r3f-building` | – | R3F building mode with snap system, ghost preview, and placement validation |
|
||||
| `@suaveplan/r3f-buildings` | – | Instanced 3D building and road rendering for R3F |
|
||||
| `@suaveplan/r3f-camera` | – | Advanced camera controls for React Three Fiber |
|
||||
| `@suaveplan/r3f-character` | – | Character controller for 3D games |
|
||||
| `@suaveplan/r3f-character-vehicle` | – | Character-vehicle integration (enter/exit, authority transfer) |
|
||||
| `@suaveplan/r3f-character-vehicle-picker` | – | Character/vehicle picker UI with 3D previews, GTA-style customisation |
|
||||
| `@suaveplan/r3f-combat` | – | R3F rendering layer for combat with hitbox debug, projectile trails, muzzle flash, impact effects, and damage numbers |
|
||||
| `@suaveplan/r3f-controller` | – | Input controller for 3D scenes |
|
||||
| `@suaveplan/r3f-earth` | – | 3D globe with port markers, shipping lanes |
|
||||
| `@suaveplan/r3f-emulator` | – | 3D device emulation interface |
|
||||
| `@suaveplan/r3f-environment` | – | Skybox, HDR, and lighting presets |
|
||||
| `@suaveplan/r3f-lod` | – | Level-of-detail management |
|
||||
| `@suaveplan/r3f-minimap` | – | Minimap renderer, entity tracking, fog-of-war |
|
||||
| `@suaveplan/r3f-networking` | ✅ | R3F-tier multiplayer state sync: frame-loop interpolation, client-side prediction with reconciliation FSM (composes `@suaveplan/fsm`), snapshot diff round-trip (composes `@suaveplan/diff`), snapshot interpolation, React hooks (`useNetworkSync`/`useNetworkedEntity`/`useNetworkPredict`) and R3F components (`<NetworkProvider>`/`<NetworkedEntity>`/`<NetworkStats>` with Storybook stories). Transport-agnostic: accepts any `Transport` from `@suaveplan/network` via injection; ships `MemoryTransport` deterministic real-impl for tests + canonical wiring docs for `network-transport-webrtc`. 5-code `R3F_NET` registry, OTel surface, AbortSignal everywhere. Property tests: interpolation monotonicity (200 runs), prediction-reconcile convergence (50 runs), snapshot diff round-trip equivalence (100 runs). 152 tests, 100/100/100/100. Max source 177 LOC, max test 184 LOC, zero `:any`/mocks. |
|
||||
| `@suaveplan/r3f-npc` | – | R3F NPC rendering with behavior tree debug, nameplates, and dialogue triggers |
|
||||
| `@suaveplan/r3f-number-plate` | – | 3D number plate component |
|
||||
| `@suaveplan/r3f-parallax` | – | Interior-parallax components (WindowParallax + GridParallax) |
|
||||
| `@suaveplan/r3f-particles` | – | GPU-accelerated particle systems (instanced) |
|
||||
| `@suaveplan/r3f-pathfinding` | – | NavMesh pathfinding, steering, crowd simulation |
|
||||
| `@suaveplan/r3f-performance` | – | Performance monitoring and optimization |
|
||||
| `@suaveplan/r3f-physics-cascade` | – | Cascading physics effects |
|
||||
| `@suaveplan/r3f-physics-core` | – | Physics engine abstraction layer |
|
||||
| `@suaveplan/r3f-physics-rapier` | – | Rapier physics engine integration |
|
||||
| `@suaveplan/r3f-shaders` | – | Custom shader effects and materials |
|
||||
| `@suaveplan/r3f-terrain` | – | Procedural terrain with chunked LOD |
|
||||
| `@suaveplan/r3f-ui` | – | HTML/React UI components in 3D space |
|
||||
| `@suaveplan/r3f-vegetation` | – | R3F procedural vegetation with biome rules, LOD, and instanced placement |
|
||||
| `@suaveplan/r3f-vehicle` | – | Vehicle controllers for React Three Fiber |
|
||||
| `@suaveplan/r3f-water` | – | Water rendering and ocean simulation |
|
||||
| `@suaveplan/r3f-weather` | – | R3F weather rendering with particles, fog, lightning, and surface wetness |
|
||||
| `@suaveplan/r3f-world` | – | World management and scene composition |
|
||||
| `@suaveplan/r3f-xr` | – | WebXR/VR/AR with controller and hand tracking |
|
||||
|
||||
### `random`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/fsm` | ✅ | Typed finite state machine with guards, actions, history tracking, hierarchical… |
|
||||
| `@suaveplan/noise` | ✅ | Seeded spatial noise fields: Perlin, Simplex, Value, Worley, fBm and Blue-noise… |
|
||||
| `@suaveplan/pseudo-random` | ✅ | Deterministic seeded PRNG implementations (Mulberry32, SFC32, Xoshiro256++, Cou… |
|
||||
| `@suaveplan/random` | ✅ | High-level CSPRNG-backed randomness: strings, UUID/ULID/Nanoid, collections, ob… |
|
||||
|
||||
### `react`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/react-charts` | – | Chart and data visualization components |
|
||||
| `@suaveplan/react-command-palette` | – | Cmd+K command palette with fuzzy search, keyboard na... |
|
||||
| `@suaveplan/react-consent-overlay` | – | Cookie/consent/privacy overlays |
|
||||
| `@suaveplan/react-data-table` | – | Data grid with sorting/filtering/pagination |
|
||||
| `@suaveplan/react-dnd` | – | Drag-and-drop with sortable lists |
|
||||
| `@suaveplan/react-error-boundary` | – | Error boundary with fallback UI |
|
||||
| `@suaveplan/react-hotkeys` | – | Keyboard shortcut management |
|
||||
| `@suaveplan/react-infinite-scroll` | – | Infinite scroll with IntersectionObserver |
|
||||
| `@suaveplan/react-iphone` | – | iPhone simulator with WebRTC support |
|
||||
| `@suaveplan/react-kanban` | – | Headless kanban board with drag-and-drop columns, sw... |
|
||||
| `@suaveplan/react-loading` | – | Loading screen with weighted task progress and tip rotation |
|
||||
| `@suaveplan/react-modal` | – | Modal/dialog system with focus trapping |
|
||||
| `@suaveplan/react-skeleton` | – | Skeleton/shimmer loading placeholders |
|
||||
| `@suaveplan/react-social` | – | Social library with real-time updates |
|
||||
| `@suaveplan/react-timeline` | – | Timeline visualization components |
|
||||
| `@suaveplan/react-tour` | – | Guided product tours and onboarding walkthroughs wit... |
|
||||
| `@suaveplan/react-tree-view` | – | Tree view component |
|
||||
| `@suaveplan/react-ui` | ✅ | Core React UI component library. ~70 shadcn-style components (Radix + cva + theme system + pack registry). i18n surface (capability spec `react-ui-i18n`): 7 formatted-value primitives (`<FormattedNumber>`, `<FormattedCurrency>`, `<FormattedDate>`, `<FormattedList>`, `<FormattedRelativeTime>`, `<Plural>`, `<FormattedMessage>`), `<ReactUiI18nBridge>` mounting `@radix-ui/react-direction` + `<html lang>`/`<html dir>` sync + `ReactUiLocaleContext`, `localeAwareSortingFn` / `localeAwareFilterFn` for tanstack tables, `formattedCell` shortcut for column cells, `<InputNumber>` (locale-aware decimal/grouping parse), `useValidationMessageBridge`, `<Drawer>` / `<Sheet>` `inline-start`/`inline-end` directions, `<Calendar>` auto-wires registry's `date-fns` adapter when present. Tooling: `bun run check:i18n-keys` (translation-key drift gate, MISSING fails / ORPHAN warns by default + `--strict` mode), `bun run check:registry-eject` (shadcn-eject smoke gate), 11 stories with consolidated snapshot test. 1368 tests pass. Initial i18n surface shipped 2026-05-10 (`react-i18n-plugin-registry-and-ui-primitives`); deferred tooling shipped 2026-05-10 (`react-ui-i18n-deferred-tooling`). |
|
||||
| `@suaveplan/react-ui-social` | – | Social UI components |
|
||||
| `@suaveplan/react-virtual` | – | Virtualized list/grid rendering |
|
||||
|
||||
### `security`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/hibp` | ✅ | Privacy-preserving Have I Been Pwned client: k-anonymity SHA-1 prefix range API (`Add-Padding: true`, NTLM mode via injectable adapter, constant-time suffix compare, byte-level no-leak suite), authenticated breach/paste API with 1500ms client-side rate-limit gate + 429 `Retry-After` honour, in-memory LRU cache (10k entries / 24h TTL) + pluggable `HibpCacheLike`, differential-privacy decoy-prefix padding, Zod boundary schemas at every response, 7 error codes, OTel histograms + outcome counters. 100% coverage. |
|
||||
| `@suaveplan/jws` | ✅ | **Impl complete** (43 `.ts` files / 2403 LOC / 24 sibling `.md` docs at 100% docs:check / 10 subpath exports / 18-code error registry). Biome / lint / typecheck / build GREEN. **Test pass + archive pending in separate session** — needs ~27 test files matching `**Verified by:**` paths in spec delta, RFC 7515 Appendix A.1–A.7 verbatim fixtures, x5c chain fixtures (consume `@suaveplan/x509`'s `build-certs.ts`), 100/100/100/100 Istanbul. Surface: RFC 7515 JSON Web Signature; compact + JSON-flat + JSON-general serializations; verify + sign across HS256/384/512, RS256/384/512, PS256/384/512, ES256/384/512, EdDSA (RFC 8037); JWK header parse, `x5c` embedded cert chain delegating to `@suaveplan/x509.validateChain`, `kid` resolver, `crit` allowlist enforcement with mandatory `b64` listing per RFC 7797 §3, `alg: "none"` two-flag explicit opt-in, RFC 7797 `b64: false` header, detached signatures (RFC 7515 §A.5), constant-time MAC compare for HMAC, ECDSA DER-form rejection (raw `r\|\|s` only). 12-step verify orchestrator with AbortSignal at three checkpoints. Gates webauthn-android-safetynet attestation. |
|
||||
| `@suaveplan/otp` | ✅ | RFC 4226 HOTP + RFC 6238 TOTP + RFC 6287 OCRA + Key URI + recovery codes (Vaulted) |
|
||||
| `@suaveplan/password-formats` | ✅ | Universal import/export layer for common password-manager export formats — Appl… |
|
||||
| `@suaveplan/password-generator` | ✅ | Cryptographically-strong password and passphrase generator: char-class mode (configurable length / classes / exclusions / required-class quotas), passphrase mode over bundled EFF Long (7776) + EFF Short 1 (1296) wordlists under tree-shakeable subpaths, pronounceable Markov-digram mode, PIN mode, bias-free `randomInt` per Lemire rejection sampling, `estimateEntropy` for both modes, `assertStrength(password, minBits)` guard, wordlist plugin API (≥256-entry minimum), injectable `Rng` via `@suaveplan/types`. χ² statistical test on 1M draws (p > 0.01). 100% coverage. |
|
||||
| `@suaveplan/password-strength` | ✅ | zxcvbn-style password strength estimator: 6 pattern matchers (dictionary with l33t+reverse, sequence, repeat, date, keyboard adjacency for QWERTY/DVORAK/AZERTY, recent-year regex), `mostGuessableMatchSequence` DP scorer with factorial penalty + bruteforce fallback, `estimateGuesses` per Wheeler §4 formulae, 4-scenario `crackTimes` with `adjustments` multiplier, 5-bucket score (literal `0\|1\|2\|3\|4`), feedback strings keyed for `@suaveplan/i18n`, `loadDictionaries` runtime swap, `userInputs` personal-info match, multilingual dicts under `./dicts/{de,fr,es,zh-Hans,ru}` subpaths, `assessReactive` streaming variant with AbortSignal, 200-char DoS guard. 100% coverage. |
|
||||
| `@suaveplan/security-codes` | ✅ | Detect and sanitise sensitive security-code leakage in strings: API keys, JWTs,… |
|
||||
| `@suaveplan/slsa` | ✅ | SLSA L1-L2 supply-chain security: provenance, SBOM, DSSE attestations, Sigstore… |
|
||||
| `@suaveplan/srp` | ✅ | SRP-6a (RFC 5054) zero-knowledge password proof: bundled 2048-bit safe-prime group at root + 1024/1536/3072/4096/6144/8192 under `./groups/<size>` subpaths (every constant byte-exact to Appendix A); SHA-256 default, SHA-1 legacy opt-in; client (`computeVerifier`, `clientStartHandshake`, `clientFinishHandshake`, `clientVerifyServerProof`) and server (`serverStartHandshake`, `serverFinishHandshake`, `serverComputeM2`) roles; reject `A mod N == 0` / `B mod N == 0` / `u == 0` per §2.5.4 with `SRP.PUBLIC_KEY_INVALID`; modpow via `@noble/curves/abstract/modular.pow` (never `BigInt ** BigInt %`); constant-time M1/M2/S compare via `@suaveplan/crypto-utils`; dual hash backends (`nobleHashBridge` sync + `subtleHashBridge` async + `defaultHashBridge` selector); `PakeProvider` interface for future OPAQUE drop-in; RFC 5054 Appendix B 1024/SHA-1 vectors verified byte-exact; locally-derived 2048/SHA-256 fixture via `scripts/generate-srp-vectors.ts`; 1000-triple property test asserts `client.K === server.K`. 100% coverage. |
|
||||
| `@suaveplan/tpm-struct` | ✅ | TPM 2.0 attestation-structure parser per TCG TPM 2.0 Library Part 2 r1.59: `parseTpmsAttest` covering all 8 `TPM_ST_ATTEST_*` variants (§10.12 + §10.11), `parseTpmtPublic` covering RSA / ECC / KEYEDHASH / SYMCIPHER (§12.2), byte-identical serializers, `computeTpmtPublicName` (§16) with injected `HashLike`, `extraDataMatches` (W3C WebAuthn L3 §8.3 step 8) with injected constant-time compare, `TPM_ALG` 35-entry + `TPM_ECC_CURVE` 9-entry frozen registries (§11.1 / §11.2.5), TPM2B length-prefix reader/writer, magic check `0xff544347` ("TCG\xff" reversed), `0 ⇒ 65537` RSA exponent normalization with `rawExponent` preservation, bounds enforcement → `TPM_STRUCT.TRUNCATED`, alg-unknown → `TPM_STRUCT.ALG_UNSUPPORTED`, debug-only `toDiagnostic` dumper, Zod boundary schemas at every public surface, 10-code error registry, OTel histograms + counters with bounded-enum attributes. Synthetic WebAuthn TPM integration fixture (no real-device byte string available). 100% coverage. |
|
||||
| `@suaveplan/vault` | ✅ | Vault orchestration layer for the Vaulted ecosystem: drive-backed encrypted… |
|
||||
| `@suaveplan/vault-sync` | ✅ | Offline-first, end-to-end-encrypted sync engine for vault items: domain-specif… |
|
||||
| `@suaveplan/x509` | ✅ | RFC 5280 v3 certificate parsing + chain validation: hand-rolled ASN.1 BER/DER codec for the X.509 subset; RFC 7468 PEM framing; TBSCertificate parser; pluggable extension decoder + critical-extension enforcement; all 8 standard extension decoders (basic constraints / key usage / EKU / SAN with DNS+IPv4+IPv6+URI+email+dirName+rfc822Name / name constraints / AKI + SKI / CRL distribution points); `validateChain` per §6.1 12-step algorithm with policy tree; validity period (UTCTime/GeneralizedTime 2050 boundary) with injectable `Clock`; signature verify dispatching to `@suaveplan/crypto-rsa` (PKCS1v15-SHA256/384/512 + PSS) and `@suaveplan/crypto-curve` (ECDSA-P256/384/521 + Ed25519); self-signed-leaf rejection; chain depth bound (default 16); `TrustAnchorStore` with configurable duplicate-SKI policy; OCSP responder interface. 45 src + 38 test files, 381 tests, 16 deterministic chain builders (real RSA/P-256/Ed25519 signatures, no mocks). 100% coverage. Gates webauthn-tpm + webauthn-android-key + webauthn-apple attestation. |
|
||||
|
||||
### `storage`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/cache-browser` | ✅ | Use `@suaveplan/cache-storage` over `@suaveplan/storage-browser-{localstorage,indexeddb,opfs}`. No separate package required. |
|
||||
| `@suaveplan/cache-memory` | ✅ | Built into `@suaveplan/cache` as `MemoryCache`. No separate package required. |
|
||||
| `@suaveplan/cache-redis` | ✅ | `AbstractCache<T>` backend over a minimal `RedisLike` interface — native `SET ... EX`, `GET`, `DEL`, `EXISTS`, `TTL`, `SCAN`. Reuses `@suaveplan/cache-storage` codecs (JSON / identity / base64). |
|
||||
| `@suaveplan/cache-storage` | ✅ | `AbstractCache<T>` backend that bridges any `@suaveplan/storage` `AbstractStorage` into a typed cache via a 32-byte fixed-width TTL header and pluggable `Codec<T>` (JSON default, identity for `Uint8Array`). Subsumes `cache-browser` by composition. |
|
||||
| `@suaveplan/storage` | ✅ | Enterprise key-value storage abstractions: AbstractStorage (StorageLike + strea… |
|
||||
| `@suaveplan/storage-browser-opfs` | ✅ | OPFS storage backend for @suaveplan/storage |
|
||||
| `@suaveplan/storage-cdn` | ✅ | Types-only foundation for CDN-flavoured storage adapters: `CdnAdapter` abstract base extending `AbstractStorage` with `purge`/`signUrl`/`edgeUrl`, `CdnCapabilities` matrix, Zod-validated `SignUrlOptions`, `STORAGE_CDN` error registry (8 codes), type-guard helpers, module augmentation for `capabilities.cdn`. Provider packages (rackspace, bunnycdn) implement against this contract. |
|
||||
| `@suaveplan/storage-cdn-bunnycdn` | ✅ | BunnyCDN Storage Zone + Pull Zone provider for `@suaveplan/storage-cdn`. Full `AbstractStorage`/`CdnAdapter` surface against the regional Storage Zone (`de`/`ny`/`la`/`sg`/`syd`/`uk`) with `AccessKey` headers, HMAC-SHA256 Token Authentication signed URLs, single + wildcard prefix purge via `api.bunny.net/purge`, transient-status retry honouring `Retry-After`, full OTel surface with `provider="bunnycdn"` + `storage_region`. The only adapter in the family with `cdnPrefixPurge: true`. |
|
||||
| `@suaveplan/storage-cdn-rackspace` | ✅ | Rackspace Cloud Files (OpenStack Swift) origin + Rackspace CDN provider for `@suaveplan/storage-cdn`. v2 identity auth with proactive token refresh, service-catalog endpoint lookup (LON/IAD/DFW/ORD/SYD/HKG), HMAC-SHA1 Swift TempURL signing, CDN purge (single-key), Range reads, native streaming, full OTel surface with `provider="rackspace"` + `region`. |
|
||||
| `@suaveplan/storage-file` | ✅ | filesystem storage |
|
||||
| `@suaveplan/storage-http` | ✅ | Universal HTTP/CDN-backed `AbstractStorage` adapter. Reads via `GET`/`HEAD`/`Range` against any `baseUrl + key` origin, optional writes through a pluggable `HttpUploader`, AbortSignal everywhere, retry on transient outcomes honouring `Retry-After`, conditional reads via ETag cache, native `ReadableStream` from `Response.body`, full OTel surface. Universal tier — no Node/Bun imports; runs unchanged in Node 18+, Bun, Deno, browsers, Workers. |
|
||||
| `@suaveplan/storage-s3` | ✅ | S3-compatible object storage adapter for @suaveplan/storage: implements Abstrac… |
|
||||
| `@suaveplan/storage-plugin-cache` | ✅ | Read-through cache decorator for any `AbstractStorage`. `cachedStorage(inner, options): CachedStorage` returns a class extending `AbstractStorage` that wraps an inner storage with a caller-supplied `AbstractCache<Uint8Array>` in front (the inverse of `cache-storage`'s storage-as-cache direction). Populates on miss when `shouldCache(key, bytes)` returns true (default 16 MiB ceiling). Invalidates on `set`/`setPartial`/`delete`/`clear` (post-success). `clear()` evicts only the wrapper's namespaced subset via `cache.keys()` enumeration; logs a warning + skips when the cache backend lacks `keys()` (avoids cross-namespace wipe). Passthrough on `exists` / `list` / `getMetadata` / `getWithMetadata` / `getSize` / `getItemSize` / `getPartial` / `createReadStream` / `createWriteStream`. 2-code `STORAGE_CACHE.*` registry; class renamed `CachedStorageError` to avoid the namespace collision with `@suaveplan/cache-storage`'s `StorageCacheError`. Cache failures swallow + counter-record. Multi-namespace isolation on a shared cache. Full OTel surface. Universal tier; composes with `drive`, `storage-uri` plugins, application code that takes a `StorageLike`. |
|
||||
| `@suaveplan/storage-uri` | ✅ | Universal URI-routed dispatcher over any `StorageLike`. Single resolver maps `scheme → UriPlugin` (`file://`, `https://`, `s3://`, `rackspace://`, `bunny://`, …), returns `ReadableStream<Uint8Array>` via `adapter.createReadStream` with one-chunk buffered fallback. 8-code `STORAGE_URI.*` registry. `SuaveplanError` subclasses thrown by adapters pass through unchanged; non-namespaced throwables wrap as `ADAPTER_ERROR`. AbortSignal propagation, full OTel surface, `runUriPluginConformance` harness exported for sibling plugin packages. |
|
||||
|
||||
### `store`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/api-plugin-inventory` | – | Inventory management plugin — items, stock levels, m... |
|
||||
| `@suaveplan/api-plugin-store` | – | E-commerce store plugin |
|
||||
| `@suaveplan/api-plugin-subscription` | – | Subscription/recurring billing plugin |
|
||||
| `@suaveplan/api-plugin-virtual-currency` | – | Virtual currency wallets, transfers, purchases, cashout |
|
||||
| `@suaveplan/r3f-inventory` | – | Grid-based inventory, equipment, loot tables |
|
||||
| `@suaveplan/react-store` | – | Store state management components |
|
||||
|
||||
### `testing`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/ab-testing` | ✅ | Enterprise A/B testing and experimentation: deterministic FNV-1a bucketing, tar… |
|
||||
| `@suaveplan/testing` | ✅ | Universal test utilities (FakeClock, Factory, FixtureManager, MemoryStore, buil… |
|
||||
| `@suaveplan/testing-dom` | ✅ | DOM/React testing — vitest+bun-test conditional re-exports, createTestConfig, p… |
|
||||
| `@suaveplan/testing-e2e` | ✅ | Playwright E2E utilities — config factory, page objects, fixtures, helpers, str… |
|
||||
| `@suaveplan/testing-integration` | ✅ | Core integration test infrastructure: ServiceFixture<T> interface, TestEnvironm… |
|
||||
| `@suaveplan/testing-integration-garage-s3` | ✅ | Garage S3 v2.0.0 testcontainers fixture for @suaveplan/testing-integration |
|
||||
| `@suaveplan/testing-integration-mailpit` | ✅ | Mailpit testcontainers fixture + assertMail helper for @suaveplan/testing-integ… |
|
||||
| `@suaveplan/testing-integration-postgres` | ✅ | PostgreSQL 16 testcontainers fixture for @suaveplan/testing-integration |
|
||||
| `@suaveplan/testing-integration-redis` | ✅ | Redis 7 testcontainers fixture for @suaveplan/testing-integration |
|
||||
| `@suaveplan/testing-integration-step-ca` | ✅ | smallstep/step-ca testcontainers fixture for @suaveplan/testing-integration — r… |
|
||||
| `@suaveplan/testing-integration-surrealdb` | ✅ | SurrealDB v3 testcontainers fixture for @suaveplan/testing-integration |
|
||||
| `@suaveplan/testing-integration-typesense` | ✅ | Typesense 27 testcontainers fixture for @suaveplan/testing-integration |
|
||||
|
||||
### `timing`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/cron` | ✅ | Universal-tier cron expression parser, next-fire calculator with timezone + DST support, and lightweight in-process scheduler |
|
||||
| `@suaveplan/datetime` | ✅ | Timezone-aware date/time utilities with business-day arithmetic, recurrence rul… |
|
||||
| `@suaveplan/timing` | ✅ | Timing and scheduling utilities — stopwatch, throttle, debounce, scheduler, dec… |
|
||||
|
||||
### `tooling`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/cli` | ✅ | Zero-dependency CLI toolkit: argument parser, command router, ANSI styling, out… |
|
||||
| `@suaveplan/electrobun` | ✅ | Electrobun desktop-app generator + orchestrator: scaffolder (9 project + 8 feature renderers), iconz-backed icon pipeline, five CLI subcommands (create/dev/build/package/sign) orchestrating the upstream `electrobun` binary via an injectable `CommandRunner`, layered config + Prompter seam, Zod-validated options, eleven `ELECTROBUN.*` codes, 5 OTel counters + 2 histograms |
|
||||
| `@suaveplan/install-agent-skills` | ✅ | Suaveplan installer plugin: bundles the agent-skills set and installs them into… |
|
||||
| `@suaveplan/installer` | ✅ | Plugin host + lifecycle for project boilerplating and asset installation: detec… |
|
||||
| `@suaveplan/license` | ✅ | Software license generation |
|
||||
| `@suaveplan/linter` | ✅ | Unified markdown + biome linting pipeline: remark-based markdown fixer, injectable CommandRunner-backed biome wrapper, glob resolver, orchestrator with structured LintReport, pure runCli, `LINTER_CODES` error registry, OTel self-telemetry. |
|
||||
| `@suaveplan/overview` | ✅ | Machine-readable monorepo manifest with Zod schemas, tier inference, server-tier filesystem discovery (`./discover` subpath), graph utilities (Kahn topo, Tarjan cycles, transitive deps), plugin/category/tier roll-ups, 6 `OVERVIEW.*` codes, OTel. 56 tests / 100/100/100/100. |
|
||||
| `@suaveplan/profiler` | ✅ | Runtime performance profiling: sampler, marks, memory snapshots, bottleneck detector, flamegraph JSON, profile comparator, text reporter, OTel exporter; injectable Clock/Timers/Logger. |
|
||||
| `@suaveplan/readline` | ✅ | Terminal I/O primitive: AsyncGenerator line reader and interactive prompt inter… |
|
||||
| `@suaveplan/sandbox` | ✅ | Server-tier sandboxed code execution — vm/worker/spawn/isolate engines, pre-warmed pool, async result streamer, regex code transformer, module resolver, sliding-window rate limiter, SHA-256 audit logger, OTel instruments, 15 SANDBOX.* error codes |
|
||||
| `@suaveplan/server-keys` | ✅ | Server key management CLI |
|
||||
| `@suaveplan/test-vite` | ✅ | Vite + Vitest configuration factories and validators with browser-environment shims, tree-shake validator, bundle-size snapshot, port allocator, HMR tester, custom matchers, TEST_VITE_CODES error registry, and OTel self-telemetry. |
|
||||
| `@suaveplan/wip` | – | Work-in-progress experiments |
|
||||
|
||||
### `unknown`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/biometrics` | ❌ | Touch ID / Face ID / Hello facade (Vaulted) |
|
||||
| `@suaveplan/clipboard` | ✅ | Enterprise Clipboard API: typed errors, OTel metrics, async queue, secure-context guard, image/blob copy + read, paste-event read fallback, permission change subscription, focus-aware retry, sanitizer hook |
|
||||
| `@suaveplan/form-detection` | ❌ | DOM heuristics for autofill (Vaulted) |
|
||||
| `@suaveplan/iconz` | ❌ | App icon generator from SVG (Vaulted) |
|
||||
| `@suaveplan/mcp` | ✅ | Universal-tier MCP server + client with JSON-RPC dispatcher, pluggable transports (in-memory, stdio, HTTP+SSE), AbortSignal cancellation, progress notifications, authorize hook, and OTel meter |
|
||||
| `@suaveplan/media` | ✅ | Universal-tier media detection, metadata extraction, and constraint validation. Pluggable `SignatureRegistry` (twelve built-in formats: PNG, JPEG, GIF, BMP, WEBP, RIFF, MP4/MOV, WebM/MKV, MP3-ID3, MP3-sync, FLAC, OGG) with offset-aware `match`/`detect`/`register`/`unregister`. Detection via `detectMediaType`, `detectSignature`, `detectMediaTypeFromName` (MIME bucket via `@suaveplan/mime`), `inferMimeFromSignature` canonical-MIME mapping. Sync `getMetadata` + async `getMetadataAsync` with optional `MediaMetadataExtractor` hook (width/height/duration). `validateMedia`/`validateMediaAsync`/`assertValidMedia` with Zod-parsed `MediaConstraints` (size/MIME/type/dimensions/duration/predicate), wrapped predicate errors. Typed `MediaError` hierarchy with eight stable `MEDIA.*` codes (`EMPTY_INPUT`, `INVALID_CONSTRAINTS`, `VALIDATION_FAILED`, `UNSUPPORTED_TYPE`, `PREDICATE_FAILED`, `DUPLICATE_SIGNATURE`, `METADATA_EXTRACTOR_FAILED`, `INVALID_SIGNATURE`) plus per-code metadata (`httpStatus`, `retryable`, `severity`, `description`). OTel: four counters (`media_detect_total`, `media_metadata_total`, `media_validate_total`, `media_error_total`), two histograms (`media_op_latency_ms`, `media_input_size_bytes`). 103 tests, 100/100/100/100 coverage. |
|
||||
| `@suaveplan/native-messaging` | ❌ | Encrypted extension↔desktop transport (Vaulted) |
|
||||
| `@suaveplan/qrcode-scan` | ❌ | QR decode subpath (Vaulted) |
|
||||
| `@suaveplan/react-map` | – | Map visualization components |
|
||||
| `@suaveplan/react-media` | – | Media rendering components |
|
||||
| `@suaveplan/react-rule-builder` | – | Visual rule builder with AND/OR groups, drag-drop, JSON config |
|
||||
| `@suaveplan/site-icons` | ❌ | Favicon fetch + cache (Vaulted) |
|
||||
| `@suaveplan/spatial-hash` | – | Spatial hashing for O(1) broad-phase 2D/3D queries |
|
||||
| `@suaveplan/storage-browser` | ❌ | **Retired** — superseded by split into `storage-browser-indexeddb` / `storage-browser-localstorage` / `storage-browser-opfs` (all ✅). See archive `2026-05-12-enterprise-browser-storage-browser-superseded-by-split`. |
|
||||
| `@suaveplan/storybook-test-utils` | – | Storybook E2E testing utilities |
|
||||
| `@suaveplan/streams-chunk-queue` | ✅ | Chunked-queue stream processing: binary protocol, decryptor, rate limiter, storage adapter, hooks, validator, OTel. |
|
||||
| `@suaveplan/streams-metadata` | ✅ | Stream transform for extracting rich media metadata via EXIF, MIME, and events. |
|
||||
| `@suaveplan/third-party-elevenlabs` | ✅ | Typed thin client for the ElevenLabs platform API: REST + WebSocket transport,… |
|
||||
| `@suaveplan/web-workers` | ✅ | Browser-tier Web Worker / SharedWorker / worker-pool runtime: typed RPC with AbortSignal + transfer-list + backpressure + circuit breaker + streaming, FIFO + round-robin / least-busy schedulers, restart-on-crash supervision, EventBus lifecycle emission, OTel observability, 13-code `WEB_WORKERS.*` registry, `defineWorker()` helper. |
|
||||
| `@suaveplan/workflow` | ✅ | Workflow/saga orchestration: fluent builder, sequential/parallel/conditional steps, saga compensation, retry middleware, hooks, transition-data pipeline, six `WORKFLOW.*` codes, OTel. |
|
||||
|
||||
### `vatdash`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/api-plugin-vatdash-bridge` | – | Bridge wiring all api-plugins into the vatdash Hono server |
|
||||
|
||||
### `video`
|
||||
|
||||
| Package | Status | Info |
|
||||
|---|:---:|---|
|
||||
| `@suaveplan/react-video` | – | Video player components |
|
||||
| `@suaveplan/video` | ✅ | Universal-tier video metadata + transform-planning: 8-format magic-byte sniffer, `calculateVideoResize` with 6 presets, `validateTrimRange`, `VideoProcessor` w/ typed EventBus, `extractMetadata`/`planTransform`, Zod-validated 9-variant `VideoTransformOpSchema`, fluent `VideoPipelineBuilder`, 16-code `VIDEO.*` registry + metadata table, OTel. 113 tests / 100/100/100/100. |
|
||||
| `@suaveplan/video-thumbnails` | ✅ | Video thumbnail extraction + sprite sheets via injectable FfmpegRunner (default fluent-ffmpeg + bundled @ffmpeg-installer/@ffprobe-installer binaries). Single-frame, batch, sliding-window, N×M tile. Zod boundaries, AbortSignal, 9 `VIDEO_THUMBNAILS.*` codes, 4 OTel instruments. 93 tests / 100/100/100/100. |
|
||||
|
||||
## Maintenance
|
||||
|
||||
When you finish migrating a package:
|
||||
|
||||
1. Confirm the folder under `packages/<plugin>/<pkg>/` builds and tests cleanly.
|
||||
2. Flip the row's status icon from `–` to `✅` and replace the suaveplan summary with the genesis description from the package's `README.md` (truncate to ~80 chars).
|
||||
3. Run `bun scripts/folder-reorg/generate-manifest.ts` to refresh `MANIFEST.md`. The dep graph is the source of truth for workspace edges.
|
||||
4. Run `bun scripts/generate-missing.ts` to refresh `MISSING.yml`.
|
||||
5. Update `SUMMARY.md` per the workflow described in `AGENTS.md` step 5 — paste the README summary into its plugin-folder section.
|
||||
|
||||
When you propose a net-new package not in `suaveplan`:
|
||||
|
||||
1. Add a row here with status `❌`, the planned parent folder, and a short note explaining what it does and why.
|
||||
2. Open the OpenSpec change (`openspec/changes/<change-id>/`) before any code lands.
|
||||
|
||||
## Superseded upstream packages — do not migrate
|
||||
|
||||
Per `openspec/changes/network-transport-family/proposal.md` (2026-05-11) the following upstream packages have been replaced by genesis equivalents and MUST NOT be migrated under their original names:
|
||||
|
||||
| Upstream | Genesis replacement |
|
||||
|---|---|
|
||||
| `@suaveplan/network-protocol` | `@suaveplan/network-plugin-protocol` ✅ |
|
||||
| `@suaveplan/network-registry` | `@suaveplan/network-plugin-registry` ✅ |
|
||||
| `@suaveplan/network-assignment` | `@suaveplan/network-plugin-assignment` ✅ |
|
||||
| `@suaveplan/network-webrtc` | `@suaveplan/network-transport-webrtc` ✅ |
|
||||
| `@suaveplan/network-topology` | `@suaveplan/network-plugin-topology` ✅ |
|
||||
| `@suaveplan/network-failover` | `@suaveplan/network-plugin-failover` ✅ |
|
||||
| `@suaveplan/network-governance` | `@suaveplan/network-plugin-governance` ✅ |
|
||||
| `@suaveplan/network-websockets` | `@suaveplan/network-transport-websockets` ✅ |
|
||||
| `@suaveplan/network-observability` | folded into `@suaveplan/network-plugin-monitor` ✅ (Wave 6.1 `network-observability-consolidation`; Prometheus exposition + Grafana dashboard generator land as universal-tier feature modules; OTel SDK bridge dropped as redundant — monitor emits through `@opentelemetry/api` directly) |
|
||||
| `@suaveplan/network-quality` | folded into `@suaveplan/network-plugin-monitor` ✅ (Wave 6.2 `network-quality-consolidation`; `BandwidthMonitor` EMA recorder + `DegradationManager` 4-level classifier with hysteresis + one-step transitions + `createQualityMiddleware` per-source throttling gate land as universal-tier feature modules under `src/quality/`; `EventEmitter`-based upstream evented API replaced by callback-handler subscriptions returning unsubscribe fns, dropping the `@suaveplan/events` runtime coupling — matches genesis no-monolithic-EE convention; `hysteresisMs: 0` newly commits same-call so the policy is genuinely usable for synchronous tests) |
|
||||
|
||||
If upstream functionality is missing from a genesis replacement, extend the replacement — do not create a parallel package under the deprecated name.
|
||||
474
SUMMARY.md
Normal file
474
SUMMARY.md
Normal file
@@ -0,0 +1,474 @@
|
||||
# Migration Summary
|
||||
|
||||
> Per-package summaries for every library shipped, in flight, or pending migration on `genesis`. The companion [`STATUS.md`](./STATUS.md) is the at-a-glance dashboard with status icons; this file holds the longer descriptions.
|
||||
>
|
||||
> Sections: **Completed** — migrated from `../suaveplan/` and building on genesis. **Pending** — still in `../suaveplan/` awaiting their port. **Genesis-Original** — created directly here, no suaveplan equivalent.
|
||||
>
|
||||
> Update this file when finishing a package: paste the README's first-paragraph summary into the matching section.
|
||||
>
|
||||
> **Architecture references for the drive / storage / network layers (added 2026-05-02):**
|
||||
>
|
||||
> - [`docs/storage/`](docs/storage/) — `AbstractStorage` contract, capability matrix, every backend (Memory, S3, OPFS, IndexedDB, Node fs, SQLite, FAT/exFAT/ext4 via WASM, ContentAddressed, Garage).
|
||||
> - [`docs/drive/`](docs/drive/) — thin VFS core, the two indexes (DirIndex + FileLocationIndex), plugin chain, on-the-fly encrypt/sign/decrypt/verify, mesh + sharing.
|
||||
> - [`docs/network/`](docs/network/) — `NetworkNode` + 21-plugin substrate, Tier 0/1/2 topology with meshed pockets, four hosting models, server failover, Ed25519 signing + token model, offline mode + reconciliation.
|
||||
> - [`docs/drive-storage-network.md`](docs/drive-storage-network.md) — the composition map: end-to-end write/read traces, what-vanishes-when-server-vanishes, deployment recipes.
|
||||
>
|
||||
> When migrating any package in the drive/storage/network space, **read these first** — they encode the genesis design decisions (`drive-port` over `enterprise-drive`, `enterprise-types-cross-cutting` archived 2026-05-02 as canonical interface home, `drive-network-integration` boundary contract) that downstream proposals must conform to.
|
||||
|
||||
## Completed
|
||||
|
||||
**207** packages already migrated from suaveplan to genesis (per `STATUS.md` ✅, refreshed 2026-05-12). The table below is partially stale — refresh per-row entries from each package's `README.md` first paragraph when next touched.
|
||||
|
||||
| Package | Summary |
|
||||
| --- | --- |
|
||||
| `@suaveplan/ab-testing` | Enterprise A/B testing and experimentation engine with deterministic FNV-1a bucketing, targeting rules, variant assignment, conversion tracking, result aggregation, and OpenTelemetry instrumentation. |
|
||||
| `@suaveplan/api-schemas` | Shared Zod schemas describing Suaveplan API request/response shapes. The single source of truth for server plugins, React data hooks, and generated OpenAPI documents. |
|
||||
| `@suaveplan/audit-logging` | Tamper-proof, GDPR-compliant audit logging for the Suaveplan monorepo. Provides a cryptographic chain-integrity engine, pluggable storage adapters, GDPR data export, configurable retention policies, and a rich event taxonomy spanning auth, authorization, data access, system, and GDPR events. |
|
||||
| `@suaveplan/auth` | Provider-agnostic authentication library for the Suaveplan monorepo. Ships JWT token lifecycle, API key management, session management, and framework-agnostic middleware — all backed by a pluggable AuthAdapter interface. |
|
||||
| `@suaveplan/barcode` | Runtime-agnostic 1D barcode generation for seven symbologies — Code 128B, Code 39, EAN-13, EAN-8, UPC-A, UPC-E, and ITF. Outputs inline SVG strings and binary bar-pattern arrays with no canvas, no DOM, and no Node.js built-ins required, making it safe to use in Bun, Deno, edge runtimes, and browsers alike. Includes check-digit calculation, barcode validation, and human-readable text rendering. Use this package to generate product labels, shipping labels (ITF/GTIN-14), retail barcodes (EAN/UPC), and document barcodes (Code 128). For 2D barcodes (QR codes) use @suaveplan/qrcode. This package does not support barcode scanning/decoding from images — it is generation-only. |
|
||||
| `@suaveplan/bitpacker` | Bit-level binary packing and unpacking for TypeScript. Designed for network protocols, game-state serialisation, and compact binary formats. Pure TypeScript, zero runtime deps beyond @suaveplan/error. Hardened 2026-05-03 (`bitpacker-hardening` archived): Zod boundary validation on `BitPacker`/strategy/context types, `./register-schemas` subpath integrating with `@suaveplan/schema-registry` (subject `bitpacker:@suaveplan/bitpacker` v1, idempotent), OTel `instruments.ts` (tracer/meter `@suaveplan/bitpacker`, spans `bitpacker.pack` / `bitpacker.unpack`, counters `bitpacker.ops.total`, histograms `bitpacker.op.duration_ms` + `bitpacker.op.bytes`), fast-check round-trip property tests, and a committed mitata baseline at `scripts/bench/bitpacker-baselines.json`. The whole bitpacker family (`-strategies`, `-validation`, `-game`, `-streaming`, `-compressed`, `-network`) shares the same hardening pattern; `bitpacker-strategies` additionally gained opt-in forward-tolerance on the `_version` strategy via `_tolerance: "strict" \| "forward"` + `_allowedVersions: number[]`. Every implementation `.ts` in the family now has a sibling `.md` (47 new docs files). |
|
||||
| `@suaveplan/cache` | Type-safe caching abstraction for the genesis monorepo. Provides MemoryCache with LRU/LFU/FIFO/TTL eviction, NamespacedCache for shared-storage isolation, a CacheFactory with module-augmentation-based provider registry, stale-while-revalidate, multi-get/set, injectable clock and event bus, OTel instrumentation, and structured CACHE. error codes. |
|
||||
| `@suaveplan/cache-storage` | `AbstractCache<T>` backend that bridges any `@suaveplan/storage` `AbstractStorage` into a typed cache via a 32-byte fixed-width TTL header and pluggable `Codec<T>` (JSON default, identity for `Uint8Array`). Auto-registers as the `"storage"` provider on `CacheFactory`. Subsumes the upstream `cache-browser` package — wire `StorageCache` over `storage-browser-{localstorage,indexeddb,opfs}` and the cache surface is identical to `MemoryCache`. |
|
||||
| `@suaveplan/cache-redis` | `AbstractCache<T>` backend over a minimal `RedisLike` interface — native `SET ... EX`, `GET`, `DEL`, `EXISTS`, `TTL`, `SCAN`. Reuses `@suaveplan/cache-storage` codecs (JSON / identity / base64). TTL is server-managed (no client header bytes, no client-side eviction). Auto-registers as the `"redis"` provider on `CacheFactory`. |
|
||||
| `@suaveplan/cli` | Enterprise-grade CLI framework for Node.js. Composable fluent API with subcommand routing, option parsing, OTel tracing, and a full set of terminal UI primitives. |
|
||||
| `@suaveplan/compress-image` | Runtime-agnostic image compression utilities with pluggable adapters for Node.js (Sharp) and testing (MemoryAdapter). Supports resize, quality reduction, format conversion (JPEG, PNG, WebP, AVIF), and metadata stripping. |
|
||||
| `@suaveplan/compress-svg` | SVG optimisation and minification for TypeScript. Pure TypeScript — no DOMParser, no external SVG library. Runs in Bun, Node.js, browsers, and edge runtimes. |
|
||||
| `@suaveplan/config` | Layered configuration management for the Suaveplan monorepo. Resolves an ordered set of typed layers (defaults, file, object, env, CLI) in priority order, deep-merges the results, applies per-environment overrides, validates via a caller-supplied Zod schema or custom function, and exposes an immutable ConfigStore with live reload and event-bus change propagation. |
|
||||
| `@suaveplan/crdt` | Conflict-free Replicated Data Types (CRDTs) for distributed systems. Provides |
|
||||
| `@suaveplan/crypto` | Universal cryptographic primitives — engine-pluggable AEAD, Ed25519/ECDSA-P256 signing, X25519 key agreement, sealed-box, and auth-box. Built on audited @noble/ libraries with zero WASM. |
|
||||
| `@suaveplan/cryptocurrency` | Universal-tier cryptocurrency utility library — wallet address validation across 15 mainnet chains (Bitcoin, Ethereum, Solana, Polygon, Arbitrum, Optimism, Avalanche, BSC, Base, TRON, Litecoin, Dogecoin, Cardano, Polkadot, Cosmos) with chain auto-detection and simplified positional EIP-55 checksumming; transaction-hash classification across EVM / UTXO / Solana shapes; static + adapter-backed chain registry (`MemoryChainRegistry`, `ChainRegistry` async façade with optional `LoggerLike` injection emitting `chain.registered` / `chain.lookup.miss` events and raising typed `CryptocurrencyError({ code: CRYPTOCURRENCY.UNKNOWN_CHAIN })`); 8-entry `COMMON_TOKENS` ERC-20 catalogue + `TokenRegistry` with case-insensitive address lookup, substring search by symbol / name, and `formatTokenBalance` bigint formatter; EIP-1559 gas estimator (`estimateGas`, `calculateMaxFeePerGas`, `calculateEffectiveGasPrice`, `formatGwei`, `estimateTxCostEth`) with bigint-pure arithmetic throughout; calldata + 4-byte function-selector decoder (`KNOWN_SELECTORS`, `lookupSelector`, `extractSelector`, `decodeCalldata`) covering ERC-20 / ERC-721 hot paths; generic + convenience unit conversion across ETH (wei / kwei / mwei / gwei / microether / milliether / ether), BTC (satoshi / microbitcoin / millibitcoin / bitcoin), SOL (lamport / sol). 5-code `CryptocurrencyError` registry (`UNKNOWN_CHAIN`, `UNKNOWN_CURRENCY`, `UNKNOWN_UNIT`, `INVALID_ADDRESS`, `INVALID_HASH`) extending `SuaveplanError` with `chainId` / `currency` / `unit` discriminants. 11 subpath exports tree-shake every public module. 183 tests across 11 files at 100/100/100/100 (Istanbul). Deps: `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/data-masking` | PII/sensitive data masking for objects and strings with field-level rules, |
|
||||
| `@suaveplan/datetime` | Timezone-aware date/time utilities covering the most common application-layer date operations that the native Date API leaves unaddressed. The package provides calendar math (addDuration, subtractDuration, startOf, endOf, getCalendarMonth, leap-year detection), business-day arithmetic with configurable holiday calendars and custom weekend definitions, an RRULE-subset recurrence engine (DAILY/WEEKLY/MONTHLY/YEARLY with BYDAY/BYMONTH/COUNT/UNTIL), and human-readable relative time formatting ("3 hours ago", "in 2 days"). All stateful entry points accept an injectable Clock for deterministic testing. Use this package for scheduling UIs, calendar components, invoice date calculations, and any domain where "next business day" or "every other Monday" has real meaning. For low-level stopwatches, throttle/debounce, or vector clocks use @suaveplan/timing. |
|
||||
| `@suaveplan/db` | Universal relational database adapter interface for the Suaveplan monorepo. Provides the DatabaseAdapter contract (extending DatabaseLike from @suaveplan/types), injectable connection singletons, structured DB. error codes, Zod-validated config types, a MigrationRunner interface, and full OpenTelemetry instrumentation. |
|
||||
| `@suaveplan/db-sqlite` | Universal SQLite adapter for the Suaveplan monorepo. Runs on bun:sqlite on the Bun server and @sqlite.org/sqlite-wasm (Web Worker) in the browser, unified behind a single SqliteAdapter that implements DatabaseAdapter from @suaveplan/db. Includes an LRU prepared-statement cache (128 entries by default), a forward-only ledger-tracked MigrationRunner with dry-run support, OpenTelemetry span instrumentation, structured SQLITE error codes, and a ./unsafe subpath for raw queries. |
|
||||
| `@suaveplan/dedup` | Entity deduplication engine for detecting and merging duplicate records in any data set. Given a list of objects with varying representations of the same real-world entity (e.g. "John Smith" vs "J. Smith", or the same email in different capitalisation), the DedupEngine scores pairs using a weighted combination of field-level similarity algorithms — Jaro-Winkler, Levenshtein, exact match, or phonetic — groups records into connected components via union-find, and merges each group with a pluggable strategy (latestWins, manual, or scoredMerge). Use this package for ETL pipelines, contact deduplication, and data cleansing. For domain-name-specific normalisation and alias resolution use @suaveplan/domain-normalizer; for string-similarity primitives only, the scoring module is exported as a standalone subpath. |
|
||||
| `@suaveplan/diff` | Zero-dependency RFC 6902 JSON Patch engine for computing, applying, and inverting structural differences between JSON documents. The diff(a, b) function produces a minimal patch using index-based or LCS array strategies; applyPatch applies it; revertPatch undoes it; and invertPatch creates an inverse patch. A HistoryManager provides undo/redo with configurable depth and branch truncation on new edits. A SyncManager handles optimistic concurrency: apply local changes speculatively, then reconcile when the server responds. RFC 6901 JSON Pointer parsing and formatting is exposed as a standalone path module. Use this package for collaborative editing, undo/redo stacks, change-event streams, and any system where you need to track or transmit the delta between two object states rather than the full state itself. |
|
||||
| `@suaveplan/dmarc` | DMARC report parser for @suaveplan — parses aggregate (RUA) and forensic (RUF) DMARC XML reports into validated, strongly-typed structures. Accepts raw XML strings or gzip-compressed binary blobs. |
|
||||
| `@suaveplan/drive` | Lightweight virtual filesystem core for the Suaveplan monorepo. File-level operations over `AbstractStorage` with a plugin slot for middleware, chunking, encryption, and P2P mesh. Structural `DriveStorage` typing, generic storage map narrowing, lifecycle ownership tracking, and capability-aware stream fallbacks. |
|
||||
| `@suaveplan/drive-plugin-chunks` | Chunk-addressing plugin for `@suaveplan/drive`. Splits files into signed, encrypted chunks using the locked `version 0x0001` binary format, with server-signed manifests and two storage layouts (`sparse-file` and `chunk-keys`). XChaCha20-Poly1305 per-chunk encryption, Ed25519 server signatures, deterministic per-chunk nonces, automatic layout selection based on storage capabilities, and read middleware for transparent reassembly. |
|
||||
| `@suaveplan/domain-normalizer` | Normalisation, matching, and alias-canonicalisation for internet domain names. Given heterogeneous URL or domain input — including common mobile/CDN subdomains and 31 built-in alias mappings (e.g. m.youtube.com → youtube.com, old.reddit.com → reddit.com) — this package produces a canonical registrable domain via tldts, strips www/m/mobile prefixes, resolves the alias map, and returns structured NormalizedDomain objects. It also provides Levenshtein-based similarity scoring (computeSimilarity), fuzzy matching (matchDomains), and a groupDomains function that clusters a mixed list by canonical root domain. Use this package for deduplicating contact email domains, canonicalising URLs before storage, and building domain-level blocklists. For full entity deduplication across arbitrary fields use @suaveplan/dedup; for MIME or URL content-type detection use @suaveplan/mime. |
|
||||
| `@suaveplan/env` | Type-safe, schema-first environment variable loading for Node.js and edge runtimes. Define a schema of typed field builders, point it at your sources, and receive a frozen record with TypeScript types inferred at compile time — no manual type annotations required. |
|
||||
| `@suaveplan/error` | Foundation error substrate for the Suaveplan monorepo. Every non-testing @suaveplan/ package extends this library's SuaveplanError base class and uses its stable code registry. The package provides: SuaveplanError<TCode> — an abstract base with literal-typed discriminant codes, injectable clock, context bag, traceId, and cause chaining; defineCodes(namespace, reasons) — a frozen, literal-typed registry that makes error codes refactor-safe; walkCauses(err) — a cycle-safe, depth-bounded generator for traversing nested error causes; lossless JSON and binary wire formats (toJSON/fromJSON/toBinary/fromBinary) with Zod validation; markSensitive/unwrapSensitive for redaction at serialization boundaries; toSpanEvent for OpenTelemetry exception span events without an OTel SDK import; and a setMessageResolver hook for i18n. Zero @suaveplan/ runtime or peer dependencies — this is the lowest-level package in the stack. |
|
||||
| `@suaveplan/etl` | Extract-Transform-Load pipeline engine with typed stages, error handling, observable progress, and OTel instrumentation. |
|
||||
| `@suaveplan/event-sourcing` | Event sourcing patterns for the genesis monorepo: typed aggregate root, in-memory event store with optimistic concurrency, snapshot support, projections, upcasting, and OTel instrumentation. |
|
||||
| `@suaveplan/events` | Typed event-bus substrate for the Suaveplan monorepo. Pub/sub with deterministic FIFO delivery, interceptor API, bounded-queue backpressure, idempotent listeners, Zod-validated envelopes, and a passive observability adapter. |
|
||||
| `@suaveplan/events-metrics` | OTel metrics companion for @suaveplan/events. Attaches via the public interceptor API to record dispatch totals, listener error counts, and latency histograms per event name. |
|
||||
| `@suaveplan/events-otel` | OpenTelemetry tracing companion for @suaveplan/events. Wraps each dispatch in a span, propagates W3C Trace Context, and records listener exceptions as span errors. |
|
||||
| `@suaveplan/events-schema` | Runtime schema-validation companion for @suaveplan/events. Validates event payloads at dispatch boundaries via Zod schemas with strict/warn/sample modes and sensitive field redaction. |
|
||||
| `@suaveplan/events-transport` | Cross-context event bridges for @suaveplan/events: MessageChannel (at-least-once with ack/retry) |
|
||||
| `@suaveplan/exif` | Dependency-free, runtime-agnostic EXIF/XMP/IPTC metadata reader, writer, and stripper for JPEG and PNG images. Parses JPEG segment chains to locate and decode the EXIF IFD (TIFF byte-order aware, including GPS and SubIFD), IPTC IIM (caption, keywords, copyright), and XMP Dublin Core (raw XML string extraction). Writes back a revised EXIF IFD without re-encoding the image pixels, and strips all metadata segments for privacy-preserving image export. Includes a ByteReader/ByteWriter pair with rational arithmetic helpers and CRC-32 for PNG chunks. Use this package when you need to read camera metadata (make, model, GPS coordinates, orientation, focal length), watermark images with IPTC copyright fields, or scrub location data before publishing user-uploaded photos. For audio file metadata (ID3, MP4 atoms, FLAC Vorbis) use @suaveplan/audio-metadata. |
|
||||
| `@suaveplan/feature-flags` | Provider-agnostic feature flag evaluation engine for controlling feature rollout without code deploys. The package ships a FlagManager high-level façade, a FlagRegistry backed by any pluggable FlagProvider, and an Evaluator strategy chain. Five composable strategies cover the full rollout playbook: PercentageStrategy (deterministic FNV-1a bucketing for consistent user cohorts), UserTargetStrategy (allowlist), AttributeStrategy (10 comparison operators for segment targeting), ScheduleStrategy (time-window gating), and CompositeStrategy (AND/OR combinator). Built-in providers include MemoryProvider and JsonProvider for file-based flag definitions. Evaluation priority is: flag disabled → in-process override → first matching strategy → default value. This package is for runtime flag evaluation only — it is not a flag management UI or remote config service. |
|
||||
| `@suaveplan/font-compress` | BTF (Binary TypeFace) binary packing and unpacking. Converts TypefaceFont (typeface.js JSON format) to a compact Uint8Array using dictionary-based integer compression, and reverses it. |
|
||||
| `@suaveplan/fsm` | Typed, generic finite state machine (FSM) for TypeScript. Ships a synchronous-by-default interpreter with guard predicates, ordered transition actions, context threading, entry/exit lifecycle hooks, history recording, and OTel instrumentation. Zero peer dependencies beyond @suaveplan/error, @suaveplan/events, and @suaveplan/types. |
|
||||
| `@suaveplan/i18n` | Enterprise-grade internationalization (i18n) core for the Suaveplan monorepo. Works in Bun, Node.js, and modern browsers — zero DOM dependencies, tree-shakeable subpath exports. The package covers the full i18n surface: a namespaced `MessageCatalog<TLocale, TMessages>` with `t<K extends DotPaths<TMessages> \| (string & {})>` typed key inference, the ergonomic options bag (`count` / `context` / `defaultValue` / `values` / nested `$t(other.key)` references with depth-4 cycle detection), namespace-scoped `set` / `load` / `keys`, in-flight loader dedup with `LOADER_FAILED` wrapping, source-discriminated `LookupResult { value, locale, namespace, source: 'catalog' \| 'fallback' \| 'handler' \| 'key' }`, and strict mode that throws`MISSING_KEY`. The full ICU MessageFormat surface lands here too: an`InterpolationEngine` with `{{ variable }}`-style tokens, locale-aware per-slot`ValueFormatter`s, and HTML-escape support; a`MessageParser`AST covering plural / select / selectordinal / typed-argument (`{date, date, short}` / `{n, number, currency/USD}`) / apostrophe quoting / rich-text tag interpolation (with a`formatChunks<T>` path that returns `Array<string \| T>` for React consumers); a `PluralizationEngine` with 14 hand-written language rules (Arabic, Russian, Celtic, etc.) plus an `Intl.PluralRules` first-resolution fallback so new CLDR languages work without a code change; ten formatters wrapping every common `Intl.*`API (`formatDate`,`formatCurrency`,`formatNumber`,`formatRelativeTime`,`formatList`,`formatDisplayName`,`formatPercent`,`formatCompact`,`formatUnit`,`formatSegment`,`compareLocale`) with per-(locale, options) caching; a`LocaleResolver` with `Intl.Locale.weekInfo`-aware`getDayNames`, BCP-47 validation that throws`INVALID_LOCALE` on garbage, script-casing-preserving `parseAcceptLanguage`, and the four-strategy chain (`exact` / `alias` / `language` / `region`); server-side`createLocaleMiddleware` with optional `setHeader('Content-Language', ...)`. Five built-in`Backend`implementations (`MemoryBackend`,`HttpBackend`,`StorageBackend` wrapping any `AbstractStorage` from `@suaveplan/storage`,`MultiFileFsBackend`,`ChainedBackend` with write-through caching) make translation loading pluggable; `MessageCatalog.preload` / `getAsync` / `invalidate` / `saveMissing` complete the lazy-load story. The `I18nInstance`facade composes every engine plus a plugin lifecycle (`use(plugin)` + `dispose()` in reverse-registration order), five canonical event topics on the `@suaveplan/events`-backed bus (`languageChanged` / `loaded` / `missingKey` / `keyAdded` / `pluralRuleAdded`), the`pseudoLocalize`post-processor (`accent` / `bracket` / `expand` modes) plus `pseudoLocalizationPlugin`, and the`createMissingKeyReporter` plugin with `persistMissing` opt-in routing through `backend.save`. Every engine constructor calls`<Schema>.safeParse(options)` and surfaces Zod failures as `I18nError` with `context.zodIssues` populated. OTel meter instruments built via `getMeter('suaveplan.i18n')` cover every hot path: `i18n.translate.total{outcome,locale}`,`i18n.translate.duration_ms`,`i18n.locale.resolve.total{strategy,locale}`,`i18n.interpolation.duration_ms`,`i18n.missing_key.total`,`i18n.format.<name>.duration_ms`,`i18n.format.cache_hit`,`i18n.backend.read.duration_ms{backend.name,outcome}`,`i18n.backend.read.total`,`i18n.backend.write.total`, and the observable`i18n.backend.cache.size` gauge. Twelve frozen `I18N.*`error codes (`MISSING_KEY` / `INVALID_LOCALE` / `INVALID_OPTIONS` / `LOADER_FAILED` / `INTERPOLATION_ERROR` / `PLURAL_RULE_NOT_FOUND` / `BACKEND_READ_FAILED` / `BACKEND_WRITE_FAILED` / `BACKEND_NOT_REGISTERED` / `BACKEND_LOCALE_NOT_AVAILABLE` / `INVALID_FORMATTER_OPTIONS` / `UNSUPPORTED_LOCALE` / `NESTING_DEPTH_EXCEEDED` / `PLUGIN_ALREADY_INSTALLED`). Do not confuse with @suaveplan/templates, which handles string interpolation in non-i18n contexts — this package is specifically for locale-keyed message catalogs and locale-sensitive formatting. |
|
||||
| `@suaveplan/i18n-backend-memory` | Universal-tier in-memory translation backend for the @suaveplan/i18n family. Ships `MemoryBackend` (full 5-flag capability matrix — `canRead/canReadMulti/canSave/canTrackModification: true`, `canWatch: false`) and `memoryBackend()` factory. Backed by `Map<locale, Map<namespace, TranslationRecord>>` plus a modification ledger keyed by `${locale} ${namespace}`. Implements every `Backend` method (`read` / `readMulti` / `save` / `lastModified`) with optional `signal?: AbortSignal` parameters that throw `I18nError(I18N.BACKEND_TIMEOUT)` on pre-aborted signals. Constructor accepts injectable `now: () => number` for deterministic `lastModified` testing. Zod boundary validation via `MemoryBackendOptionsSchema` throws `I18nError(I18N.INVALID_OPTIONS)` on malformed input. OTel surface: `i18n.backend.memory.read_duration_ms` (histogram), `read_total{outcome}` (counter), `read_errors_total{code}` (counter), `write_duration_ms` (histogram), `write_total{outcome}` (counter). Tested with the shared `runBackendConformance(...)` harness from `@suaveplan/i18n-shared/testing`. Universal — runs identically in browser and server with no platform-specific imports. Zero runtime dependency on `@suaveplan/i18n` core; depends only on `@suaveplan/i18n-shared`, `@suaveplan/types`, `@suaveplan/error`, `@opentelemetry/api`, `tslib`, and `zod`. |
|
||||
| `@suaveplan/i18n-backend-http` | Universal-tier `fetch`-based translation backend for the @suaveplan/i18n family. Ships `HttpBackend` (read-only 5-flag capability matrix — `canRead: true`; `canReadMulti`/`canSave`/`canTrackModification`/`canWatch: false`) and `httpBackend()` factory. Fetches `<loadPath>` per `(locale, namespace)` via injectable `fetchFn` (defaults to `globalThis.fetch`; `defaultFetch()` returns a stub throwing `BACKEND_READ_FAILED` when `globalThis.fetch` is unavailable). URL templating substitutes `{locale}` and `{namespace}` placeholders with `encodeURIComponent`-applied values via the exported `buildUrl(loadPath, locale, namespace)` helper. Pluggable `parsePayload` (default `defaultParsePayload`: JSON-object parser rejecting arrays, null, and primitive payloads as `BACKEND_READ_FAILED` because translation records are always objects). Pluggable `customHeaders(locale, namespace)` builder merged into every `RequestInit`; `crossDomain: true` shorthand sets `credentials: "include"` + `mode: "cors"` (preserving any explicit `requestOptions` overrides). Optional `retry?: HttpBackendRetry` (structurally compatible with `@suaveplan/retry`'s `RetryPolicy` so consumers can pass that instance directly without `@suaveplan/i18n-backend-http` taking a hard dep) wraps every fetch. Optional `signal?: AbortSignal` on `read` throws `I18nError(I18N.BACKEND_TIMEOUT)` on pre-aborted signals (no network request issued) and is threaded into the underlying `RequestInit` so `fetch` can cancel mid-flight; AbortError / `ABORT_ERR` codes surfaced from `fetchFn` map to `BACKEND_TIMEOUT` so consumers can distinguish cancellation from transport faults. Error semantics: HTTP 404 → `I18nError(I18N.BACKEND_LOCALE_NOT_AVAILABLE)` (the chained-backend fallthrough signal); HTTP 5xx, network failures (`TypeError` from `fetch`), and parse failures → `I18nError(I18N.BACKEND_READ_FAILED)` (chain-stopping — transport failures must propagate, not silently degrade). Zod boundary validation via `HttpBackendOptionsSchema` throws `I18nError(I18N.INVALID_OPTIONS)` on malformed input. OTel surface: `i18n.backend.http.read_duration_ms{locale,namespace,outcome}` (histogram, milliseconds), `read_total{outcome}` (counter, `outcome ∈ hit / miss / error`), `read_errors_total{code,status?}` (counter — `code` is the `I18N.*` error code; `status` is the HTTP status string when the error originated from a known HTTP response, omitted for transport / parse failures). Tested with the shared `runBackendConformance(...)` harness from `@suaveplan/i18n-shared/testing`. Universal — runs identically in browsers, Bun, and Node.js >= 18. Zero runtime dependency on `@suaveplan/i18n` core, on `@suaveplan/retry`, or on any other backend package; depends only on `@suaveplan/i18n-shared`, `@suaveplan/types`, `@suaveplan/error`, `@opentelemetry/api`, `tslib`, and `zod`. |
|
||||
| `@suaveplan/i18n-backend-storage` | Universal-tier `AbstractStorage`-backed translation backend for the @suaveplan/i18n family. Ships `StorageBackend` (full 5-flag capability matrix — `canRead/canReadMulti/canSave/canTrackModification: true`, `canWatch: false`) and `storageBackend()` factory. Adapts any `AbstractStorage` instance from `@suaveplan/storage` (`MemoryStorage`, `OPFSStorage`, `IndexedDBStorage`, `S3Storage`, `NodeFsStorage`, `SqliteStorage`, `GarageStorage`, content-addressed CDNs, …) into a `Backend`. Three wire layouts driven by a constructor option: `single-key-per-namespace` (default — one storage key per `(locale, namespace)` at `<prefix>/<locale>/<namespace>.json`), `single-key-per-locale` (one storage key per locale at `<prefix>/<locale>.json` holding the whole namespace map; saves are read-modify-write of the locale map), `flat-keys` (one storage key per leaf string at `<prefix>:<locale>:<namespace>:<dotPath>`; reads list-by-prefix and reassemble; saves write each leaf independently). Pluggable codec via constructor `encode` / `decode`; the exported `defaultCodec` does JSON via `TextEncoder` / `TextDecoder` and rejects non-object payloads (arrays, null, primitives) as `BACKEND_READ_FAILED` because translation records are always objects in the contract. Optional `signal?: AbortSignal` on `read` / `readMulti` / `save` throws `I18nError(I18N.BACKEND_TIMEOUT)` on pre-aborted signals. Zod boundary validation via `StorageBackendOptionsSchema` throws `I18nError(I18N.INVALID_OPTIONS)` on malformed input or missing `storage`. `lastModified` consults `AbstractStorage.getMetadata().modifiedAt` (max across leaves for `flat-keys`); swallows storage exceptions and returns `undefined` rather than throw. Storage failures surface as `BACKEND_WRITE_FAILED` (with `cause`) preserving any inner `I18nError` unwrapped. OTel surface: `i18n.backend.storage.read_duration_ms` (histogram), `read_total{outcome,layout}` (counter — `outcome ∈ hit / miss / error`),`read_errors_total{code,layout}` (counter), `write_duration_ms` (histogram), `write_total{outcome,layout}` (counter — `outcome ∈ ok / error`). The`layout` attribute is a 3-value low-cardinality enum so production teams can chart per-layout latency. Tested with the shared `runBackendConformance(...)` harness from `@suaveplan/i18n-shared/testing` against all three layouts. Universal — runs identically in browser and server; the concrete `AbstractStorage` decides the deployment tier. Zero runtime dependency on `@suaveplan/i18n` core; depends on `@suaveplan/i18n-shared`,`@suaveplan/types`,`@suaveplan/storage`,`@suaveplan/error`,`@opentelemetry/api`,`tslib`, and`zod`. |
|
||||
| `@suaveplan/i18n-backend-chained` | Universal-tier chained backend orchestrator for the @suaveplan/i18n family. Ships `ChainedBackend` (capabilities matrix derived from union of children — `canRead: true`, `canSave/canTrackModification/canWatch` each follow `backends.some(...)`) and `chainedBackend()` factory accepting either an array shorthand or full options object. `read(locale, namespace, signal?)` walks `backends` in registration order; the first backend whose `read` resolves wins, subsequent backends are skipped. `BACKEND_LOCALE_NOT_AVAILABLE` triggers fallthrough to the next backend; `BACKEND_READ_FAILED` propagates immediately to stop the chain (transport failures must not silently degrade). After a successful read from backend N (where N > 0) the resolved record is written back to every preceding backend (0..N-1) that implements `save` — best-effort, per-backend save failures swallowed and never block the read return. When every backend in a non-empty chain reports `BACKEND_LOCALE_NOT_AVAILABLE` the chain throws the same code (preserves catalog-layer fallthrough semantics one level up); when the chain is empty at runtime past the schema check the chain throws `CHAINED_BACKEND_ALL_FAILED` defensively. `readMulti(locale, namespaces, signal?)` fans out concurrently and merges per `mergeStrategy` (`first-wins` default — earlier backends win on overlap, later backends fill gaps; `last-wins` — later backends win on overlap, earlier backends fill gaps); per-backend `readMulti` errors are swallowed (slot omitted, not propagated). `save(locale, namespace, record, signal?)` calls `save` on every save-capable child sequentially; failures surface as `BACKEND_WRITE_FAILED` with `cause` preserved (existing `I18nError` instances pass through unchanged). `lastModified` walks children in order returning the first defined timestamp. `init(context)` forwards in registration order; `dispose()` runs in reverse — both safely callable multiple times and skip children that do not implement the hook. Optional `signal?: AbortSignal` on `read` / `readMulti` / `save` throws `I18nError(I18N.BACKEND_TIMEOUT)` on pre-aborted signals before any child is called, and is threaded into every child call when the child accepts a third positional argument. Zod boundary validation via `ChainedBackendOptionsSchema` rejects empty `backends` arrays as `INVALID_OPTIONS`. OTel surface: `i18n.backend.chained.read_duration_ms{backend,locale,namespace,outcome}` (histogram), `read_total{backend,outcome}` (counter — `outcome ∈ hit / miss / error / cache_hit / cache_miss / primary` extending the base outcome set with chained-cache classification: single-backend chains report `primary`, multi-backend chains report `cache_hit` (index 0 served) or `cache_miss` (a later backend served after fallthrough), errors report `error`, total miss reports `miss`), `read_errors_total{backend,code}` (counter), `write_duration_ms{backend,outcome}` (histogram), `write_total{backend,outcome}` (counter), `fallthrough_total{from_backend,to_backend}` (counter — incremented on every `BACKEND_LOCALE_NOT_AVAILABLE`-driven fallthrough between adjacent links so production teams can size cache layers correctly). Tested with the shared `runBackendConformance(...)` harness from `@suaveplan/i18n-shared/testing`. Universal — runs identically in browser and server; the tier of any concrete chain depends on the constituent backends. Zero runtime dependency on `@suaveplan/i18n` core; depends on `@suaveplan/i18n-shared`, `@suaveplan/i18n-backend-memory`, `@suaveplan/types`, `@suaveplan/error`, `@opentelemetry/api`, `tslib`, and `zod`. |
|
||||
| `@suaveplan/i18n-backend-fs-multifile` | **Server-tier** Node `fs/promises`-backed translation backend for the @suaveplan/i18n family. Ships `MultiFileFsBackend` (5-flag capability matrix: `canRead: true`, `canReadMulti: false`, `canSave: true`, `canTrackModification: true`, `canWatch` follows the `watch` option) and `multiFileFsBackend()` factory. Three wire layouts driven by the `layout` option: `nested` (default — `<root>/<locale>/<namespace>.<ext>`), `flat` (`<root>/<locale>.<namespace>.<ext>`), and `glob` (caller-supplied pattern with `{locale}` / `{namespace}` placeholders joined to `root`). JSON encode and decode are built in (`JSON.parse` / `JSON.stringify`); YAML/YML and JSON5 decode auto-detected via dynamic `import()` of the `yaml` / `json5` peers (never declared as direct deps); a missing peer surfaces as `BACKEND_READ_FAILED` with `peer` in the error context so callers can install the right package. Optional `fs.watch` loop on `root` emits `loaded` events through `BackendInitContext.emitLoaded(locale, namespace, { backend })` for both `nested` and `flat` layouts so HMR and React Suspense consumers can refresh without polling; `dispose()` aborts the watcher and is idempotent. Error mapping: ENOENT / ENOTDIR → `BACKEND_LOCALE_NOT_AVAILABLE` (chained-backend fallthrough signal); other errno codes (`EACCES`, `EBUSY`, …) → `BACKEND_READ_FAILED`; `writeFile` failure → `BACKEND_WRITE_FAILED` with `cause` preserved (existing `I18nError` instances pass through unchanged); unsupported encode extension → `BACKEND_WRITE_FAILED`. Optional `signal?: AbortSignal` on `read` and `save` throws `I18nError(I18N.BACKEND_TIMEOUT)` on pre-aborted signals before any filesystem call. The `MultiFileFsLike` shim is fully injectable; defaults to `node:fs/promises` via dynamic `import("node:fs/promises")` so the package never resolves the Node built-in at module-evaluation time in browsers. Zod boundary validation via `MultiFileFsBackendOptionsSchema` throws `INVALID_OPTIONS` on bad `layout`, empty `name`, or `glob` layout without `globPattern`. OTel surface: `i18n.backend.fs_multifile.read_duration_ms{backend,locale,namespace,outcome,layout}` histogram, `read_total{backend,outcome,layout}` counter, `read_errors_total{backend,code,layout}` counter, `write_duration_ms{backend,outcome,layout}` histogram, `write_total{backend,outcome,layout}` counter — `layout` is a 3-value low-cardinality enum so production teams can split SLOs per layout (glob patterns hide expensive resolutions). `package.json` declares `engines.node: ">=20"` and omits the `browser` field; `src/backend/backend.ts` opens with a `// Server-tier package — uses node:fs/promises. Browser consumers: use @suaveplan/i18n-backend-http or @suaveplan/i18n-backend-storage.` marker comment so universal-tier consumers cannot accidentally bundle this package; the slim `@suaveplan/i18n` core (Wave 6) does NOT re-export it. Tested with the shared `runBackendConformance(...)` harness from `@suaveplan/i18n-shared/testing` against a real `node:fs.mkdtemp` tmpdir (no `node:fs` mocks per CLAUDE.md "no mocks" rule) plus impl-specific cases over an injected `MultiFileFsLike` shim. Zero runtime dependency on `@suaveplan/i18n` core, on any other backend package, or on `yaml` / `json5`; depends only on `@suaveplan/i18n-shared`, `@suaveplan/types`, `@suaveplan/error`, `@opentelemetry/api`, `tslib`, and `zod`. |
|
||||
| `@suaveplan/i18n-detector` | Universal-tier locale-detection chain for the @suaveplan/i18n family. Ships the chain orchestrator (`createDetectorChain` with sync-first, async-raced execution, environment gating, AbortSignal threading, optional `throwOnExhausted` opt-in surfacing `I18nError(I18N.DETECTOR_CHAIN_EXHAUSTED)`), the BCP-47 default negotiator (`bcp47Negotiate`), the standalone `getTextDirection` / `isRtl` direction helpers (consuming `RTL_LOCALES` from `@suaveplan/i18n-shared`), nine detector implementations (`querystring`, `userPreference`, `cookie`, `localStorage`, `htmlLang`, `acceptLanguage`, `navigator`, `cloudflareIpGeo`, `fallback`) — each gated by an `environment: "browser" \| "server" \| "both"` discriminator so the same package runs in either tier — and the `defaultDetectorChain` factory returning the canonical 8/9-entry chain. The `userPreferenceDetector` accepts an injectable `LoggerLike` from `@suaveplan/types` (defaults to no-op) so the package keeps zero non-shared dependencies; the `cloudflareIpGeoDetector` consumes `getAllCountries()` from `@suaveplan/locale-data` to map ISO 3166-1 alpha-2 country codes to BCP-47 locales with an optional override map; the `fallbackDetector` throws `I18nError(I18N.INVALID_LOCALE)` on empty / whitespace input at chain-build time. OTel surface: `i18n.detect` span with `resolved_locale` and `winning_detector` attributes, `i18n.detector.match_total{detector}` aggregate counter, `i18n.detector.detected_total{detector,locale}` per-locale counter, `i18n.detector.duration_ms{detector}` histogram, `i18n.detector.chain.exhausted_total` counter. Every detector ships under its own `./detectors/<name>` subpath plus a tree-shakeable root barrel; `"sideEffects": false` on `package.json`. Consolidates the chain orchestrator (previously misplaced in the `@suaveplan/i18n` monolith) with the eight individual detector implementations (previously misplaced in the React-tier `@suaveplan/react-i18n` package) into a single universal-tier package depending only on `@suaveplan/i18n-shared`, `@suaveplan/locale-data`, and `@suaveplan/types`. |
|
||||
| `@suaveplan/idempotency` | Idempotency key guard and deduplication for async operations. The package provides a complete pending → completed \ |
|
||||
| `@suaveplan/jwt-tenant` | JWT-based multi-tenant token issuance and verification for the Suaveplan monorepo. Ships an Ed25519 / ECDSA-P256 signing pipeline, in-memory JWKS store with rotation and expiration, JWKS HTTP endpoint formatter, token issuer and verifier with injectable clock, and structured JWTTENANT error codes — all backed by jose, Zod-validated configs, and OpenTelemetry instrumentation. |
|
||||
| `@suaveplan/logger` | Enterprise-grade structured logger for the Suaveplan monorepo. Provides a full pipeline: level filtering → OTel trace enrichment → sampling → redaction → sinks. Supports multiple output strategies (console, JSON/NDJSON, event bus, storage) with buffering and backpressure. |
|
||||
| `@suaveplan/audio-core` | Universal audio data model and processing primitives. Provides a paged `AudioDocument`, a non-destructive `PlanEngine`, a block-level `ProcessPipeline`, and a typed `OpRegistry` — all with zero browser or Node.js dependencies. |
|
||||
| `@suaveplan/audio-metadata` | Universal audio metadata extractor that parses ID3v1/v2, MP4/M4A atoms, FLAC Vorbis comments, Ogg Vorbis, WAV RIFF, and AIFF/AIFC containers, returning a normalised Track record. Works in both Node.js and browser environments — zero native dependencies. |
|
||||
| `@suaveplan/migration` | Database-agnostic schema migration runner with ledger tracking, checksum validation, dry-run reporting, distributed locking, event bus integration, and OTel instrumentation. |
|
||||
| `@suaveplan/mime` | Structured MIME-type detection, parsing, and registry for the Suaveplan platform. Provides RFC 7231 Content-Type parsing, magic-byte file signature detection, charset sniffing, and a 150+ entry IANA-sourced type database — all in a zero-dependency, tree-shakeable package. |
|
||||
| `@suaveplan/network` | Core network interfaces and runtime for the genesis mesh system. |
|
||||
| `@suaveplan/network-transport-websockets` | Universal-tier WebSocket `Transport` + `NetworkPlugin` for `@suaveplan/network` mesh built on top of the pre-existing `WebSocketClient` (typed message framing, heartbeat/ping-pong, backpressure-aware queue, injectable `WebSocketAdapter`). `WebSocketTransport` composes per-peer `WebSocketClient` instances keyed by `peerId`: `connect(peerId, address, signal?)` with `PEER_ALREADY_CONNECTED` duplicate-key rejection, `disconnect(peerId)` no-op-on-unknown, `send(peerId, envelope)` raising `PEER_UNKNOWN` or `TRANSPORT_CLOSED`, fan-in `onMessage(handler)` returning disposer, `getConnectedPeers()` snapshot, idempotent `close()`, optional `maxPeers` enforcement raising `MAX_PEERS_EXCEEDED`. `WebSocketPlugin` mirrors `WebRTCPlugin`: `name="websockets"`, `install(node)` registers via `node.registerTransport(this.transport)`, `start()` lazy no-op, `stop()` closes + unregisters, `dispose()` idempotent. Extended `defineCodes("NET_WS", [...])` registry with 4 new transport-level codes on top of the existing client codes. OTel surface: `ws_transport.connect`/`disconnect`/`send` spans, `ws_transport.peers_connected{outcome}` + `ws_transport.messages_total{direction,outcome}` counters, observable `ws_transport.active_peers` gauge. `AbortSignal` threaded through the new connect path. New `./transport`, `./plugin`, `./otel` subpath exports; existing `./client`/`./queue`/`./heartbeat`/`./adapters`/`./types` preserved. fast-check property test on per-peer registry duplicate-key rejection invariant. Conformance tests run against a real `ws` `WebSocketServer` (no mocks); edge tests run against an injectable `WebSocketAdapter`. Every barrel uses explicit named exports per CLAUDE.md §6a; every test file imports runner primitives from `@suaveplan/testing/runner` per §7. 88 tests (60 baseline + 23 conformance + 5 edge), 100% statement/branch/function/line coverage. Deps: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/network`, `@suaveplan/retry`, `@suaveplan/serialize`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`. |
|
||||
| `@suaveplan/oauth-client` | Universal OAuth 2.0 / OIDC client for Node.js, browsers, Bun, Deno, and edge runtimes. |
|
||||
| `@suaveplan/opentelemetry` | OpenTelemetry SDK setup and configuration helpers for the Suaveplan monorepo. Provides batteries-included OTel initialization helpers, resource detection, ergonomic span/metrics wrappers, W3C trace-context propagation utilities, and a no-op SDK façade that works without any heavyweight @opentelemetry/sdk- packages. |
|
||||
| `@suaveplan/packer` | Binary packing/unpacking utilities. Two modes: |
|
||||
| `@suaveplan/pagination` | Universal pagination primitives for cursor-based, offset-based, and keyset-based (seek) pagination strategies. The package covers the full pagination surface: request normalisation, response envelope construction, opaque cursor and page-token encoding, async iterators that drain pages automatically, a pluggable adapter interface for any data source, and optional OpenTelemetry instrumentation. All three strategies share the same error hierarchy and Zod schemas so they compose cleanly in multi-strategy APIs. |
|
||||
| `@suaveplan/password-formats` | Universal import/export layer for common password-manager export formats. Parses Apple Passwords CSV, Bitwarden JSON, Chrome CSV, and KeePass XML into a normalized PasswordEntry[] array, and exports back to a universal CSV format. |
|
||||
| `@suaveplan/permissions` | Full RBAC/ABAC permissions engine for the Suaveplan monorepo. Ships role management, policy evaluation, CASL-inspired ability builder, TTL permission cache, condition evaluation, and framework-agnostic middleware. |
|
||||
| `@suaveplan/qrcode` | QR code generation producing SVG, pixel matrix, terminal (Unicode blocks), and data URL representations. Implements QR code versions 1-10, error correction levels L/M/Q/H, automatic version and mode selection. |
|
||||
| `@suaveplan/queue` | Enterprise-grade queue management: memory/priority/delayed queues, concurrent workers with retry and dead-letter forwarding, job state machines, and OTel instrumentation. |
|
||||
| `@suaveplan/rate-limiter` | Enterprise rate limiter with three algorithms, rule-based routing, tenant multipliers, pluggable storage, and OTel instrumentation. |
|
||||
| `@suaveplan/react-i18n` | React bindings for `@suaveplan/i18n`. Ships the `<TranslationProvider>` (with caller-managed and Provider-managed instance modes), twelve hooks (`useI18n`, `useTranslation`, `useTranslate`, `useLocale`, `useDirection`, `useErrorMessage`, `usePlural`, `useDate`, `useNumber`, `useCurrency`, `useRelativeTime`, `useList`, `useLocaleDetection`), four headless components (`<Trans>` with rich-text tag interpolation via `MessageParser.formatChunks<ReactNode>`, `<LanguageSwitcher>` render-prop with `useTransition` wrapping, `<MissingKeyOverlay>` dev-mode overlay capped at 50 entries with `aria-live="polite"`, `<I18nSuspense>` convenience wrapper with default `aria-live` fallback), the `withTranslation` HOC for class components, the nine built-in locale detectors (`querystring`, `userPreference`, `cookie`, `localStorage`, `htmlLang`, `acceptLanguage`, `navigator`, `cloudflareIpGeo`, `fallback`) plus `defaultDetectorChain` and the `DetectorChainProvider` context wiring, the branded `TranslationKey` typing contract with `AppTranslationKeys` (consumer-app augmentation) and `ContributedTranslationKeys` (downstream-package augmentation), the `tDynamic` runtime escape hatch with the `REACT_I18N.UNKNOWN_KEY` code, the five-code `REACT_I18N_CODES` registry (`NO_PROVIDER`, `NAMESPACE_LOAD_FAILED`, `HYDRATION_MISMATCH`, `TRANS_INVALID_CHILD`, `UNKNOWN_KEY`), the React-tier OTel instruments (counters `react_i18n.missing_key.total` and `react_i18n.suspense.fallback.total`, histogram `react_i18n.render.duration_ms`, plus the detector telemetry `i18n.detector.match_total{detector}` / `i18n.detector.duration_ms{detector}` / span `i18n.detect`), and the warn-once / identity-translator soft-failure substrate. Leaf-most React package — the `check-no-backwards-deps.ts` CI gate enforces that no leaf React package (`react-ui*`, `react-forms*`, `react-ui-theme-*`, `react-ui-pack-*`) appears in any dep bucket so downstream `ContributedTranslationKeys` augmentation stays sound. Supersedes the legacy `@suaveplan/react-translate` (i18next-based) proposal. |
|
||||
| `@suaveplan/retry` | Retry strategies and circuit-breaker for async operations. The package provides constant, linear, and exponential backoff with four jitter modes (full, equal, decorrelated, none), a sleep helper with AbortSignal support, a per-attempt withTimeout wrapper, and a three-state circuit breaker (closed → open → half-open) with event emission and configurable failure/success thresholds. All entry points accept an injectable Clock and AbortSignal so they integrate cleanly with test harnesses and request cancellation. |
|
||||
| `@suaveplan/schema-registry` | Versioned schema registry with forward/backward/full compatibility checking for evolving data contracts. Register schemas by subject name, enforce evolution rules, and detect breaking changes before they reach production. |
|
||||
| `@suaveplan/search` | Full-text and fuzzy search primitives with in-memory BM25 engine, trigram fuzzy matching, faceted search, and pagination support. |
|
||||
| `@suaveplan/security-codes` | Detect and sanitise sensitive security-code leakage in strings. Handles API |
|
||||
| `@suaveplan/serialize` | Round-trip-preserving structured serialization for JavaScript's rich type system. The standard JSON.stringify/JSON.parse pair silently drops Date objects, Map, Set, BigInt, Uint8Array, URL, RegExp, undefined, NaN, and ±Infinity. This package preserves all of them via a tagged-JSON codec (serialize/deserialize) and an efficient MessagePack binary codec (encodeBinary/decodeBinary). It also provides 4-byte-framed streaming codecs for ReadableStream pipelines, cycle detection with path context, a pluggable type registry for custom classes, and an opt-in /unsafe subpath that handles Function-bearing payloads. The default entry point is eval-safe. Use @suaveplan/serialize whenever you need to serialise application state across process boundaries, persist structured data, or stream binary payloads — and want to get back exactly what you put in. |
|
||||
| `@suaveplan/sitemap` | Sitemap generation and parsing for the sitemaps.org XML protocol. Produces valid sitemap.xml, sitemap index documents, and parses existing sitemaps. Supports Google image and video extensions as well as hreflang alternate links. |
|
||||
| `@suaveplan/slsa` | SLSA L1-L2 supply-chain security for the Suaveplan monorepo. Generates and |
|
||||
| `@suaveplan/storage` | Enterprise key-value storage abstractions for the genesis monorepo. Provides AbstractStorage (implementing StorageLike), MemoryStorage with TTL and zero-copy streaming, NamespacedStorage with scoped operations, and StorageFactory with a module-augmentation provider registry. |
|
||||
| `@suaveplan/storage-cdn` | Types-only foundation for CDN-flavoured `@suaveplan/storage` adapters. Declares `CdnAdapter` (abstract base class extending `AbstractStorage` with `purge`, `signUrl`, `edgeUrl`), the `CdnCapabilities` boolean matrix, Zod-validated `SignUrlOptions`, the `STORAGE_CDN` error registry (8 codes), type-guard helpers (`isCdnAdapter`/`assertCdnAdapter`), default-not-supported helpers (`defaultPurgeNotSupported` / `defaultSignUrlNotSupported` / `defaultPrefixPurgeNotSupported`), and the module augmentation that adds an optional `cdn?: CdnCapabilities` slot to `@suaveplan/storage`'s capabilities map. Universal-tier; provider packages (`storage-cdn-rackspace`, `storage-cdn-bunnycdn`) implement against this contract. |
|
||||
| `@suaveplan/storage-cdn-bunnycdn` | BunnyCDN Storage Zone + Pull Zone provider for `@suaveplan/storage-cdn`. Full `AbstractStorage`/`CdnAdapter` surface against the regional Storage Zone (`de` unprefixed, `ny`/`la`/`sg`/`syd`/`uk` prefixed) with `AccessKey`-header authentication, HMAC-SHA256 Token Authentication signed URLs (URL-safe base64, no padding), single + wildcard prefix purge via `api.bunny.net/purge`, Range reads with 206/200/416 disambiguation, native `ReadableStream` from `Response.body`, AbortSignal everywhere, transient-status retry honouring `Retry-After`, full OTel surface with `provider="bunnycdn"` + `storage_region` attributes. Two-key model: per-zone `storageAccessKey` for CRUD vs. account-level `purgeApiKey` for invalidation; rotated independently. The only adapter in the family with `cdnPrefixPurge: true`. |
|
||||
| `@suaveplan/storage-cdn-rackspace` | Rackspace Cloud Files (OpenStack Swift) origin + Rackspace CDN provider for `@suaveplan/storage-cdn`. v2 identity auth with proactive token refresh (five minutes before `expires`), concurrent-call dedup, and service-catalog endpoint lookup for LON/IAD/DFW/ORD/SYD/HKG. HMAC-SHA1 Swift TempURL signing (with `temp-url-prefix` mode support), CDN cache purge per object via the management endpoint, Range reads, native streaming via `Response.body`, AbortSignal threaded through every public method, transient-status retry honouring `Retry-After`, full OTel surface with `provider="rackspace"` + `region` attributes. Universal tier — runs in Node 18+, Bun, Deno, browsers, Cloudflare Workers. |
|
||||
| `@suaveplan/storage-http` | Universal HTTP/CDN-backed `AbstractStorage` adapter. Maps every storage operation onto an HTTP request against a configured `baseUrl + key`: full read surface (`GET`/`HEAD`/`Range`/native `ReadableStream` streaming/ETag-cached conditional reads via `If-None-Match` → `304 Not Modified`), optional writes through an injectable `HttpUploader`, AbortSignal threaded into every fetch (pre-aborted short-circuits, mid-flight propagates), retry on transient outcomes (`408`/`425`/`429`/`5xx` + network rejection) honouring `Retry-After`, `STORAGE_HTTP` error registry with 10 codes, OTel surface (`storage_http.*` instruments with `status_class` attribute, `range_fallback_total` counter for misconfigured origins). The natural binding for static-site CDNs, prefix-readable S3 buckets, public BunnyCDN Pull Zones, GitHub Pages, Caddy file servers — provider-specific adapters (`storage-cdn-rackspace`, `storage-cdn-bunnycdn`) layer on top via the `HttpUploader` slot. Universal tier; runs unchanged in Node 18+, Bun, Deno, browsers, Cloudflare Workers. |
|
||||
| `@suaveplan/storage-s3` | S3-compatible object storage adapter for @suaveplan/storage. Extends AbstractStorage backed by @aws-sdk/client-s3 with presigned URL generation, multipart upload, retry integration, and OpenTelemetry instrumentation. |
|
||||
| `@suaveplan/storage-plugin-cache` | Read-through cache decorator for any `@suaveplan/storage` `AbstractStorage`. Returns `CachedStorage extends AbstractStorage` that wraps an inner storage with a caller-supplied `AbstractCache<Uint8Array>` in front — the structural inverse of `@suaveplan/cache-storage` (which exposes a storage *as* a cache). Read-through `get(key)` consults the cache first, falls through to the inner on miss, populates when `shouldCache(key, bytes)` returns true (default ceiling 16 MiB matching `drive-mesh-protocol`'s `MAX_FRAME_BYTES`). Invalidates on `set`/`setPartial`/`delete`/`clear` post-success. `clear()` evicts only the wrapper's namespaced subset via `cache.keys()` enumeration; logs a warning and skips when the cache backend lacks `keys()` (avoids obliterating other wrappers' entries). Passthrough on `exists` / `list` / `getMetadata` / `getWithMetadata` / `getSize` / `getItemSize` / `getPartial` / `createReadStream` / `createWriteStream`. 2-code `STORAGE_CACHE.*` registry (`INVALID_OPTIONS` / `INNER_MISSING`); class is `CachedStorageError` (distinct from `cache-storage`'s `StorageCacheError`). Cache failures never propagate — they're counter-recorded but the inner storage's bytes still reach the caller. Multi-namespace isolation on a shared cache (`namespace::key` keys). Full OTel surface (`storage_cache.{hits,misses,populations,invalidations,errors}_total{namespace}` + `entry_bytes` histogram + outcome-tagged spans). Universal tier, zero `node:`/`bun:` imports; composes uniformly with `drive`, `storage-uri` plugins, and any application code that accepts a `StorageLike`. |
|
||||
| `@suaveplan/storage-uri` | Universal URI-routed dispatcher over any `StorageLike` adapter. A single `StorageUriResolver` maps `scheme → UriPlugin` (`file://`, `https://`, `s3://`, `rackspace://`, `bunny://`, …) and exposes `open` / `read` / `write` / `stat` / `exists` / `remove` / `list` keyed by URI. Streaming reads forward `adapter.createReadStream` when present, fall back to a one-chunk wrap of `adapter.get` otherwise (counter-observed). Streaming writes drain via `Response(body).arrayBuffer()` for adapters without `createWriteStream`. AbortSignal propagation. 8-code `STORAGE_URI.*` error registry; `SuaveplanError` subclasses thrown by adapters pass through with their original code so downstream packages (`drive-uri`, `storage-http`, …) surface namespaced codes directly to consumers. Full OTel surface; `runUriPluginConformance(plugin, fixtures)` harness exported for sibling plugin packages. Universal tier — zero `node:`/`bun:` imports; runs unchanged in Node 18+, Bun, Deno, browsers, Cloudflare Workers. |
|
||||
| `@suaveplan/streams` | WHATWG-Streams-based primitives: backpressure-first transforms, zero-copy Uint8Array operations, AbortSignal integration end-to-end, structured errors, OTel instrumentation, and property-based invariant tests. |
|
||||
| `@suaveplan/suppression` | Email suppression list management for the Suaveplan monorepo. Stores and checks email addresses that should not receive outgoing mail — unsubscribes, hard/soft bounces, complaints, manual suppressions, and invalid addresses. Supports domain-level blocking, bulk CSV/JSON import and export, OTel instrumentation, and full serialisation round-trips. |
|
||||
| `@suaveplan/surrealdb` | Universal SurrealDB client wrapper for the genesis monorepo. Implements @suaveplan/db's DatabaseAdapter interface with connection lifecycle management, typed CRUD, schema discovery, database initialisation, version detection, structured SURREAL. error codes, and OTel instrumentation. |
|
||||
| `@suaveplan/templates` | A plugin-based string template engine for TypeScript. Resolves {variable} and {variable:transform} tokens in strings using a priority-sorted, regex-scan pipeline — no eval, no AST parser overhead. Designed for filling templates in notification messages, email subjects, file name patterns, code generation, and similar string-expansion tasks. Ships nine built-in plugins: dot-notation and array-index variable resolution; case, regex, and split transforms; counter, datetime, and UUID dynamic generators; and conditional/loop control flow. Three preset bundles (minimal, standard, full) let you trade features for bundle size. The injectable Clock and Rng make every rendering call deterministic in tests. Do not confuse with @suaveplan/i18n, which handles locale-keyed message catalogs and plural forms — use this package when your source strings are already in the target language and you only need token substitution. |
|
||||
| `@suaveplan/testing` | Shared test infrastructure for the Suaveplan monorepo. Provides runner primitives (Vitest or bun-test via conditional exports), structured builders, a deterministic FakeClock, Factory and FixtureManager, an in-memory store, async helpers, and a WSL2-aware createTestConfig() factory. Every other @suaveplan/ package imports from here when writing tests. |
|
||||
| `@suaveplan/testing-dom` | DOM and React component testing utilities for the Suaveplan monorepo. Provides a single install that covers vitest config generation with WSL2-safe defaults, @testing-library/react render helpers, accessibility-first query aliases, @testing-library/jest-dom matchers, user-event v14 setup, and a dual-runner shim that routes between vitest and bun:test via package.json conditional exports. |
|
||||
| `@suaveplan/testing-e2e` | Playwright E2E testing utilities for the Suaveplan monorepo. Provides a config factory with WSL2/CI-safe defaults, an abstract PageObject base class, extended test fixtures (apiContext, freshPage), browser-context helpers (clipboard mock, geolocation mock, network-idle wait, screenshot-on-failure), and structured error types. The package has no @suaveplan/ runtime dependencies — it is a Layer 0 utility that anything in the stack can depend on without introducing cycles. |
|
||||
| `@suaveplan/timing` | Timing and scheduling utilities for application-layer time management. The package covers measurement (stopwatch with lap recording), rate control (throttle and debounce with leading/trailing edge and maxWait), value decay (exponential and linear curves), delayed-task scheduling with drain/dispose lifecycle, repeating interval timers, countdowns with pause/resume, and vector clocks for distributed happens-before ordering. All stateful primitives accept an injectable Clock so they can be driven deterministically in tests using FakeClock from @suaveplan/testing. |
|
||||
| `@suaveplan/typescript-config` | Shared TypeScript configuration presets for the Suaveplan monorepo. This package ships composable JSON presets consumed via "extends" across every workspace package — no runtime code, no build step, zero dependencies. |
|
||||
| `@suaveplan/undo` | Enterprise-grade undo/redo engine with branching history, async commands, history compression, selective undo, and command merging. Built for collaborative editors, document tools, and any application requiring robust, reversible state management. |
|
||||
| `@suaveplan/utils` | Zero-external-dependency utility library for the Suaveplan monorepo. Covers debug helpers, deep object manipulation, platform detection, IEEE 754 float packing, pseudo-random selection, email normalization, audio sprite management, string generation, hex validation, and byte-size enforcement. |
|
||||
| `@suaveplan/validation` | Runtime schema validation with a Zod-compatible API. This package is a self-contained parse engine — it does not import Zod or any other validation library — so it is safe to use in environments where Zod is absent or where bundle size is a constraint. It covers the full schema surface expected from a production validator: all primitive and composite types, parse/safeParse/parseAsync, refine (sync + async), transform, describe, object shape builders (pick, omit, partial, extend, merge), three unknown-key modes, async lifecycle hooks, and composable helpers (pipe, mergeResults). Structured errors surface via @suaveplan/error with stable VALIDATION. codes. When Zod is already a project dependency and you need its ecosystem (inference helpers, plugins), use Zod directly; use this package for internal platform code and zero-dep environments. |
|
||||
| `@suaveplan/webhook-client` | Webhook delivery client with retry logic, HMAC signature generation/verification, payload transformation, and delivery tracking. Runtime-agnostic (injectable fetch). |
|
||||
|
||||
## Pending
|
||||
|
||||
**253** packages from suaveplan awaiting migration (per `STATUS.md` –, refreshed 2026-05-12).
|
||||
|
||||
| Package | Summary |
|
||||
| --- | --- |
|
||||
| `@suaveplan/analytics-browser` | Client-side event tracking with batched transport |
|
||||
| `@suaveplan/animation-events` | Animation event dispatching, foot contact detection, procedural modifiers |
|
||||
| `@suaveplan/api` | Core API framework with plugin system |
|
||||
| `@suaveplan/api-docs` | API documentation generator |
|
||||
| `@suaveplan/api-gateway` | API gateway with routing, load balancing, circuit br... |
|
||||
| `@suaveplan/api-plugin-admin` | Admin panel plugin |
|
||||
| `@suaveplan/api-plugin-analytics` | Product analytics, funnel/cohort analysis |
|
||||
| `@suaveplan/api-plugin-anticheat` | Server-side anti-cheat with speed, damage, rate, and teleport validation |
|
||||
| `@suaveplan/api-plugin-asset-register` | Asset register — fixed assets, depreciation, mainten... |
|
||||
| `@suaveplan/api-plugin-audit` | Audit trail plugin |
|
||||
| `@suaveplan/api-plugin-auth` | Authentication plugin |
|
||||
| `@suaveplan/api-plugin-bank` | Game bank with virtual currency + interest |
|
||||
| `@suaveplan/api-plugin-banking` | Banking/payment plugin |
|
||||
| `@suaveplan/api-plugin-campaigns` | Campaign management API plugin |
|
||||
| `@suaveplan/api-plugin-cms` | Content management plugin |
|
||||
| `@suaveplan/api-plugin-contacts` | Contacts and groups API plugin |
|
||||
| `@suaveplan/api-plugin-customs` | Cross-border customs — declarations, HS codes, borde... |
|
||||
| `@suaveplan/api-plugin-database` | Database management plugin |
|
||||
| `@suaveplan/api-plugin-events` | Events management plugin |
|
||||
| `@suaveplan/api-plugin-export` | Data export (CSV, JSON, XLSX) |
|
||||
| `@suaveplan/api-plugin-file-upload` | Multipart/chunked file uploads with validation |
|
||||
| `@suaveplan/api-plugin-game-persistence` | Game save/load with slots, auto-save, and size limits |
|
||||
| `@suaveplan/api-plugin-game-server` | Game server session management with tick rate and heartbeats |
|
||||
| `@suaveplan/api-plugin-graphql` | GraphQL adapter with schema building |
|
||||
| `@suaveplan/api-plugin-gtap` | GTAP integration plugin |
|
||||
| `@suaveplan/api-plugin-import` | Data import plugin |
|
||||
| `@suaveplan/api-plugin-inventory` | Inventory management plugin — items, stock levels, m... |
|
||||
| `@suaveplan/api-plugin-logistics` | Enterprise logistics — shipments, carriers, pallets,... |
|
||||
| `@suaveplan/api-plugin-matchmaking` | Skill-based matchmaking with queue management and gap expansion |
|
||||
| `@suaveplan/api-plugin-messaging` | Messaging plugin |
|
||||
| `@suaveplan/api-plugin-notifications` | Notifications plugin |
|
||||
| `@suaveplan/api-plugin-payment` | Payment intent lifecycle, refunds, webhooks |
|
||||
| `@suaveplan/api-plugin-port-tracking` | Port tracking — vessel positions, port calls, shippi... |
|
||||
| `@suaveplan/api-plugin-procurement` | Procurement — purchase orders, goods receipt notes, ... |
|
||||
| `@suaveplan/api-plugin-rate-limit` | API-level rate limiting |
|
||||
| `@suaveplan/api-plugin-search` | Full-text search with relevance scoring |
|
||||
| `@suaveplan/api-plugin-site` | Site management plugin |
|
||||
| `@suaveplan/api-plugin-social` | Social features plugin |
|
||||
| `@suaveplan/api-plugin-sse` | Server-Sent Events for real-time push |
|
||||
| `@suaveplan/api-plugin-store` | E-commerce store plugin |
|
||||
| `@suaveplan/api-plugin-subscription` | Subscription/recurring billing plugin |
|
||||
| `@suaveplan/api-plugin-tenant` | Multi-tenant management plugin |
|
||||
| `@suaveplan/api-plugin-user` | User management plugin |
|
||||
| `@suaveplan/api-plugin-vatdash-bridge` | Bridge wiring all api-plugins into the vatdash Hono server |
|
||||
| `@suaveplan/api-plugin-vault` | Secrets vault plugin |
|
||||
| `@suaveplan/api-plugin-virtual-currency` | Virtual currency wallets, transfers, purchases, cashout |
|
||||
| `@suaveplan/api-plugin-warehouse-ops` | Warehouse operations — pick/pack/ship, cycle counts,... |
|
||||
| `@suaveplan/api-plugin-webhook` | Webhook delivery with retries and signing |
|
||||
| `@suaveplan/api-shared` | Shared API utilities and types |
|
||||
| `@suaveplan/audience-rules` | Dynamic audience segmentation using rule-based filte... |
|
||||
| `@suaveplan/audio-controllers` | MIDI hardware controller plugins for Kontrol S88, Ma... |
|
||||
| `@suaveplan/audio-engine-browser` | Browser-tier `TimelineAdapter` implementation: Web Audio rendering, AudioWorklet hosting, and `AudioFxPlugin` `createAudioNode()` integration for the universal `TimelineEngine`. |
|
||||
| `@suaveplan/audio-engine-server` | Server-tier `TimelineAdapter` implementation: file source/sink adapters and offline render driver for the universal `TimelineEngine`. |
|
||||
| `@suaveplan/audio-plugins-fx` | DSP audio effect plugins for the Web Audio API — EQ,... |
|
||||
| `@suaveplan/audio-sprite-packer` | Binary audio sprite map packer with 80%+ compression... |
|
||||
| `@suaveplan/audio-timeline` | Headless DAW timeline engine with multi-track arrangements, clips, playback, loop regions, selection, zoom, per-track mixer state, undo/redo, and a pluggable TimelineAdapter. |
|
||||
| `@suaveplan/audio-transport` | Universal musical clock and transport: sample-accurate scheduling with PPQ, BPM automation, swing, time signatures, and loop regions. Zero browser or Node dependencies. |
|
||||
| `@suaveplan/behavior-tree` | Behavior tree engine with composite nodes, decorators, blackboard, and fluent builder |
|
||||
| `@suaveplan/bone-mapping` | Skeleton detection, bone matching, retargeting math |
|
||||
| `@suaveplan/booking` | Universal-tier JSON-configurable booking engine: pluggable availability resolution, four built-in conflict detectors, pricing pipeline (base + tiers + modifiers + tax + seasonal + dynamic), rule evaluator with 11 operators, configurable status state machine, typed event bus over 15 lifecycle events, in-package `Hooks<TMap>` transform pipeline, six domain presets (seats / accommodation / appointments / resources / activities / transport), Zod schemas, and typed `BookingError` with 7-code registry. 29 subpath exports, 150 tests at 100/100/100/100. |
|
||||
| `@suaveplan/broadcast-channel` | Cross-tab BroadcastChannel communication |
|
||||
| `@suaveplan/building` | Player building system with snap placement, structural integrity, and blueprints |
|
||||
| `@suaveplan/caddy-imgproxy` | Image proxy integration |
|
||||
| `@suaveplan/campaigns` | Campaign orchestration combining audience, template,... |
|
||||
| `@suaveplan/character-physics` | Engine-agnostic character controller physics (floating capsule, plugins) |
|
||||
| `@suaveplan/character-vehicle-picker` | Character/vehicle picker schemas, catalogs, and state management with BitPacker |
|
||||
| `@suaveplan/chat` | In-game chat with channels, whispers, rate limiting, and profanity filter |
|
||||
| `@suaveplan/clipboard` | Clipboard API abstraction with fallbacks |
|
||||
| `@suaveplan/combat` | Core combat mechanics with damage types, health, armor, shields, and status effects |
|
||||
| `@suaveplan/contacts` | Contact and group management with hierarchical nesti... |
|
||||
| `@suaveplan/controller` | Controller abstractions |
|
||||
| `@suaveplan/crafting` | Recipe-based crafting with stations, material requirements, quality tiers, and queues |
|
||||
| `@suaveplan/currency` | Universal-tier currency conversion engine — `CurrencyConverter` orchestrator combining a pluggable `CurrencyProvider` chain (primary + ordered `fallbackProviders`), TTL-bounded `RateCache` with direct / inverse / single-hop triangulated lookup, virtual-currency seeding (registered through `@suaveplan/money` `registerCurrency`) with post-construction `setVirtualRate` / `updateVirtualRate` (aliases) + `getVirtualRates` defensive-copy inspector, `AbortSignal`-aware fallback chain that stops between attempts on abort, and Zod-validated public boundary on every options bundle (`ConverterConfigSchema`, `ConvertOptionsSchema`, `VirtualCurrencyConfigSchema`, `RateFetchOptionsSchema`) plus `parseConverterConfig` / `parseConvertOptions` / `parseVirtualCurrencyConfig` / `parseRateFetchOptions` helpers. Enriched `ConversionResult { amount, rate, inverseRate, from, to, rateAgeMs, providerName, triangulatedThrough }` — `providerName` is `"<identity>"` for `from === to`, `"<cache>"` for cache hits, `"<virtual>"` for virtual-currency rates, or the actual provider's `name`; `triangulatedThrough` is the intermediate base for triangulated rates (or `null` for direct / inverse / identity). Public methods: `convert(money, to, options?)`, `convertAmount(amount, from, to, options?)`, `getRate(from, to, options?)`, `setRate(from, to, rate)`, `clearCache()`, `getCache()`, plus the virtual mutators. `RateCache` exposes `set` / `get` / `has` / `getRate` / `getTriangulationPath` / `getTimestamp` / `prune` / `clear` / `size` and skips zero rates during inverse / triangulation to avoid `Infinity`. Reference providers: `InMemoryProvider` (mutable `setRates` / `setRate` + `fetchRates`) and `createStaticProvider(name, rateMap, options?)` (frozen). Typed `CurrencyError extends SuaveplanError<CurrencyErrorCode>` with 7-code registry (`INVALID_CONFIG`, `INVALID_OPTIONS`, `PROVIDER_UNAVAILABLE`, `UNSUPPORTED_CURRENCY`, `RATE_UNAVAILABLE`, `TRIANGULATION_IMPOSSIBLE`, `ABORTED`) and optional `from` / `to` / `providerName` discriminants — every code is reachable from a real call site. 6 subpath exports (`./converter/converter`, `./rate-cache/rate-cache`, `./provider/provider`, `./error-codes/error-codes`, `./schemas/schemas`, `./types/common`) for tree-shaking. 107 tests across 5 files at 100/100/100/100 (statements / branches / functions / lines) istanbul coverage. Universal-tier — only `Date.now`, `Map`, `AbortSignal`; no `node:`/`bun:`/DOM/fs imports. Deps: `@suaveplan/error`, `@suaveplan/money`, `tslib`, `zod`. |
|
||||
| `@suaveplan/currency-provider-binance` | Universal-tier Binance sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Public surface: `BinanceProvider implements CurrencyProvider` (`name = "binance"`, `supportsCrypto = true`, `supportsFiat = false`) plus the stable constants `BINANCE_PROVIDER_NAME` (`"binance"`) and `BINANCE_DEFAULT_BASE_URL` (`"https://api.binance.com/api/v3"`). Constructor accepts an optional `NoKeyConfig & { readonly timeoutMs?: number }` — `baseUrl` (override for staging / proxies / the regional `api{1,2,3,4}.binance.com` mirrors), `fetch` (`FetchLike` from `@suaveplan/types`, default `globalThis.fetch`), `timeoutMs` (flows through to the shared `httpRequest()` helper which derives an `AbortSignal.timeout(ms)` composed with the caller's `signal`). No API key required — uses Binance's public spot ticker endpoint. `fetchRates(options?)` issues `GET ${baseUrl}/ticker/price`, routes through the shared `httpRequest()` helper (typed `CurrencyProviderError(HTTP_ERROR)` on non-2xx with `status` populated), parses the JSON body (`CurrencyProviderError(PARSE_ERROR)` on malformed body; `cause` attached only when the parser threw an `Error`), structurally validates the payload shape as `Array<{ symbol: string; price: string }>` (`CurrencyProviderError(UPSTREAM_ERROR)` for non-array root, null row, wrong-typed row, or missing field), defaults `options.base` to `"USDT"`, decodes tickers whose symbol ends in the base suffix via `endsWith` + length guard (a symbol equal to the suffix decodes to an empty key — defensively skipped), inverts the price into the converter's "1 base = N units of asset" convention via `1 / price` (skipping zero / negative / non-finite prices), optionally filters by `options.targets` and raises `CurrencyProviderError(UNKNOWN_CURRENCY)` (with `upstreamMessage` set to the missing target) when a requested target is absent — surfacing upstream listing changes immediately rather than silently dropping them as the upstream form did. Improvements over the upstream `BinanceProvider` (`../suaveplan/packages/finance/currency-providers/src/crypto/binance.ts`): typed errors (upstream threw plain `Error`), structural payload validation (upstream cast on faith), `Number.isFinite` price guard (upstream only checked `price > 0`), `AbortSignal` propagation through `RateFetchOptions.signal`, per-call timeout via `timeoutMs`, injectable `FetchLike`, eager `UNKNOWN_CURRENCY` on missing targets. 1 subpath export (`./binance/binance`); explicit named barrels everywhere. 21 tests at 100/100/100/100 (statements / branches / functions / lines) istanbul coverage covering identity, every happy-path branch, every sanitisation branch, every error code, abort + timeout forwarding, and the default-`globalThis.fetch` fallback. Universal-tier — only `Response`, `Number.parseFloat`, `Number.isFinite`, `Date.now`, `String.endsWith`/`.slice`, and the injected `FetchLike` (default `globalThis.fetch`); no `node:`/`bun:`/DOM/fs imports. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-coingecko` | Universal-tier CoinGecko sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Public surface: `CoinGeckoProvider implements CurrencyProvider` (`name = "coingecko"`, `supportsCrypto = true`, `supportsFiat = true`) with optional `CoinGeckoConfig extends NoKeyConfig & { readonly apiKey?: string }` — `baseUrl` (default `https://api.coingecko.com/api/v3`, override for the Pro `https://pro-api.coingecko.com/api/v3` endpoint or for staging / proxies), `fetch` (`FetchLike` from `@suaveplan/types`, default `globalThis.fetch`), `apiKey` (Pro tier; attached as `x-cg-pro-api-key` header on every outbound request). Both tiers share the same `/simple/price` request shape so free → Pro is a one-field config change. `fetchRates(options?)` builds `GET ${baseUrl}/simple/price?ids=<curated-ids>&vs_currencies=<base.toLowerCase()>`, routes through the shared `httpRequest()` helper (caller `AbortSignal` propagation, per-call timeouts, typed `CurrencyProviderError(HTTP_ERROR)` on non-2xx), pre-validates `options.targets` against the curated `COIN_IDS` map (16 cryptos: BTC, ETH, SOL, USDT, USDC, BNB, XRP, ADA, DOGE, DOT, AVAX, MATIC, LINK, UNI, ATOM, LTC) — unknown targets reject with `CurrencyProviderError(UNKNOWN_CURRENCY)` before any network round trip, naming the offending symbol — parses the JSON body (`CurrencyProviderError(PARSE_ERROR)` on malformed bodies; `cause` attached when the parser threw an `Error`), shape-guards the parsed value as `Record<string, Record<string, number>>` (`CurrencyProviderError(UPSTREAM_ERROR)` on wrong shape), inverts upstream "1 crypto = X fiat" prices into the converter's "1 base = X crypto" convention (skipping symbols whose upstream price is missing, zero, negative, or non-numeric), and returns `{ base, rates, timestamp: Date.now() }`. Also exposes `COIN_IDS: Readonly<Record<string, string>>` (frozen) and `getCoinGeckoId(symbol): string \| undefined` (case-insensitive lookup; returns `undefined` for unknown symbols, never throws). 2 subpath exports (`./coingecko/coingecko`, `./coin-ids/coin-ids`); explicit named barrels everywhere. 23 tests across 2 files at 100/100/100/100 (statements / branches / functions / lines) istanbul coverage covering both keyless and Pro-key branches, both error and happy paths, and the proactive `UNKNOWN_CURRENCY` gate (the recording `FetchLike` is asserted unused on the rejection branch). Universal-tier — only `URLSearchParams`-style URL building, `Response`, `Date.now`, `Map`, `Object.entries`, `Object.freeze`, and the injected `FetchLike` (default `globalThis.fetch`); no `node:`/`bun:`/DOM/fs imports. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-kraken` | Universal-tier Kraken sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Public surface: `KrakenProvider implements CurrencyProvider` (`name = "kraken"`, `supportsCrypto = true`, `supportsFiat = false`) constructed from `KrakenProviderConfig extends NoKeyConfig` — optional `baseUrl` defaulting to `DEFAULT_KRAKEN_BASE_URL` (`https://api.kraken.com/0/public`), optional injectable `FetchLike` from `@suaveplan/types` (default `globalThis.fetch` captured at construction time), optional `extraSymbols: Readonly<Record<string, string>>` merged on top of the canonical 12-entry map at construction time so callers can extend coverage to newly-listed Kraken pairs without forking. Also exports `KRAKEN_SYMBOL_MAP` (frozen forward map: BTC, ETH, SOL, USDT, DOT, ADA, LINK, AVAX, ATOM, LTC, DOGE, XRP → Kraken pair name — covering both the legacy `X<base>Z<quote>` asset-prefix shape `XXBTZUSD` / `XETHZUSD` / `XDGUSD` / `XLTCZUSD` / `XXRPZUSD` and the flat `<base>USD` newer-listing shape `SOLUSD` / `USDTZUSD` / `DOTUSD` / `ADAUSD` / `LINKUSD` / `AVAXUSD` / `ATOMUSD`), `KRAKEN_PAIR_TO_SYMBOL` (frozen reverse map derived at module load — O(1) wire-to-symbol recovery in the hot path), and `mergeSymbolMap(extras?)` (hot-path fast-path: returns the canonical maps by reference when extras are absent or empty; otherwise allocates a fresh forward + reverse pair without mutating the canonical maps). `fetchRates(options?)` builds `GET <baseUrl>/Ticker?pair=<csv>` (Kraken's documented single-round-trip batch shape — the pair CSV is the value list of the merged forward map), routes through the slim core's shared `httpRequest()` helper (caller `RateFetchOptions.signal` forwarded into the helper for `AbortSignal` composition, non-2xx surfaces typed `CurrencyProviderError(HTTP_ERROR)` with `status` populated), parses the JSON body and shape-guards `{ error: string[], result: object }` (typed `PARSE_ERROR` on malformed JSON with the original parse error attached as `cause`, on JSON `null`, on a non-object body, on a non-array `error` field, on a missing or non-object `result` field, and on `result === null`), reads each ticker's `c[0]` last-trade-closed price and inverts it (`rates[symbol] = 1 / Number.parseFloat(c[0])`) into the converter's `1 USD = N crypto` convention, silently skips entries whose price is missing, non-string, fails to parse, is non-finite, or is non-positive (Kraken occasionally returns `"0.00"` for freshly-listed pairs — dropping these is intentional, not an error), skips Kraken pair names that don't appear in the merged reverse map, defaults `options.base` to `"USD"` (forwarded verbatim otherwise so callers can label the result as `"USDT"` / `"USDC"` etc. when wiring the provider behind a stablecoin re-base), filters the rate map by `options.targets` when supplied, raises `CurrencyProviderError(UPSTREAM_ERROR)` with `upstreamMessage` set to the first element of `data.error` when the array is non-empty (falls back to `"unknown upstream error"` when the first element is `null`), and raises `CurrencyProviderError(UNKNOWN_CURRENCY)` when `options.targets` is non-empty but no requested symbol resolves to a mapped Kraken pair — so callers see the failure rather than a silent empty rate map. Every raised error carries `providerName: "kraken"`. 2 subpath exports (`./symbols/symbols`, `./provider/provider`); explicit named barrels everywhere (root + each feature). 34 tests across 3 files at 100/100/100/100 (statements / branches / functions / lines) istanbul coverage. Universal-tier — only `globalThis.fetch`, `JSON.parse`, `Number.parseFloat`, `Number.isFinite`, `Object.entries`, `Object.values`, `Object.freeze`, `Date.now`, `Map`; no `node:`/`bun:`/DOM/fs imports in production code. Deferred (carried forward from the slim-core archive): authenticated Kraken endpoints (private balance / order), WebSocket / streaming ticker, historical-rate / OHLC, retry / rate-limit / OTel wrappers — all will land as composable wrappers consuming this and every other sibling. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-cryptocompare` | Universal-tier CryptoCompare sibling for the slim `@suaveplan/currency-providers` core (2026-05-17). Exposes `CryptoCompareProvider implements CurrencyProvider` (`name = "cryptocompare"`, `supportsCrypto = true`, `supportsFiat = false`) plus `CryptoCompareConfig extends NoKeyConfig` with an **optional** `apiKey` field — the free public tier is fully keyless; supplying `apiKey` attaches `authorization: Apikey <key>` on every outbound request per CryptoCompare's documented Apikey auth scheme (whitespace-only strings treated as absent so `process.env.CRYPTOCOMPARE_API_KEY` can be passed in directly). `fetchRates(options?)` issues a single `GET <baseUrl>/pricemulti?fsyms=<targets>&tsyms=<base>` via the slim core's shared `httpRequest()` (caller `AbortSignal` forwarded, non-2xx → `CurrencyProviderError(HTTP_ERROR)` with `status` populated, raw `Response` returned for body handling), defaults `base` to `"USD"`, defaults `targets` to a built-in 15-symbol list (BTC, ETH, SOL, USDT, USDC, BNB, XRP, ADA, DOGE, DOT, AVAX, LINK, UNI, ATOM, LTC) when omitted or empty, inverts each upstream price (`1 / price`) into the converter's "1 base = X crypto" convention, and skips quotes that are not finite positive numbers. Error mapping: `HTTP_ERROR` (shared helper), `PARSE_ERROR` (invalid JSON or non-object payload), `UPSTREAM_ERROR` when the payload carries `Response: "Error"` (the `Message` field — or `"unknown upstream error"` when absent — is copied into `error.upstreamMessage`), `UNKNOWN_CURRENCY` when no requested symbol produces a positive quote. Every raised error carries `providerName: "cryptocompare"`. 1 subpath export (`./cryptocompare/cryptocompare`); explicit named barrels everywhere. 15 tests at 100/100/100/100 (statements / branches / functions / lines) istanbul coverage, covering both keyless and keyed branches, custom baseUrl, empty-targets fallback, all four error codes, non-Error `cause` handling, and `AbortSignal` forwarding. Universal-tier — only `globalThis.fetch`, URL composition, `Object.entries`, `Number.isFinite`, `Date.now`, `Response`; no `node:`/`bun:`/DOM/fs imports. Deferred: historical-rate endpoints (`/v2/histo*`), OHLCV, WebSocket streaming, multi-tsym aggregation, fiat rates (`supportsFiat: false` by design — install one of the fiat siblings), CryptoCompare-specific metadata endpoints. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-coinmarketcap` | Universal-tier CoinMarketCap sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Public surface: `CoinMarketCapProvider implements CurrencyProvider` (`name = "coinmarketcap"`, `supportsCrypto = true`, `supportsFiat = false`) constructed from `ApiKeyConfig` (`apiKey` required — empty / whitespace raises `CurrencyProviderError(MISSING_CONFIG)` synchronously from the constructor with `providerName: "coinmarketcap"`; optional `baseUrl` defaulting to `https://pro-api.coinmarketcap.com/v1`; optional injectable `FetchLike` from `@suaveplan/types`, default `globalThis.fetch` resolved at call time so test setups that replace the global after construction still work). `fetchRates(options?)` builds `GET <baseUrl>/cryptocurrency/quotes/latest?symbol=<csv>&convert=<base>`, attaches the upstream-required `X-CMC_PRO_API_KEY` header carrying `config.apiKey` plus `Accept: application/json`, routes through the slim core's shared `httpRequest()` (caller `RateFetchOptions.signal` forwarded, non-2xx → `CurrencyProviderError(HTTP_ERROR)` with `status` populated, raw `Response` returned), defaults `base` to `"USD"` and `targets` to the built-in top-10 crypto-by-market-cap (`BTC, ETH, SOL, USDT, USDC, BNB, XRP, ADA, DOGE, DOT`) when omitted or empty, parses the JSON body (typed `PARSE_ERROR` on invalid JSON, non-object body, or missing / non-object `data` field with no usable upstream signal), inspects the upstream `status.error_code` (typed `UPSTREAM_ERROR` when non-zero numeric; `upstreamMessage` copied from `status.error_message` when present; `error_code === 0` treated as success so normal responses are not flagged), inverts each upstream "1 crypto = X fiat" price into the converter's "1 fiat = X crypto" convention (silently dropping symbols whose price is missing, non-numeric, NaN, zero, negative, or quoted against a different base than requested), raises typed `UNKNOWN_CURRENCY` when no requested symbol resolved to a positive quote, and returns `{ base, rates, timestamp: Date.now() }`. Every raised error carries `providerName: "coinmarketcap"`. 2 subpath exports (`./coinmarketcap/coinmarketcap`); explicit named barrels everywhere. 22 tests at 100/100/100/100 (statements / branches / functions / lines) istanbul coverage, covering identity, both constructor-validation branches, the canonical URL + header shape, the top-10 default symbol set (both omitted-options and empty-`targets`-array branches), `AbortSignal` forwarding, custom `baseUrl`, the `globalThis.fetch` fallback, every error path (`HTTP_ERROR` via `httpRequest`; `PARSE_ERROR` for invalid JSON, JSON `null`, missing data, non-object data; `UPSTREAM_ERROR` for populated `error_code` with and without `error_message`, and the missing-data + `error_code` race; `UNKNOWN_CURRENCY` for empty resolution), the `error_code === 0` success branch, and the price-filter rules (skip non-numeric, NaN, zero, negative, wrong-base, missing-quote, undefined-entry, partial-quote-shape). Universal-tier — only `globalThis.fetch`, `Object.entries`, `Number.isFinite`, `Date.now`, `String`, and the injected `FetchLike`; no `node:`/`bun:`/DOM/fs imports in production code. Deferred (carried forward from the slim-core archive): CoinMarketCap fiat conversion endpoints (`/v1/fiat/map`, `/v2/tools/price-conversion`) — out of scope for this crypto-rates sibling; install one of the fiat siblings (`currency-provider-ecb`, `currency-provider-fixer`, `currency-provider-open-exchange-rates`) for fiat coverage. Historical-rate / OHLCV / WebSocket / CoinMarketCap-metadata endpoints will land if real consumer demand justifies them. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-virtual` | Universal-tier in-memory programmable virtual / game currency sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Public surface: `VirtualCurrencyProvider implements CurrencyProvider` (`name = "virtual-currency"`, `supportsCrypto = false`, `supportsFiat = false`, `supportsVirtual = true`) constructed from an optional `VirtualCurrencyProviderConfig` (`defaultBase` default `"USD"`; `initialRates: readonly VirtualRateEntry[]` upserted at construction via the same `setRate` codepath so duplicates within the array are last-write-wins; `clock: Clock` from `@suaveplan/types`, default `() => Date.now()`). Programmable runtime API: `setRate(from, to, rate): void` (upsert — calling twice for the same pair replaces without throwing), `removeRate(from, to): boolean` (idempotent — `true` on first removal, `false` thereafter, auto-cleans empty inner maps so `listCurrencies()` stays accurate, never throws), `clear(): void`, `hasRate(from, to): boolean`, `getRate(from, to): number \| undefined`, `listCurrencies(): readonly string[]` (lexicographic snapshot so equal sets list identically regardless of insertion order). `fetchRates(options?)` consults the nested `Map<from, Map<to, number>>` rate table for `options.base` (default `defaultBase`), optionally filters by `options.targets` (inclusion list — missing targets simply absent from the result, no throw), returns `{ base, rates, timestamp: clock() }`. The promise wrapping satisfies the `CurrencyProvider` async contract without observable microtask delay; no I/O occurs. Throws `CurrencyProviderError(CURRENCY_PROVIDER.UNKNOWN_CURRENCY)` with `providerName: "virtual-currency"` when the requested `base` has no rates stored — the registry can legitimately be empty and there is no triangulation. Triangulation is deliberately absent: `CurrencyConverter` from `@suaveplan/currency` triangulates from its rate cache once a `RateFetchResult` is in hand; duplicating that here would split the algorithm. Also exports `makeUnknownCurrencyError(base): CurrencyProviderError` (canonical error constructor with pre-populated `providerName` discriminant), `VIRTUAL_PROVIDER_NAME: "virtual-currency"`, and `VIRTUAL_CURRENCY_PROVIDER_CODES: { UNKNOWN_CURRENCY }` re-export. Constructor is side-effect-free (unlike upstream `VirtualCurrencyProvider` which called `@suaveplan/money` `registerCurrency` from its constructor — a hidden side effect that broke tree-shaking; callers wanting money-registry integration call `registerCurrency` themselves). 2 subpath exports (`./error-codes/error-codes`, `./provider/provider`); explicit named barrels everywhere. 25 tests across 3 files at 100/100/100/100 (statements / branches / functions / lines) istanbul coverage covering identity, programmable rate-table API (upsert, idempotent remove + cleanup, clear, has / get / list), `fetchRates` (default base fallback, custom base, targets filter, empty-targets-overlap), every `UNKNOWN_CURRENCY` path (missing requested base, empty default base), `FakeClock`-driven deterministic timestamps and `Date.now()` fallback, and a structural assertion that production source contains zero `FetchLike` / `globalThis.fetch` / `fetch(` references. Universal-tier — only `Map`, `Set`, `Date.now`, `Promise.resolve`/`Promise.reject`, and the injected `Clock` contract; no `node:`/`bun:`/DOM/fs imports in production code. Deferred: triangulation (use `CurrencyConverter`), persistence (in-memory by design), `@suaveplan/money` `registerCurrency` integration (call it yourself before construction). Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-ecb` | Universal-tier European Central Bank sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Public surface: `EcbProvider implements CurrencyProvider` (`name = "ecb"`, `supportsFiat = true`, `supportsCrypto = false`) with optional `NoKeyConfig` — `baseUrl` (default `https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml`, override for proxies / staging / fixtures), `fetch` (`FetchLike` from `@suaveplan/types`, default `globalThis.fetch` resolved at call time so test setups that replace the global after construction still work). `fetchRates(options?)` calls `GET <baseUrl>` through the shared `httpRequest()` helper from the slim core (caller `AbortSignal` propagation via `RateFetchOptions.signal`, typed `CurrencyProviderError(HTTP_ERROR)` on non-2xx with `status` populated), reads the XML body, parses the EUR-quoted currency table via the zero-dep regex `/currency='([A-Z]+)'\s+rate='([\d.]+)'/g` (raising `CurrencyProviderError(PARSE_ERROR)` if the body contains zero matches so an empty rate map is never silently cached), then either returns the raw `{ base: "EUR", rates, timestamp: Date.now() }` view or — when the caller requests a non-EUR `base` — synthesises the requested base view by inverting through the EUR feed (`result.EUR = 1 / baseRate`, `result.<other> = <eurRate> / baseRate`; the requested base is absent from the returned map because a currency does not quote itself; raises `CurrencyProviderError(UNSUPPORTED_BASE)` when the requested non-EUR base is missing from the feed). `RateFetchOptions.targets` filtering raises `CurrencyProviderError(UNKNOWN_CURRENCY)` when every requested target is absent from the resolved map (silent-drop for partial overlap; empty `targets` array means no filter). 1 subpath export (`./ecb/ecb`); explicit named barrels everywhere (root + feature). 16 tests across 2 files at 100/100/100/100 (statements / branches / functions / lines) istanbul coverage covering every branch — EUR base / non-EUR base / target filter / each error path / custom baseUrl / injected fetch (and global fallback) / `AbortSignal` forward / empty-targets short-circuit / partial-targets overlap. Universal-tier — only `globalThis.fetch`, `Response`, `Number.parseFloat`, `Date.now`, `Object.entries`, and `RegExp`; no `node:`/`bun:`/DOM/fs imports in production code. Deferred (carried forward from the slim-core archive): retry / rate-limit / OTel / historical-rate / WebSocket / aggregator surfaces will land as composable wrappers consuming this and every other sibling. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-fixer` | Universal-tier [Fixer.io](https://fixer.io/) sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Public surface: `FixerProvider implements CurrencyProvider` (`name = "fixer"`, `supportsFiat = true`, `supportsCrypto = false`) constructed from the slim core's shared `ApiKeyConfig` (`apiKey` required; optional `baseUrl` default `https://data.fixer.io/api`; optional injectable `fetch` `FetchLike` from `@suaveplan/types`, default `globalThis.fetch`). Constructor validates `config.apiKey` is non-empty and raises `CurrencyProviderError(MISSING_CONFIG)` synchronously when it is the empty string — so a missing `process.env.FIXER_KEY` fails before any request burns against the monthly quota. `fetchRates(options?)` builds `GET <baseUrl>/latest?access_key=<key>&base=<base>[&symbols=<csv>]` (default `base = "EUR"`, `targets` comma-joined into `symbols` when the array is non-empty; an empty array is treated as no filter), routes through the slim core's shared `httpRequest()` helper (`AbortSignal` composition via `RateFetchOptions.signal`, non-2xx → `CurrencyProviderError(HTTP_ERROR)` with `providerName`+`status`), then awaits `response.json()` inside a `try` / `catch` (parse failure → `CurrencyProviderError(PARSE_ERROR)` with the underlying `cause` attached). Non-object / `null` / array bodies also raise `PARSE_ERROR`. Discriminates the upstream envelope on `success`: `{ success: true, base, rates, timestamp }` returns `{ base, rates, timestamp: data.timestamp * 1000 }` (Unix seconds → milliseconds); a `success: true` envelope missing any of `base` / `rates` / `timestamp` raises `PARSE_ERROR` (defensive — Fixer's documented success shape always includes them). `{ success: false }` envelopes map to typed codes via the integer `error.code` discriminator: `105` (`base_currency_access_restricted`, the free-tier base lock) → `UNSUPPORTED_BASE`; `201` (`invalid_base_currency`) and `202` (`invalid_currency_codes`) → `UNKNOWN_CURRENCY`; any other code (or missing `error` field entirely) → `UPSTREAM_ERROR`. Every typed error carries `providerName: "fixer"`; the upstream `error.info` (or `"<unknown>"` when absent) is preserved on `upstreamMessage` so the upstream narrative survives the typed mapping. Improvements over the upstream `FixerProvider` (`../suaveplan/packages/finance/currency-providers/src/fiat/fixer.ts`): construction-time `apiKey` guard (upstream silently accepted `""` and let the API reject on the wire), typed `CurrencyProviderError` everywhere (upstream threw bare `Error`), `AbortSignal` propagation through `RateFetchOptions.signal` (upstream did not forward), injectable `FetchLike` seam, typed code discrimination on `error.code` (upstream only checked the `success` flag). 1 subpath export (`./fixer/fixer`); explicit named barrels everywhere (root + feature). 22 tests at 100/100/100/100 (statements / branches / functions / lines) istanbul coverage covering identity flags, construction guard, default + custom baseUrl, default + custom base, targets join, empty targets short-circuit, injected fetch + global fallback, success-envelope projection (incl. ms timestamp), every typed error mapping (105 / 201 / 202 / 999 / missing `error`), every parse-failure branch, `HTTP_ERROR` via the slim core, caller-signal forwarding, no-side-effect import, and universal-tier purity (no `node:`/`bun:`/DOM imports in `src/`). Deferred (carried forward from the 2026-05-17 archive): historical-rate endpoints (`/{date}` and `/timeseries`), fluctuation, conversion (`/convert`), and rate-limit / retry / aggregator wrappers. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-provider-open-exchange-rates` | Universal-tier Open Exchange Rates sibling for the `@suaveplan/currency-providers` slim core (2026-05-17). Public surface: `OpenExchangeRatesProvider implements CurrencyProvider` (`name = "open-exchange-rates"`, `supportsFiat = true`, `supportsCrypto = false`) and `OpenExchangeRatesProviderConfig extends ApiKeyConfig & { readonly allowNonUsdBase?: boolean }`. `apiKey` (OXR `app_id`) is required — the constructor raises `CurrencyProviderError(MISSING_CONFIG)` synchronously when it is the empty string. `baseUrl` defaults to `https://openexchangerates.org/api`; `fetch` defaults to `globalThis.fetch`; `allowNonUsdBase` defaults to `false`. `fetchRates(options?)` issues `GET ${baseUrl}/latest.json?app_id=<key>&base=<base>` (both `apiKey` and `base` are `encodeURIComponent`-wrapped to prevent injection) via the shared `httpRequest()` helper from the slim core (caller `AbortSignal` forwarded; non-2xx → `CurrencyProviderError(HTTP_ERROR)` with `status` and `providerName` populated). Free-tier safe by default: when `options.base !== "USD"` and `allowNonUsdBase !== true`, `fetchRates` raises `CurrencyProviderError(UNSUPPORTED_BASE)` before any network round trip. After the response, the body is parsed and validated for the OXR success shape `{ base: string, rates: object, timestamp: number }`; any deviation (non-object body, non-string `base`, non-number `timestamp`, `rates` being an array or null, invalid JSON — `cause` is normalised to an `Error` so non-`Error` rejections are still surfaced) raises `CurrencyProviderError(PARSE_ERROR)`. The OXR error-payload shape (`{ error: true, status, message, description? }`, returned with a 2xx HTTP status) is detected after parsing and raises `CurrencyProviderError(UPSTREAM_ERROR)` with `upstreamMessage` and `status` populated; an error payload missing `status` or `message` falls back to `PARSE_ERROR`. `RateFetchOptions.targets` is honoured by filtering the rates map; any code absent from the upstream payload raises `CurrencyProviderError(UNKNOWN_CURRENCY)` naming the offending symbol. Returns `{ base, rates, timestamp: payload.timestamp * 1000 }` (Unix seconds → ms). 1 subpath export (`./provider/provider`); explicit named barrels everywhere. 22 provider tests + 4 dep-graph tests at 100/100/100/100 (statements / branches / functions / lines) istanbul coverage, covering every error path including the non-`Error` `cause` branch via a synthetic `Response`-like that rejects with a string. Universal-tier — only `globalThis.fetch` (or injected `FetchLike`), `Response`, `encodeURIComponent`, `Object.entries`; no `node:`/`bun:`/DOM/fs imports. Deferred (carried forward from the slim-core `## Out of scope`): historical-rate endpoint (`/historical/<date>.json`), `/currencies.json`, `/usage.json`, `/convert/{value}/{from}/{to}`, `/ohlc.json`, `/time-series.json`, retry / backoff / rate-limit-header surfacing, OTel spans. Deps: `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/currency-providers` | Universal-tier **slim plugin core** for `@suaveplan/currency` rate providers (slimmed 2026-05-17 from the original 9-provider bundle to a foundation-with-sibling-plugins shape matching `@suaveplan/network` and `@suaveplan/drive`; the concrete `EcbProvider` / `FixerProvider` / `OpenExchangeRatesProvider` / `BinanceProvider` / `CoinGeckoProvider` + `getCoinGeckoId` / `CoinMarketCapProvider` / `CryptoCompareProvider` / `KrakenProvider` / `VirtualCurrencyProvider` classes now ship as `@suaveplan/currency-provider-<name>` sibling packages in the follow-up wave). Public surface: `CurrencyProviderRegistry` (name-keyed `register(provider)` / `get(name)` / `has(name)` / `list(): readonly string[]` (sorted snapshot for deterministic downstream tests) / `unregister(name): boolean` (idempotent, never throws); duplicates raise `PROVIDER_ALREADY_REGISTERED`, unknown lookups raise `PROVIDER_NOT_REGISTERED`, both carry `providerName` on the typed `CurrencyProviderError`). `defineCurrencyProvider(input): CurrencyProvider` — factory that returns an `Object.freeze`-d `CurrencyProvider` from a plain `DefineCurrencyProviderInput` (`name`, `supportsCrypto`, `supportsFiat`, optional `supportsVirtual`, `fetchRates`); preserves the optional `supportsVirtual` property only when explicitly provided. Shared HTTP helpers used by every sibling: `httpRequest(input): Promise<Response>` (accepts `url`, `fetch`, `providerName`, optional `init` / `signal` / `timeoutMs`; derives `AbortSignal.timeout` when set, composes with caller's `signal`, raises `CurrencyProviderError(HTTP_ERROR)` with populated `status` on non-2xx, returns raw `Response` so callers handle `.json()` / `.text()` / `.arrayBuffer()`) and `composeAbortSignal(caller, timeout): AbortSignal \| undefined` (pure four-way composition — `undefined` / `caller` / `timeout` / `AbortSignal.any([caller, timeout])`). Configs: `ApiKeyConfig` (`apiKey`, `baseUrl?`, `fetch?`), `NoKeyConfig` (`baseUrl?`, `fetch?`), `HttpFetchOptions` (`timeoutMs?`, `headers?`), `ProviderConfigBase` (`baseUrl?`, `fetch?`). Typed `CurrencyProviderError extends SuaveplanError<CurrencyProviderErrorCode>` with the 8-code `CURRENCY_PROVIDER_*` registry (`HTTP_ERROR`, `PARSE_ERROR`, `UPSTREAM_ERROR`, `UNSUPPORTED_BASE`, `MISSING_CONFIG`, `UNKNOWN_CURRENCY`, `PROVIDER_NOT_REGISTERED`, `PROVIDER_ALREADY_REGISTERED`) + optional `providerName` / `status` / `upstreamMessage` discriminants — the first six codes are reachable from sibling provider packages, the two registry codes are reachable from this package, and `HTTP_ERROR` is additionally reachable from the shared `httpRequest` helper. 5 subpath exports (`./error-codes/error-codes`, `./types/configs`, `./registry/registry`, `./factory/factory`, `./http/http`); explicit named barrels everywhere. 32 tests across 5 files at 100/100/100/100 (statements / branches / functions / lines) istanbul coverage. Universal-tier — only `globalThis.fetch`, `AbortSignal.timeout`, `AbortSignal.any`, `Object.freeze`, `Map`, `Response`; no `node:`/`bun:`/DOM/fs imports in production code. Deferred (carried forward from the 2026-05-17 archive): rate-limit-aware wrapper (`@suaveplan/rate-limiter` pending), retry + backoff wrapper (`@suaveplan/retry` integration), historical-rate endpoints (interface extension), WebSocket streaming (Binance / Kraken), on-chain oracle adapter (`@suaveplan/cryptocurrency` pending), `AggregatorProvider`, health monitor, OTel spans, stablecoin redirect routing. Deps: `@suaveplan/currency`, `@suaveplan/error`, `@suaveplan/types`, `tslib`. |
|
||||
| `@suaveplan/day-night` | Day/night cycle with configurable time, light schedules, and gameplay modifiers |
|
||||
| `@suaveplan/delivery-tracking` | Channel-agnostic delivery event tracking |
|
||||
| `@suaveplan/destructibles` | Destructible environment system with fracture patterns, debris, and loot |
|
||||
| `@suaveplan/dialogue` | Branching dialogue trees with conditional choices, bark system, and conversation management |
|
||||
| `@suaveplan/discord-client` | Generic Discord API client for webhooks, bot messaging, and embeds |
|
||||
| `@suaveplan/dmx` | DMX512 lighting control engine |
|
||||
| `@suaveplan/docs` | Documentation site |
|
||||
| `@suaveplan/drive-sharing` | Partitioned drive with envelope encryption for the Suaveplan ecosystem. AEAD envelope (`envelopeEncrypt`/`envelopeDecrypt`) with mandatory `partitionId\|\|fileId` AAD, default AES-256-GCM (configurable to XChaCha20-Poly1305). `KeyRing` with `add`/`get`/`remove`/`rotateKey`/versioned `export`+`import`, algorithm-tag + checksum validation, constant-time comparisons, and zeroize-on-drop. `PartitionPolicy` with deterministic `validateAction` state machine, commutative `mergePolicies`, and four `DEFAULT_PRESETS` (public/private/shared/encrypted). `PartitionStore` interface + `InMemoryPartitionStore`; `PartitionManager` with read-only fallback on store failure. `GrantManager` with injectable Clock-driven expiry, `grant:created`/`grant:revoked` events. `TokenManager` with HMAC-SHA-256 integrity MAC, configurable TTL, `revoke` finality. `TransferManager` with two-phase export→validate→import + rollback, AbortSignal cancellation, atomic DEK rewrap. Eleven structured `DriveShareError` codes; safe-by-construction OTel spans + counters + histograms; fast-check property tests for grant expiry, token revocation finality, policy merge commutativity; mitata `grant_check_10k` regression gate (<50 ms). Inlines policy validation rather than depending on `@suaveplan/permissions` to keep the partition-action policy domain-specific. |
|
||||
| `@suaveplan/drive-plugin-cache` | Read-through cache middleware for `@suaveplan/drive`. Returns `driveCachePlugin(options): DrivePlugin<DriveCacheHandle>` that registers a `DriveMiddleware` intercepting `Drive.read(path)` against a caller-supplied `AbstractCache<Uint8Array>` — memory (built-in `MemoryCache`), OPFS / file (`StorageCache` over any browser/server `AbstractStorage`), Redis (`RedisCache`), or any other `cache-storage`-backed backend the consumer picks via `createCache(...)`. Read-through populates on miss when `shouldCache(path, bytes)` returns true (default 16 MiB ceiling matching `drive-mesh-protocol`'s `MAX_FRAME_BYTES`). Write/delete invalidate post-success (avoiding the race window of optimistic invalidation); `stat` and `list` passthrough (list invalidation across prefix mutations is a separate design problem, deferred). 3-code `DRIVE_CACHE.*` registry (`INVALID_OPTIONS` / `CACHE_TOO_LARGE` / `INSTALL_FAILED`) — the wrapper's surface is intentionally narrow; cache and drive errors carry their own namespaced codes through unchanged. Cache failures never propagate to the consumer (perf optimisation, not correctness boundary); they're counter-recorded via `drive_cache.errors_total{namespace, op}`. AbortSignal pre-abort short-circuits cache work and forwards live signals into cache operations when accepted. Multi-namespace isolation on a shared cache so several drives can share one `AbstractCache` instance without cross-talk. Full OTel surface (`drive_cache.{hits,misses,populations,invalidations,errors}_total{namespace}` + `entry_bytes` histogram + outcome-tagged spans). `DriveCacheHandle` exposes `getStats()` / `invalidate(path)` / `clear()` / `cache` (the underlying `AbstractCache` reference so consumers can compose `wrap()`-style stale-while-revalidate on top). Universal tier; integration tests use a real `Drive` + real `MemoryCache` + real `StorageCache` over `storage-file` per CLAUDE.md rule 2 (no mocks). |
|
||||
| `@suaveplan/drive-uri` | Drive/+mesh/+template URI plugins for the `StorageUriResolver` in `@suaveplan/storage-uri`. Three factories — `driveUriPlugin`, `driveMeshUriPlugin`, `driveTemplateUriPlugin` — each wrap a caller-provided `Drive` instance in a `StorageLike` shim and plug into the existing storage-uri resolver alongside file/https/s3/cdn plugins. `drive+template://` plugin fetches the template via an injected `templateProvider`, hydrates the drive via `Drive.hydrateFrom`, and caches the `(templateId → in-flight hydration)` map so concurrent reads share one round-trip. `DriveStorageLikeAdapter` translates Drive's throw-on-missing into `StorageLike`'s undefined-on-missing and forwards `createReadStream` for native streaming through the resolver's fast path. 7-code `DRIVE_URI.*` error registry (`INVALID_URI` / `DRIVE_ID_MISMATCH` / `TEMPLATE_NOT_AVAILABLE` / `TEMPLATE_INVALID` / `WRITE_UNSUPPORTED` / `CAPABILITY_MISSING` / `ABORTED`). No new resolver class, no umbrella composition helper, no duplication of chunk-codec / peer-discovery / template-validation logic — those live in `drive`, `drive-mesh`, `drive-template`, `drive-plugin-chunks` and are exercised via the supplied `Drive` reference. Universal tier; integration tests use real `Drive` + `chunksPlugin` + `storage-file` per CLAUDE.md rule 2 (no mocks). |
|
||||
| `@suaveplan/drive-webrtc` | WebRTC P2P mesh plugin for drive |
|
||||
| `@suaveplan/economy` | In-game economy with wallets, trading, marketplace, and auction system |
|
||||
| `@suaveplan/ecs` | Archetype-based Entity Component System with bitset queries |
|
||||
| `@suaveplan/electrobun` | Electrobun desktop app generator |
|
||||
| `@suaveplan/email-processing` | Email parsing and MIME processing |
|
||||
| `@suaveplan/email-templates` | Email template components |
|
||||
| `@suaveplan/emailer` | Email sending with plugins |
|
||||
| `@suaveplan/emblem-solver` | Minimal-rectangle SVG converter for pixelated sprite... |
|
||||
| `@suaveplan/emotes` | Emote/expression system with registry, cooldowns, and level-gated unlocks |
|
||||
| `@suaveplan/fauna` | Wildlife AI with herding, predator-prey, territorial zones, and taming |
|
||||
| `@suaveplan/font-decompress` | Font decompression utilities |
|
||||
| `@suaveplan/form-builder` | Dynamic form schema builder with validation, conditi... |
|
||||
| `@suaveplan/fruit-machine` | Nested state-machine engine for fruit-machine / AWP games (Adders & Ladders reference) |
|
||||
| `@suaveplan/game-loop` | Fixed-timestep game loop with tick scheduling |
|
||||
| `@suaveplan/game-persistence` | Save/load game state with versioned migrations |
|
||||
| `@suaveplan/game-server` | Universal game server framework with sessions, networking, matchmaking, and game queue |
|
||||
| `@suaveplan/game-state-schemas` | Bit-level quantization schemas for multiplayer state compression |
|
||||
| `@suaveplan/geo` | Abstract geospatial geometry library: ellipsoids, spherical coordinates, Cartesian transforms, great-circle and Vincenty distance, bearing/destination/midpoint, angular separation, bounding boxes, polygons, distance units with conversion, and Zod schemas for all public types. Built with 100% test coverage and OTel instrumentation. |
|
||||
| `@suaveplan/geo-planet` | Configurable planet surface geometry from PlanetConfig — ellipsoid derivation, atmosphere scale-height density, Kasten–Young air mass, and Zod schemas for atmosphere, satellite, and orbital configurations. Delegates all geometry to `@suaveplan/geo`. |
|
||||
| `@suaveplan/geo-earth` | Canonical Earth configuration (EARTH constant and pre-instantiated earthSurface), plus legacy geospatial utilities: in-memory geocoding provider, geohash encode/decode with round-trip fidelity, and decimal/DMS coordinate parsing and formatting. |
|
||||
| `@suaveplan/universe` | Observer model (ground/space discriminated unions), coordinate frame conversions, binary wire-protocol packets for stars and celestial bodies, and a UniverseClient registry for solar, star, tidal, and weather providers. |
|
||||
| `@suaveplan/solar-system` | Solar system provider with injectable AstronomyEngine — body position lookup, season detection, equinox/solstice prediction, sunrise/sunset calculation, and lunar phase estimation. Implements SolarProvider from `@suaveplan/universe`. |
|
||||
| `@suaveplan/stars` | Star catalog provider with priority-ordered StarCatalogSource interface, in-memory catalog source, cone queries, magnitude filtering, and StarProvider implementation for `@suaveplan/universe`. |
|
||||
| `@suaveplan/tides` | Tidal prediction providers: TICON harmonic engine with constituent superposition, ADMIRALTY station provider, and physics-based multi-satellite tide calculation. Implements TidalProvider from `@suaveplan/universe`. |
|
||||
| `@suaveplan/health-check` | Standardized /health, /ready, /live endpoints |
|
||||
| `@suaveplan/heightmap-tiles` | Delta-encoded heightmap tile pyramid with adaptive bit depth |
|
||||
| `@suaveplan/hitbox` | Hierarchical hit detection with per-bone hitboxes, raycasting, and sweep tests |
|
||||
| `@suaveplan/ik-solver` | Pure math inverse kinematics solvers (two-bone, look-at, multi-chain) |
|
||||
| `@suaveplan/image` | Universal-tier image metadata extraction, format detection, resize math, crop validation, and transform pipeline planning. Zero browser or Node dependencies — runs in Bun, Node, and browsers alike. Modular subpath exports for types, errors, error-codes, and processor. |
|
||||
| `@suaveplan/image-quantisation` | Image complexity analysis and adaptive colour quantisation with pool detection, spatial-aware anti-aliasing cleanup, and multiple quantisation algorithms (wuquant, neuquant, rgbquant). Wraps `image-q` with Zod option validation, OTel counter/histogram instrumentation, and structured errors via `@suaveplan/error`. Includes property-based tests (fast-check), mitata benchmarks, and 100% Istanbul coverage. Zero browser or Node dependencies — runs in Bun, Node, and browsers alike. |
|
||||
| `@suaveplan/imessage-adapter` | Mac iMessage SQLite watcher adapter for @suaveplan/m... |
|
||||
| `@suaveplan/job-scheduler` | Background job scheduling with cron |
|
||||
| `@suaveplan/keys` | Key management & vault factory |
|
||||
| `@suaveplan/leaderboard` | Leaderboard system with rankings, seasonal resets, and around-player queries |
|
||||
| `@suaveplan/license` | Software license generation |
|
||||
| `@suaveplan/linter` | Linting utilities |
|
||||
| `@suaveplan/lobby` | Multiplayer lobby management with matchmaking and countdown |
|
||||
| `@suaveplan/macos-contacts` | macOS Contacts.app bridge via AppleScript for contact syncing |
|
||||
| `@suaveplan/macos-keychain` | macOS Keychain bridge via the `security` CLI. `MacOSKeychainBridge` performs credential CRUD (`addItem`/`findItem`/`updateItem`/`deleteItem`/`listItems`/`itemExists`) through a pluggable `CommandExecutor` interface; the default executor uses `Bun.spawn` (or `node:child_process` `execFile`) with arguments-as-array — never shell interpolation — to eliminate command-injection risk. Eight-code error registry (`KEYCHAIN_NOT_FOUND`, `ITEM_EXISTS`, `ACCESS_FAILED`, `LOCKED`, `UNAVAILABLE`, `PERMISSION_DENIED`, `ITEM_INVALID`, `TIMEOUT`) with a stderr-precedence error classifier mapping macOS exit codes to typed factories. Injectable `Clock` for deadline calculations, `AbortSignal` cancellation on every method, OTel spans + ops counters + cli-duration histograms, Zod schemas at every public boundary, `zeroize` on the temporary stdout buffer after password extraction. Unit tests use a real injected stub executor (no mocks); integration tests are tagged `@integration` and skip on non-Darwin. |
|
||||
| `@suaveplan/mcp` | MCP server/client with tools, resources, prompts |
|
||||
| `@suaveplan/media` | Media processing utilities |
|
||||
| `@suaveplan/message-bridge` | |
|
||||
| `@suaveplan/mixamo` | Mixamo character utilities |
|
||||
| `@suaveplan/money` | Universal-tier precise currency/money arithmetic using `bigint` minor units — immutable `Money` class with full arithmetic (`add`, `subtract`, `multiply`, `divide`, `abs`, `negate`), comparison (`compareTo`, `equals`, `greaterThan/lessThan[OrEqual]`, `isZero/Positive/Negative`), and conversion (`toMajor`, `toDecimalString`, `toJSON`, `toString`); 5 `RoundingMode`s (`half-up`, `half-down`, `half-even`, `floor`, `ceil`) via standalone `applyRounding`. Built-in currency registry: 30 ISO 4217 fiats (USD, EUR, GBP, JPY, CHF, CAD, AUD, CNY, INR, BRL, KRW, MXN, SGD, HKD, NOK, SEK, DKK, NZD, ZAR, PLN, THB, TRY, TWD, AED, SAR, ILS, RUB, CZK, CLP, PHP) + 5 cryptocurrencies (BTC, ETH, SOL, USDT, USDC) with canonical exponents; `getCurrency` / `getCurrencyOrThrow` / `registerCurrency` / `getAllCurrencyCodes` / `getVirtualCurrencies` / `isVirtualCurrency` / `minorUnitMultiplier`. `Intl.NumberFormat`-derived locale-aware `formatMoney` (showSymbol / showCode / decimal+thousands separator overrides / trailingZeros control) and `formatMoneyCompact` with K/M/B suffixes. Fair allocators preserving the input total via round-robin remainder distribution: `allocateEvenly`, `allocateByRatios`, `allocateByPercentages`. Aggregators with cross-currency guards: `sumMoney`, `minMoney`, `maxMoney`. Lossless JSON round-trip (`Money.toJSON` / `Money.fromJSON`). Typed `MoneyError extends SuaveplanError<MoneyErrorCode>` with stable `MONEY.*` code registry (`CURRENCY_MISMATCH`, `UNKNOWN_CURRENCY`, `DIVISION_BY_ZERO`, `INVALID_ALLOCATION`, `EMPTY_INPUT`, `PARSE_ERROR`) and optional `currency` / `otherCurrency` discriminants; hardened decimal-string parser raises `MONEY.PARSE_ERROR` on malformed input. 6 subpath exports (`./money/money`, `./currencies/currencies`, `./formatter/formatter`, `./allocator/allocator`, `./error-codes/error-codes`, `./types/common`). 90 tests across 5 files at 100/100/100/100 (statements/branches/functions/lines) istanbul coverage. Universal-tier — only `Intl.NumberFormat`; no `node:`/`bun:`/DOM/fs imports. Deps: `@suaveplan/error`, `tslib`. |
|
||||
| `@suaveplan/network-bootstrap` | Pluggable peer-discovery chain orchestrator for `@suaveplan/network`. Universal-tier. Owns `DiscoveryChain` (ordered strategy execution, bounded concurrency, dedup by peerId, early stop), `PeerCache` (TTL-based eviction, oldest-first capacity eviction, optional `PeerCacheStorageLike` for cross-restart persistence with `CACHE_CORRUPTED` rejection on bad JSON), `BootstrapPlugin` (`NetworkPlugin` lifecycle wrapper with `install`/`start`/`stop`/`dispose` idempotency over `@suaveplan/retry` `createExponentialBackoff` + full-jitter delay between strategy retries), `BootstrapMiddleware` (typed message interceptor for incoming + outgoing). NetworkBus emits `bootstrap:started`/`:peer-found`/`:exhausted` with typed `BootstrapEventMap`. Injectable `Clock` from `@suaveplan/types` everywhere — no `Date.now()` at module scope. `defineCodes("NET_BOOTSTRAP", [...])` with 4 codes (`CHAIN_EXHAUSTED`, `CACHE_CORRUPTED`, `STRATEGY_TIMEOUT`, `INVALID_OPTIONS`) extending `SuaveplanError`. Zod boundary validation on `PeerInfo`, `PeerCacheConfig`, `BootstrapRetryConfig`, `BootstrapPluginConfig`, `BootstrapMiddlewareConfig`. OTel surface: `bootstrap.chain.execute` span, `bootstrap.peers_found_total{strategy}` + `bootstrap.failures_total{strategy,code}` counters, `bootstrap.chain_duration_ms` histogram. `AbortSignal` threaded through every async path with inner controller chained to caller's signal in the timeout handler. fast-check property tests cover cache eviction invariants and chain dedup invariants. Every barrel uses explicit named exports per CLAUDE.md §6a; every test file imports runner primitives from `@suaveplan/testing/runner` per §7. 67 tests, 100% statement/branch/function/line coverage. Deps: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/network`, `@suaveplan/retry`, `@suaveplan/storage`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`. |
|
||||
| `@suaveplan/network-discovery-crdt` | Browser-tier CRDT-awareness `DiscoveryStrategy` for the `@suaveplan/network` mesh, designed to chain into `@suaveplan/network-bootstrap`'s `DiscoveryChain`. Two interchangeable provider backends behind a single `CRDTProviderAdapter` interface: **Yjs** (`y-webrtc` signaling + awareness via `yjsDocFactory`) and **Automerge** (`@automerge/automerge-repo` + `broadcast-channel` + `websocket` + `IndexedDB` adapters via `automergeRepoFactory`). Provider is selected at construction time from a Zod-strict config — orchestrator never branches on `provider` strings post-construction; the adapter shape is the only seam. Five-module orchestrator: `CRDTDiscovery` (main strategy), `CRDTPeerManager` (peer lifecycle), `CRDTIntervals` (periodic reconciliation over `@suaveplan/timing` `Scheduler` + injectable `Clock` — no `setInterval`), `YjsProvider` and `AutomergeProvider` (each implementing `CRDTProviderAdapter`). **`signaling` URL list REQUIRED** by the Zod schema — no `wss://signaling.yjs.dev` default survives anywhere. Production wires factories to real `yjs`/`@automerge/*` packages; tests wire to `FakeYjsBroker` and `FakeAutomergeBroker` — real in-memory implementations of the broker/repo interfaces (NOT `vi.mock`). Optional `EventBusLike<CRDTEventMap>` integration (typed, emitted only when bus provided). Optional `@suaveplan/service-discovery` `ServiceRegistry` integration: discovered peers auto-register as `crdt:<room>:<peerId>`, auto-deregister on drop. 7-code `defineCodes("NET_DISC_CRDT", [...])` registry covering provider-failure / state-desync / peer-reject / invalid-options / disposed / yjs-awareness-failed / automerge-sync-failed. Zod boundary validation on every public option struct + every wire-format message. OTel surface: `disc_crdt.start` span, `disc_crdt.provider.<yjs\/automerge>.sync` per-provider span, `peers_discovered_total{provider,outcome}` counter, `reconcile_duration_ms{provider}` histogram. `AbortSignal` threaded through `start`/`stop`/`discover`/`isAvailable`/`announce`. fast-check property test (`numRuns: 8`) covers state reconciliation convergence + peer dedup across the Automerge path. Constructor-rejection tests cover every missing-config / missing-factory path. Every barrel uses explicit named exports per CLAUDE.md §6a; every test imports runner primitives from`@suaveplan/testing/runner` per §7. 92 tests (16 Yjs + 13 Automerge + 32 orchestrator + property/error/otel suites), 100% statement/branch/function/line coverage. Deps: `@suaveplan/error`,`@suaveplan/events`,`@suaveplan/network`,`@suaveplan/network-bootstrap`,`@suaveplan/service-discovery`,`@suaveplan/timing`,`@suaveplan/types`,`yjs` + `y-webrtc` + `@automerge/automerge-repo` + `@automerge/automerge-repo-network-broadcastchannel` + `@automerge/automerge-repo-network-websocket` + `@automerge/automerge-repo-storage-indexeddb` (all catalog `network-discovery`),`@opentelemetry/api`,`tslib`,`zod`. |
|
||||
| `@suaveplan/network-discovery-gundb` | Browser-tier GunDB gossip-over-WebSocket `DiscoveryStrategy` for the `@suaveplan/network` mesh, designed to chain into `@suaveplan/network-bootstrap`'s `DiscoveryChain`. **Enterprise security uplift over upstream's flagged-fragile audit** is the load-bearing characteristic of this migration. Five-module orchestrator built on injectable `gunFactory`: production wires to real `gun`, tests wire to `FakeGun` — real in-memory implementation of `GunLike` (NOT `vi.mock`). **`peers` relay list mandatory** — Zod schema `peers: z.array(z.string().url()).min(1)`; constructor throws `INVALID_OPTIONS` on missing/empty. **No public Gun-relay URL survives anywhere in the `.ts` code** — `https://gun-manhattan.herokuapp.com/gun` and any sibling defaults removed (only quoted in `.md` docs to document the removal). **Ed25519-signed peer-announces enforced**: every announce carries an Ed25519 signature over the canonical-serialized body; the inbound handler is defensive by design — unsigned announces are silently dropped with `unsigned` reason, single-bit-tampered signatures are silently dropped with `bad-signature` reason, never bubbling an exception (DoS hardening). Verify dispatches to `@suaveplan/crypto-curve`. **Constant-time peer-ID compare** via `constantTimeEqual` from `@suaveplan/crypto-utils` — closes the trivial side-channel from byte-by-byte string equality. **Wire-payload Zod validation** on every inbound message before reaching business logic; `GunDBPeerAnnounceSchema` + `GunDBMessageSchema` discriminated-union wrapper; malformed / wrong-kind / wrong-type payloads silently dropped + counted. peerId-spoofing rejected (signing-key derived peerId must match claimed peerId). Optional `EventBusLike<GunDBEventMap>` integration. Optional `@suaveplan/service-discovery` integration: peers auto-register as `gundb:<topic>:<peerId>`, auto-deregister on drop. Intervals via `@suaveplan/timing` Scheduler + injectable Clock. 8-code `defineCodes("NET_DISC_GUNDB", [...])` registry: `RELAY_FAILED`, `PEER_CONNECT_FAILED`, `INVALID_OPTIONS`, `DISPOSED`, `NOT_RUNNING`, `UNSIGNED_ANNOUNCE`, `BAD_SIGNATURE`, `INVALID_WIRE_PAYLOAD`. OTel surface: `disc_gundb.start` span, `peers_discovered_total{outcome}` counter, **`gundb.invalid_messages_total{reason}`** + **`gundb.signature_rejected_total{reason}`** security counters (the observability levers for the new defensive layer), `gundb_announce_duration_ms` histogram. `AbortSignal` threaded through `start`/`stop`/`discover`/`isAvailable`/`announce`. `./signing` subpath exports `signAnnounce(privateKey, body)` helper for callers emitting announces. fast-check property tests: 50-run sign/verify round-trip over any valid body + 20-run orchestrator-level signed-announce ingestion. 8 dedicated security tests: unsigned-drop, bad-signature-drop, malformed-payload-drop, payload-with-valid-discriminator-but-bad-types, peerId-spoofing-reject, `constantTimeEqual` direct verification, local-self-echo filter via constant-time path, differing-length peerIds reach length-mismatch branch. Every barrel uses explicit named exports per CLAUDE.md §6a; every test imports runner primitives from `@suaveplan/testing/runner` per §7. 67 tests across 5 files, 100% statement/branch/function/line coverage (228/104/38/210). Deps: `@suaveplan/crypto-curve`, `@suaveplan/crypto-utils`, `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/network`, `@suaveplan/network-bootstrap`, `@suaveplan/service-discovery`, `@suaveplan/timing`, `@suaveplan/types`, `gun` (catalog `network-discovery`), `@opentelemetry/api`, `tslib`, `zod`. |
|
||||
| `@suaveplan/network-discovery-hybrid` | Browser-tier multi-strategy `DiscoveryStrategy` orchestrator for the `@suaveplan/network` mesh, composing the five Wave-2 sibling discovery strategies (peerjs/crdt/trystero/webtorrent/gundb) into a single unified surface that itself implements `DiscoveryStrategy` from `@suaveplan/network-bootstrap` — so a `HybridDiscoveryManager` can chain into a higher-level `DiscoveryChain`. Five-module composer architecture: **`HybridDiscoveryManager`** (top-level orchestrator), **`StrategyManager`** (per-strategy health-score + priority bookkeeping), **`Lifecycle`** (start/stop transition FSM), **`PeerTracker`** (deduped peer set with `PeerSource` provenance — which strategy found this peer), **`StrategyLoader`** (dynamic `import()`-based lazy resolution that gracefully handles missing siblings). All five Wave-2 discovery packages declared as `peerDependenciesMeta.{...}.optional: true` — the orchestrator never crashes when a peer-dep is absent; a missing strategy surfaces as `STRATEGY_LOAD_FAILED` and is recorded. Priority-based peer selection. Per-strategy `StrategyHealth` tracking with automatic fallback on degradation. **Banned-mock removal is load-bearing for this migration**: upstream's `__mocks__/strategy-mock.ts` was explicitly NOT ported; in-package `TestStrategy` real-implementation fixture lives at `tests/setup/test-strategy.ts` (deterministic, configurable peer list / failure mode / announce delay). Independently grep-verified zero `vi.mock` / `jest.mock` / `__mocks__/` / `: any` anywhere in the package. Optional `EventBusLike<HybridDiscoveryEventMap>` integration. Optional `@suaveplan/service-discovery` integration: peers register as `hybrid:<strategy>:<peerId>` preserving provenance. 6-code `defineCodes("NET_DISC_HYBRID", [...])` registry (`NO_STRATEGIES`, `ALL_STRATEGIES_DEGRADED`, `STRATEGY_LOAD_FAILED`, `DUPLICATE_STRATEGY`, `INVALID_OPTIONS`, `DISPOSED`). Zod boundary validation on `HybridDiscoveryConfig`, `StrategyConfig`, `StrategyPriority`. OTel surface: `hybrid.peer_sources_total{strategy}` counter, `hybrid.strategy_health_score{strategy}` histogram, `hybrid.active_strategies` gauge, `hybrid.fallback_total{from,to}` counter. `AbortSignal` threaded through every async path. fast-check property tests on peer dedup commutativity + priority-order stability. **Integration test (`tests/integration.test.ts`, 4 tests)** verifies: all 5 sibling-id registration with dedup + `PeerSource` correctness, one-strategy-degradation triggers priority fallback, lifecycle start/stop/restart idempotency, all 5 real shipped sibling packages reachable via dynamic `import()`. `openspec/specs/network-discovery-hybrid/spec.md` written. Every barrel uses explicit named exports per CLAUDE.md §6a; every test imports runner primitives from `@suaveplan/testing/runner` per §7. 89 tests across 7 files, 100% statement/branch/function/line coverage. Deps: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/network`, `@suaveplan/network-bootstrap`, `@suaveplan/service-discovery`, `@suaveplan/timing`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`. Optional peer-deps on all five `@suaveplan/network-discovery-{peerjs,crdt,trystero,webtorrent,gundb}`. |
|
||||
| `@suaveplan/network-discovery-peerjs` | Browser-tier PeerJS-backed `DiscoveryStrategy` for the `@suaveplan/network` mesh, designed to chain into `@suaveplan/network-bootstrap`'s `DiscoveryChain`. Wraps the PeerJS signaling-server client + WebRTC `DataConnection` lifecycle in a `PeerJSDiscovery` orchestrator with supporting modules: `PeerConnectionManager` (per-peer connection registry), `RoomManager` (logical room semantics over PeerJS IDs with admission cap), `PeerJSConnection` (typed `RTCDataConnection` wrapper), `PeerJSMessageHandlers` (wire-format routing with dedup). Mandatory `host` AND mandatory injectable `peerFactory` — constructor throws `INVALID_OPTIONS` on either omission; **no public-`peerjs.com` fallback survives anywhere in the code**. Production wires `peerFactory` to real `peerjs`; tests wire it to `FakeBroker`/`FakePeer` — real implementations of `PeerLike`/`DataConnectionLike` (NOT `vi.mock`-style mocks). Optional `EventBusLike` integration (typed `PeerJSEventMap`) — emitted only when a bus is provided. Optional `@suaveplan/service-discovery` `ServiceRegistry` integration: discovered peers auto-register as `peerjs:<room>` services + auto-deregister on drop. Interval loops replaced with `@suaveplan/timing` `Scheduler` + injectable `Clock`. 5-code `defineCodes("NET_DISC_PEERJS", [...])` registry (`SIGNALING_FAILED`, `PEER_CONNECT_FAILED`, `ROOM_FULL`, `INVALID_OPTIONS`, `DISPOSED`). Zod boundary validation on every public option struct + every wire-format message. OTel surface: `disc_peerjs.start` span, `peers_discovered_total{outcome}` counter, `signaling_duration_ms` histogram. `AbortSignal` threaded through `start`/`stop`/`discover`/`isAvailable`/`connect`. fast-check property tests cover room-admission cap + message-handler dedup. Every barrel uses explicit named exports per CLAUDE.md §6a; every test imports runner primitives from `@suaveplan/testing/runner` per §7. 100 tests, 100% statement/branch/function/line coverage. Deps: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/network`, `@suaveplan/network-bootstrap`, `@suaveplan/service-discovery`, `@suaveplan/timing`, `@suaveplan/types`, `peerjs` (catalog `network-discovery`), `@opentelemetry/api`, `tslib`, `zod`. |
|
||||
| `@suaveplan/network-discovery-trystero` | Browser-tier Trystero `DiscoveryStrategy` for the `@suaveplan/network` mesh, chains into `@suaveplan/network-bootstrap`'s `DiscoveryChain`. Four interchangeable transports — **BitTorrent trackers** (`TorrentTransportAdapter`), **Nostr relays** (`NostrTransportAdapter`), **MQTT brokers** (`MqttTransportAdapter`), **IPFS libp2p pubsub** (`IpfsTransportAdapter`) — all implementing a single `TrysteroTransportAdapter` interface; orchestrator selects at construction via `pickAdapter()` and **never branches on transport-name strings post-construction**. Per-transport injectable factories (`torrentRoomFactory` / `nostrRoomFactory` / `mqttRoomFactory` / `ipfsRoomFactory`); production wires to real `trystero`, tests wire to `FakeTrysteroBroker` + `FakeTrysteroRoom` — real in-memory implementations of `TrysteroRoomLike` (NOT `vi.mock`). Every transport sub-schema enforces `.min(1)` non-empty lists (`trackers`/`relays`/`brokers`/`bootstrap`) — **no hardcoded public relay/tracker URLs survive anywhere in the code** (upstream `room-config.ts` rewritten as per-transport adapters consuming caller-supplied factories). Auto-fallback between transports via `@suaveplan/retry` `createExponentialBackoff` + injectable `sleepFn`; fallback swaps the adapter instance while preserving the peer-manager + intervals. Optional `EventBusLike<TrysteroEventMap>` integration (typed, emit only when bus provided). Optional `@suaveplan/service-discovery` integration: peers auto-register as `trystero:<transport>:<room>:<peerId>`, auto-deregister on drop. Intervals via `@suaveplan/timing` `Scheduler` + injectable `Clock`. 10-code `defineCodes("NET_DISC_TRYSTERO", [...])` registry (`INVALID_OPTIONS`, `DISPOSED`, `NOT_RUNNING`, `RELAY_FAILED`, `ROOM_FULL`, `ALL_TRANSPORTS_DEGRADED`, `TORRENT_TRACKER_FAILED`, `NOSTR_RELAY_FAILED`, `MQTT_BROKER_FAILED`, `IPFS_BOOTSTRAP_FAILED`). Zod boundary validation on every public option struct + every wire-format message. OTel surface: `disc_trystero.start` span, `disc_trystero.transport.<id>.connect` per-transport span, `peers_discovered_total{transport,outcome}` + `transport_fallback_total{from,to}` counters, `connect_duration_ms{transport}` histogram. `AbortSignal` threaded through `start`/`stop`/`discover`/`isAvailable`/`announce`. fast-check property tests on peer dedup across transports + fallback ordering stability. Every barrel uses explicit named exports per CLAUDE.md §6a; every test imports runner primitives from `@suaveplan/testing/runner` per §7. 118 tests, 100% statement/branch/function/line coverage (584 stmts / 206 branches / 127 fns / 543 lines). Deps: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/network`, `@suaveplan/network-bootstrap`, `@suaveplan/retry`, `@suaveplan/service-discovery`, `@suaveplan/timing`, `@suaveplan/types`, `trystero` (catalog `network-discovery`), `@opentelemetry/api`, `tslib`, `zod`. |
|
||||
| `@suaveplan/network-discovery-webtorrent` | Browser-tier WebTorrent-tracker `DiscoveryStrategy` for the `@suaveplan/network` mesh, designed to chain into `@suaveplan/network-bootstrap`'s `DiscoveryChain`. Built on top of [`p2pt`](https://github.com/subins2000/p2pt) — a WebTorrent-tracker-based WebRTC peer connector — surfaced via an injectable `p2ptFactory`. Production wires the factory to real `p2pt`; tests wire to `FakeP2PT` — a real in-memory implementation of `P2PTLike` (NOT `vi.mock`). **Tracker list mandatory** — Zod schema `trackers: z.array(z.string().min(1)).min(1)` rejects missing-or-empty list AND empty-string entries with `INVALID_OPTIONS`. **No public-tracker fallback survives anywhere in the code** — every upstream default (`tracker.openwebtorrent.com`, `tracker.btorrent.xyz`, `tracker.files.fm:7073/announce`, `spacetradersapi-chatbox.herokuapp.com:443/announce`) removed; trackers are caller-supplied only. Optional `EventBusLike<WebTorrentEventMap>` integration (5 typed event types: `peer:joined`, `peer:left`, `tracker:announce`, `tracker:error`, `error`). Optional `@suaveplan/service-discovery` `ServiceRegistry` integration: peers auto-register as `webtorrent:<infoHash>:<peerId>`, auto-deregister on drop. Conforms to the `DiscoveryStrategy` interface from `@suaveplan/network-bootstrap` so it plugs into a `DiscoveryChain` directly. 5-code `defineCodes("NET_DISC_WEBTORRENT", [...])` registry (`TRACKER_FAILED`, `PEER_CONNECT_FAILED`, `INVALID_OPTIONS`, `DISPOSED`, `NOT_RUNNING`) extending `SuaveplanError`. Zod boundary validation on `WebTorrentDiscoveryConfig`, `PeerInfo`, `PeerId`, `Multiaddr` + wire payloads. OTel surface: `disc_webtorrent.start` span, `peers_discovered_total{outcome}` + `tracker_errors_total{tracker}` counters, `tracker_announce_duration_ms` histogram. `AbortSignal` threaded through every async method (`start`, `stop`, `discover`, `isAvailable`, `announce`) — including factory-resolve mid-flight cancellation. fast-check property test on peer dedup invariant (same peerId never emitted twice from concurrent tracker announces). **Test suite written from scratch — upstream had `describe.skip`-only.** Every barrel uses explicit named exports per CLAUDE.md §6a; every test imports runner primitives from `@suaveplan/testing/runner` per §7. 46 tests, 100% statement/branch/function/line coverage (171/79/34/155). Deps: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/network`, `@suaveplan/network-bootstrap`, `@suaveplan/service-discovery`, `@suaveplan/timing`, `@suaveplan/types`, `p2pt` (catalog `network-discovery`), `@opentelemetry/api`, `tslib`, `zod`. |
|
||||
| `@suaveplan/network-gundb-server` | Server-tier (Node-only) GunDB relay daemon paired with `@suaveplan/network-discovery-gundb` ✅ clients. Idempotent `start({signal?})` / `stop()` lifecycle; injectable `gunFactory` + `FakeGun` real impl. Inline `/health`/`/ready`/`/live` JSON endpoints. **Security wire-payload validation** mirrors the client's defensive model — Zod-validates inbound announces against shared `GunDBPeerAnnounceSchema` (single source of truth, server/client cannot drift), silently drops malformed/wrong-kind/missing-sig/missing-key with `gundb_server.invalid_messages_total{reason}` counter. Signature verification deliberately at client per gossip-security model. **Real-server-boot integration test** boots a real Node `http.Server` on port 0, hits all three health endpoints over real `fetch()`, drives two simulated clients through a shared in-memory Gun graph, confirms malformed payloads dropped without crashing, verifies graceful stop closes the listener. 6-code `NET_GUNDB_SERVER` registry, OTel surface (span + 3 counters + gauge + histogram). `engines.node: ">=20"`, no `browser` field. 56 tests across 7 files, 100% statement/branch/function/line coverage. Deps: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/logger`, `@suaveplan/network-discovery-gundb`, `@suaveplan/testing-integration`, `@suaveplan/types`, `gun` (catalog `network-discovery`), `@opentelemetry/api`, `tslib`, `zod`. |
|
||||
| `@suaveplan/network-transport-hyperswarm` | Server-tier (Node-only) `Transport` + `NetworkPlugin` over the Hyperswarm DHT. Provides hole-punched peer-to-peer connections discovered via a Kademlia-style DHT keyed by 32-byte topics. Renamed from upstream `@suaveplan/network-hyperswarm` per `network-transport-family` convention. **Required injectable `hyperswarmFactory`** — the package NEVER imports `hyperswarm` directly; consumers wire the factory at the app boundary so native deps stay outside the unit-test surface. Tests wire to `FakeHyperswarm`/`FakeSwarmHub` real-implementations under `tests/setup/fake-hyperswarm.ts` (NOT `vi.mock`). Eight-module architecture: `messaging/` (length-prefix wire codec over `@suaveplan/serialize`), `security/` (DHT-level Zod wire-payload validation + Ed25519-signed peer-announce defensive handler — silent drops on bad signatures), `reputation/` (`ReputationStore` with monotonicity invariant property-tested), `discovery/` (DHT topic management with AbortSignal-aware join/leave), `connections/` (per-peer connection registry), `transport/` (`HyperswarmTransport implements Transport`; `connect(peerId, address)` is idempotent-success for already-attached peers, raises `PEER_UNKNOWN` otherwise — honest about being a discovery transport not a dialer), `plugin/` (`HyperswarmPlugin implements NetworkPlugin` mirroring `WebRTCPlugin` shape), `errors/`, `otel/`, `types/`. 10-code `defineCodes("NET_HYPERSWARM", [...])` registry (`DHT_LOOKUP_FAILED`, `PEER_REJECTED`, `STREAM_FAILED`, `INVALID_TOPIC`, `INVALID_OPTIONS`, `DISPOSED`, `NOT_RUNNING`, `PEER_ALREADY_CONNECTED`, `PEER_UNKNOWN`, `TRANSPORT_CLOSED`). Zod boundary validation on every public option struct + wire frames. OTel surface: `hyperswarm.connections_opened_total{outcome}` + `hyperswarm.bytes_total{direction}` counters, `hyperswarm.dht_lookup_ms` + `hyperswarm.dial_ms` histograms, `hyperswarm.active_peers` gauge. Optional `EventBusLike<HyperswarmEventMap>` integration. `AbortSignal` threaded through `join`/`leave`/`connect`/`stop`. **3-peer in-process integration test** spins up three `HyperswarmTransport` instances sharing a `FakeSwarmHub`, verifies all-to-all peer discovery + envelope exchange, asserts each inbox receives messages from the other two (not itself). A second integration test verifies disconnect propagation. **Scoped out (documented in commit history)**: upstream `hyperswarm-resource-limits.ts` per-peer DoS rate-limit layer was NOT ported — belongs in a future `network-plugin-qos` integration; the reputation store covers failure-counting. `@suaveplan/feature-flags` Composite-strategy reputation integration was NOT used — sub-millisecond mutation paths need direct access and pluggable flag evaluator adds wrong dep direction. Upstream `examples/` consolidated to a single `examples/basic.ts` against the modern `Transport` contract (upstream samples used pre-`Transport`-contract shapes). `package.json` declares `"engines": { "node": ">=20" }` and omits the `browser` field; server-tier marker comment on line 1 of `transport.ts`. Every barrel uses explicit named exports per CLAUDE.md §6a; every test imports runner primitives from `@suaveplan/testing/runner` per §7. 94 tests across 11 files — **effectively 100% net-new** because upstream coverage was 8.98%. 100% statement/branch/function/line coverage. Deps: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/network`, `@suaveplan/retry`, `@suaveplan/serialize`, `@suaveplan/timing`, `@suaveplan/types`, `hyperswarm` (catalog `network-transport`), `@opentelemetry/api`, `tslib`, `zod`. |
|
||||
| `@suaveplan/network-transport-libp2p` | Universal-tier `Transport` + `NetworkPlugin` over the libp2p stack for the `@suaveplan/network` mesh. Renamed from upstream `@suaveplan/network-libp2p` per `network-transport-family` convention. Stitches together the canonical libp2p stack (websockets + webrtc sub-transports + Noise encryption + Yamux muxer + identify + ping + bootstrap + circuit-relay-v2) into a `Libp2pTransport` with connection lifecycle, peer addressing, streaming messaging, and metrics. **TCP and mDNS sub-transports deliberately deferred** to a future server-tier sibling — universal-tier websockets+webrtc was the only in-scope path. Required injectable `libp2pFactory` — the package NEVER imports `libp2p` at module-eval; consumers wire `createLibp2p` at the app boundary, tests wire `FakeLibp2pNode` real-implementation under `tests/setup/fake-libp2p.ts` (NOT `vi.mock`). 9-module architecture: `connection/` (libp2p node create + dial), `messaging/{peer-message,frame,send}` (stream-based request/response + wire format over `@suaveplan/serialize` + addressed-send helpers), `streams/` (protocol handler registration), `transport/` (`Libp2pTransport implements Transport`), `plugin/` (`Libp2pPlugin implements NetworkPlugin` mirroring `WebRTCPlugin` / `HyperswarmPlugin`), `errors/`, `otel/`, `types/`. **`bootstrap` list MANDATORY** — Zod schema `bootstrap: z.array(z.string()).min(1)` rejects missing/empty with `INVALID_OPTIONS`; **no public-bootstrap fallback survives** anywhere in the code. 11-code `defineCodes("NET_LIBP2P", [...])` registry (`BOOTSTRAP_FAILED`, `DIAL_FAILED`, `STREAM_FAILED`, `PROTOCOL_NOT_SUPPORTED`, `IDENTIFY_REJECTED`, `INVALID_OPTIONS`, `DISPOSED`, `NOT_RUNNING`, `PEER_ALREADY_CONNECTED`, `PEER_UNKNOWN`, `TRANSPORT_CLOSED`). Zod boundary validation on every public option struct. OTel surface: `libp2p.connections_opened_total{outcome}` + `libp2p.streams_opened_total` + `libp2p.bytes_total{direction}` counters, `libp2p.dial_ms` + `libp2p.stream_ttfb_ms` histograms, `libp2p.active_peers` gauge. Optional `EventBusLike<Libp2pEventMap>` integration. `AbortSignal` threaded through `start`, `dialPeerAddress`, `createAndStartNode`, `registerStreamHandler`, `consumeInboundStream`, `sendEnvelope(Frame)`. **2-peer integration tests** for both `websockets` and `webrtc` sub-transports via `FakeLibp2pHub`. **Discipline metrics: every source file ≤300 LOC (max: `transport.ts` at 290), every test file ≤500 LOC (max: `transport.test.ts` at 419). Zero `react`/`react-dom` imports anywhere in `src/`. Zero `: any` in production source. Zero `vi.mock` / `jest.mock` calls.** Upstream's 749-line `libp2p-demo.tsx` was NOT ported — deferred to Wave 7's `@suaveplan/react-network-libp2p-demo`. Upstream duplicate 56-line `network-libp2p.spec.ts` DELETED (`.spec.ts` is Playwright-only per CLAUDE.md). Upstream 505-line `network-libp2p.test.ts` split across 9 module-scoped test files. Every barrel uses explicit named exports per CLAUDE.md §6a; every test imports runner primitives from `@suaveplan/testing/runner` per §7. 74 tests across 11 files, 100% statement/branch/function/line coverage. Deps: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/network`, `@suaveplan/network-transport-webrtc`, `@suaveplan/retry`, `@suaveplan/serialize`, `@suaveplan/timing`, `@suaveplan/types`, 12 libp2p subpackages catalog `network-transport`: `libp2p`, `@libp2p/websockets`, `@libp2p/webrtc`, `@libp2p/bootstrap`, `@libp2p/circuit-relay-v2`, `@libp2p/echo`, `@libp2p/identify`, `@libp2p/interface`, `@libp2p/ping`, `@chainsafe/libp2p-noise`, `@chainsafe/libp2p-yamux`, `@multiformats/multiaddr`. NO `react`, `react-dom`, `@libp2p/tcp`, or `@libp2p/mdns`. `@opentelemetry/api`, `tslib`, `zod`. |
|
||||
| `@suaveplan/network-libp2p-server` | Server-tier libp2p bootstrap + circuit-relay-v2 daemon paired with the just-shipped `@suaveplan/network-transport-libp2p` ✅ client. Idempotent `start({signal?}) → {peerId, multiaddrs[]}` / `stop()` lifecycle. Production libp2pFactory wires `tcp() + webSockets() + circuitRelayServer() + identify() + noise() + yamux() + bootstrap()`; daemon NEVER resolves `libp2p` at module-eval so native deps stay off the unit-test surface. Tests wire `FakeLibp2pServerNode` real-implementation (NOT vi.mock). Eight-module architecture: `errors/`, `health/health.ts` (verb pipeline for /health/ready/live), `health-listener/` (inline HTTP listener with `listenHttp`), `peer-events/` (peer-connected wiring), `server/server.ts` (daemon class, 299 LOC at hard cap), `otel/`, `types/`. 6-code `defineCodes("NET_LIBP2P_SERVER", [...])` registry (`START_FAILED`, `LISTEN_FAILED`, `INVALID_OPTIONS`, `DISPOSED`, `NOT_RUNNING`, `RELAY_FAILED`). Zod boundary validation on `Libp2pServerConfig`. OTel surface: `libp2p_server.start` span, `libp2p_server.connections_total{outcome}` + `libp2p_server.relayed_streams_total` counters, `libp2p_server.active_peers` gauge, `libp2p_server.start_duration_ms` histogram. AbortSignal threaded through start + graceful stop. **4-scenario in-process integration test**: real HTTP health/ready/live probes, client dial recorded as peer-connected end-to-end, server brokers stream observation across two clients (autoRelay enabled), graceful stop closes the inline listener. **Discipline metrics: largest source 299 LOC, largest test 384 LOC, zero `: any`, zero `vi.mock` / `jest.mock`, zero React deps, every barrel uses explicit named exports per CLAUDE.md §6a, every test imports runner primitives from `@suaveplan/testing/runner` per §7.** `engines.node: ">=20"`, no `browser` field, server-tier marker on the main entry point. 59 tests across 8 files (errors 3, health 13, otel 5, peer-events 7, health-listener 6, server 20, dep-graph 1, integration 4), 100% statement/branch/function/line coverage. Adds `@libp2p/tcp ^11.0.6` to root catalog `network-transport` group (server-only sub-transport; the Wave-4.2 client deliberately omitted it). Deps: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/logger`, `@suaveplan/network-transport-libp2p` (shared types + wire-format compatibility), `@suaveplan/testing-integration`, `@suaveplan/types`, 9 libp2p subpackages catalog `network-transport`, `@opentelemetry/api`, `tslib`, `zod`. |
|
||||
| `@suaveplan/network-observability` | **Folded into `@suaveplan/network-plugin-monitor`** (Wave 6.1 `network-observability-consolidation`, 2026-05-12). Upstream shipped three feature modules — Prometheus exposition (via `prom-client`), Grafana dashboard generator, and an OTel SDK exporter bridge. Audit showed all three are output adapters consuming an external metric collector; only ~600 LOC of universally-portable surface survived after filtering `prom-client` (Node-only) and external `@opentelemetry/sdk-metrics`. Prometheus exposition was rewritten as a hand-rolled universal-tier text emitter (`0.0.4` format, 195 LOC, sanitised metric/label names + escaped values, counter/gauge/histogram/summary support, configurable quantiles, optional timestamps, pinned `text/plain; version=0.0.4; charset=utf-8` Content-Type, Zod-validated options). Grafana dashboard generator was ported as a pure Zod-validated JSON builder (auto-layout, injectable `Rng` for deterministic UID generation, pinned Prometheus datasource binding `{type:"prometheus",uid:"prometheus"}`, `schemaVersion: 38`). The OTel SDK exporter was deliberately dropped — `network-plugin-monitor` already emits through `@opentelemetry/api` so any externally-registered `MeterProvider` (NodeSDK, `@suaveplan/opentelemetry`'s `NoopSdk`/`SdkFactory`, etc.) picks up the instruments natively, making the bridge structurally redundant. Net result: a single canonical telemetry surface (`@suaveplan/network-plugin-monitor`) instead of two overlapping packages, both feature modules at 100/100/100/100 coverage with sibling `.md` ≥200-word docs. Consumers wanting the standalone `@suaveplan/network-observability` package should depend on `@suaveplan/network-plugin-monitor` and import `createPrometheusExposition` / `GrafanaDashboardGenerator` from its root barrel. |
|
||||
| `@suaveplan/network-quality` | **Folded into `@suaveplan/network-plugin-monitor`** (Wave 6.2 `network-quality-consolidation`, 2026-05-12). Upstream shipped three feature modules — `BandwidthMonitor` (EMA sample recorder), `DegradationManager` (4-level quality classifier with hysteresis + one-step transitions), and `createQualityMiddleware` (per-source message-throttling middleware factory) — together with a `QualityPlugin` host wrapper and a `QualityLevel`/`BandwidthSample`/`QualityThresholds` shared types layer. Audit confirmed zero overlap with the shipped `network-plugin-monitor` (which scores per-peer health but does NOT classify into a 4-rung ladder, run hysteresis, or shape outgoing traffic). All three features ported under `src/quality/` as sibling feature modules in the monitor package; the upstream `EventEmitter<TMap>`-based evented API was rewritten as callback-handler subscriptions returning unsubscribe functions to drop the `@suaveplan/events` runtime coupling and match genesis's no-monolithic-EE convention; `hysteresisMs: 0` newly commits same-call (upstream required a second `evaluate()` call); `QualityLevel` rewritten as a `const` object + value-union to drop TypeScript enum reverse-mapping; types Zod-validated at every boundary (`BandwidthSampleSchema`, `QualityThresholdsSchema`, `LevelThresholdSchema`). The upstream `QualityPlugin` `NetworkPlugin`-wrapper was deliberately NOT ported — `network-plugin-monitor` already exposes `MonitorPlugin`, and consumers wanting periodic auto-evaluation wire `BandwidthMonitor` + `DegradationManager` directly behind their own scheduler. 43 net-new tests, all four feature files (`quality-types.ts`, `bandwidth-monitor.ts`, `degradation-manager.ts`, `quality-middleware.ts`) at 100/100/100/100 coverage with sibling `.md` ≥200-word docs. Consumers wanting the standalone `@suaveplan/network-quality` package should depend on `@suaveplan/network-plugin-monitor` and import `BandwidthMonitor` / `DegradationManager` / `createQualityMiddleware` / `QualityLevel` / `QualityThresholds` from its root barrel. |
|
||||
| `@suaveplan/network-transport-standby` | Universal-tier pre-emptive WebRTC standby transport for the `@suaveplan/network` mesh. Maintains a pool of warm `RTCPeerConnection` slots so consumer code experiences ~10 ms peer attach instead of the typical 1-3 s ICE/DTLS handshake. `StandbyTransport implements Transport` + `StandbyPlugin implements NetworkPlugin` (mirrors `WebRTCPlugin` / `WebSocketPlugin` shape: install registers transport, start lazy, stop closes + unregisters, dispose idempotent). Six-module architecture: `types/`, `errors/`, `otel/`, `health/monitor` (EMA-smoothed RTT + jitter + loss composite scoring), `reconnect/reconnect` (auto-reconnect FSM via `@suaveplan/retry` `createExponentialBackoff` + equal jitter — replaces upstream ad-hoc `Math.min(base * 2^n, cap)` loop), `pool/pool` (min/max slot config, eviction, warm-up scheduler over `@suaveplan/timing` `createScheduler` + injectable `Clock`, `#topUpInflight` mutex serialising concurrent eviction-driven warms so capacity invariants never break), `transport/transport`, `plugin/plugin`. Injectable `RtcConnectionFactory` — production wires to real `RTCPeerConnection`, tests wire to `FakeRtcConnection` real implementation (NOT `vi.mock`). 6-code `defineCodes("NET_STANDBY", [...])` registry (`POOL_EXHAUSTED`, `SLOT_UNHEALTHY`, `PROMOTION_FAILED`, `INVALID_OPTIONS`, `DISPOSED`, `NOT_RUNNING`). Zod schemas on `StandbyPoolConfig`, `ConnectionRequest`, `SlotState` with cross-field invariants (`minSlots ≤ maxSlots`, `reconnectBaseDelayMs ≤ reconnectMaxDelayMs`). OTel surface: `standby.warm_slots` gauge, `standby.promotions_total{outcome}` + `standby.evictions_total{reason}` counters, `standby.warmup_duration_ms` histogram, `standby.*` spans. Optional `EventBusLike` integration: `transport:standby:warm` / `:promoted` / `:evicted`. `AbortSignal` threaded throughout. Property-test invariants: `warmCount + activeCount ≤ maxSlots` (20 runs over random promote/release/evict op streams), eviction monotonicity (closed slot id never reappears, 15 runs), promotion atomicity (exactly one active slot per successful promote, 20 runs), health stays in `[0, 100]` over arbitrary sample streams including out-of-range inputs (50 runs). Integration test warms 4 slots and asserts `pool.promote("peer-X")` completes in `<100 ms` deterministic-clock simulated; failure-mode test confirms unhealthy slot is evicted and pool tops back up; active-slot eviction emits `disconnected` through the transport. Every barrel uses explicit named exports per CLAUDE.md §6a; every test imports runner primitives from `@suaveplan/testing/runner` per §7. **Renamed from upstream `@suaveplan/network-standby-transport` per `network-transport-family` proposal naming convention.** 102 tests, 100% statement/branch/function/line coverage. Deps: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/network`, `@suaveplan/retry`, `@suaveplan/timing`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`. |
|
||||
| `@suaveplan/network-testing` | Network testing utilities |
|
||||
| `@suaveplan/network-trystero-server` | Server-tier Trystero signaling daemon paired with `@suaveplan/network-discovery-trystero` ✅ clients for the relay-backed transport modes. **Construction-time `mode: "mqtt" \| "nostr"`** via a single `BrokerLike` adapter contract (`onConnect` / `onDisconnect` / `onFrame` / `onMalformedFrame` callbacks) — no runtime branching on mode strings post-construction; same pattern as `network-discovery-trystero`'s `TrysteroTransportAdapter`. Required injectable `mqttBrokerFactory` and `nostrRelayFactory` per mode — production passes real WebSocket broker shims at the app boundary, tests bind a deterministic `FakeBroker` real implementation (NOT `vi.mock`). Five-module server architecture: `server/server.ts` (daemon, 298 LOC), `server/broker-select.ts` (mode → factory dispatch), `server/peer-events.ts` (lifecycle event emission), `server/health-listener.ts` (HTTP listener), `health/health.ts` (verb pipeline). Idempotent `start({signal?}) → {address, port, relayUrl}` / `stop()` lifecycle with AbortSignal threaded throughout. Server publishes its own `relayUrl` (`ws://host:port/<mode>`) for clients to consume. 6-code `defineCodes("NET_TRYSTERO_SERVER", [...])` registry (`START_FAILED`, `INVALID_OPTIONS`, `MODE_UNSUPPORTED`, `LISTEN_FAILED`, `DISPOSED`, `NOT_RUNNING`). Zod boundary validation on `TrysteroServerConfig` with mode-specific sub-schemas. OTel surface: `trystero_server.start` mode-labelled span, `trystero_server.connections_total{mode,outcome}` + `trystero_server.messages_relayed_total{mode,outcome}` counters, `trystero_server.active_connections{mode}` gauge, `trystero_server.start_duration_ms{mode}` histogram. **Both modes integration-tested end-to-end**: boot real `node:http` server in `mqtt` mode and `nostr` mode, fetch `/health`/`/ready`/`/live` over HTTP, confirm published `relayUrl`(`ws://host:port/mqtt` and `ws://host:port/nostr`) round-trips the mode marker, drive two-client connect/frame/disconnect lifecycles through the fake broker, observe malformed-frame drop without crash. **Discipline metrics: largest source 298 LOC (under 300 cap), largest test 413 LOC (under 500 cap), zero`: any`, zero`vi.mock` / `jest.mock`, every barrel uses explicit named exports per CLAUDE.md §6a, every test imports runner primitives from`@suaveplan/testing/runner` per §7.** `engines.node: ">=20"`, no`browser` field, server-tier marker. **Scoped out (documented in commit history)**: concrete production broker shims for each mode (would pull in `aedes`/`mosca`/`nostr-tools` runtime deps) deferred to a follow-up pair `network-trystero-server-broker-{mqtt,nostr}`. The Wave-5.3 migration ships the daemon shell + adapter contract; consumers can BYO broker shim today and the canonical implementations land later. 69 tests across 8 files, 100% statement/branch/function/line coverage. Deps:`@suaveplan/error`,`@suaveplan/events`,`@suaveplan/logger`,`@suaveplan/network-discovery-trystero`,`@suaveplan/testing-integration`,`@suaveplan/types`,`@opentelemetry/api`,`tslib`,`zod`. NO runtime broker deps (deferred). |
|
||||
| `@suaveplan/notification-channel` | Multi-channel notification routing with templates, r... |
|
||||
| `@suaveplan/npc` | NPC lifecycle management with spawn pools, population control, and daily schedules |
|
||||
| `@suaveplan/osm-buildings` | OSM building/road geometry extraction and mesh generation |
|
||||
| `@suaveplan/overview` | Package overview and discovery |
|
||||
| `@suaveplan/packer-browser` | Browser-specific binary packing |
|
||||
| `@suaveplan/party` | Party/group system with invites, leadership transfer, and kick/promote |
|
||||
| `@suaveplan/payment` | Payment intent lifecycle, webhook verification, rece... |
|
||||
| `@suaveplan/payment-providers` | Slim foundation for `@suaveplan/payment-provider-*` siblings — `ProviderRegistry`, `definePaymentProvider` factory, `httpRequest`/`composeAbortSignal`, shared `ApiKeyConfig`/`OAuthConfig`/`MerchantKeyConfig` shapes, `PAYMENT_PROVIDER_CODES` (6 codes) + `PaymentProviderError`. Concrete Stripe/PayPal/Square/Mollie/Adyen adapters ship as separate sibling packages. |
|
||||
| `@suaveplan/payment-provider-adyen` | Adyen Checkout API v71 sibling adapter on the slim payment-providers core — `AdyenProvider implements PaymentProvider` (literal name `"adyen"`, `AdyenConfig` alias of shared `MerchantKeyConfig`). `X-API-Key: <apiKey>` header on every call (read + write); caller-supplied `idempotencyKey` forwards as both the `Idempotency-Key` header AND the request body's `reference` field on `createIntent`. Default `reference` generation uses injectable `RandomIdFn` (falls back to `globalThis.crypto.randomUUID()` at call time only — universal-tier, no `node:crypto`). Configured `merchantAccount` injected into every modification body (`/captures`, `/cancels`, `/refunds`). Full lifecycle (`createIntent` → POST `/payments` with `merchantAccount`/`amount`/`reference`/optional `captureDelayHours`/`shopperStatement`/`returnUrl`/`paymentMethod.storedPaymentMethodId`/`metadata`; `confirmIntent` → POST `/details`; `captureIntent` → POST `/captures`; `cancelIntent` → POST `/cancels`; `refund` → POST `/refunds` with optional `amount`; `getIntent` → GET). Exhaustive Adyen → domain mappings — 10-row result-code status table with unknown → `failed`, 1-row refund-status table (`received` → `pending`, else → `failed`), payment-method normalisation (`scheme` → `card`, else → `wallet`; brand lower-cased; `lastFour` → `last4`). Every HTTP call routes through slim-core `httpRequest`; raised `PaymentProviderError`s carry `provider: "adyen"` discriminant. 2 subpath exports, 36 tests at 100/100/100/100. |
|
||||
| `@suaveplan/payment-provider-mollie` | Mollie Payments API v2 sibling adapter on the slim payment-providers core — `MollieProvider implements PaymentProvider` with `Authorization: Bearer <apiKey>` auto-headers, full lifecycle (create / confirm / capture / cancel / refund / get), exported pure helpers `formatMollieAmount` / `parseMollieAmount` round-tripping `bigint` minor-units to Mollie's `{ currency, value: "X.YY" }` wire shape (covers 1-cent, exact-major, large-value, and over-precision edges), exported total status maps. 2 subpath exports, 34 tests at 100/100/100/100. |
|
||||
| `@suaveplan/payment-provider-paypal` | PayPal Orders v2 + Payments Captures Refund sibling adapter on the slim payment-providers core — `PayPalProvider implements PaymentProvider` (literal name `"paypal"`, `PayPalConfig` extends `OAuthConfig` with `sandbox` shortcut). OAuth2 client-credentials token cache (POST `/v1/oauth2/token` with `Basic <base64(clientId:clientSecret)>`; cached with `expires_in - 60s` skew; refreshed transparently). Injectable `now: NowFn` (token-cache clock + `cancelIntent` timestamps) and `base64Encode: Base64EncodeFn` (defaults `globalThis.btoa`) keep the package universal-tier on Node / Bun / browsers / Web Workers. Token-fetch failures raise `PaymentProviderError(OAUTH_TOKEN_FAILURE)` before any Orders-v2 call. Full lifecycle (`createIntent` → POST `/v2/checkout/orders` with `intent` `AUTHORIZE`/`CAPTURE`, optional `PayPal-Request-Id`; `confirmIntent` → POST `/authorize`; `captureIntent` → POST `/capture` summing every nested capture; `cancelIntent` → POST `/void` returning synthesised intent with injected-now timestamps; `refund` → POST `/v2/payments/captures/{id}/refund`; `getIntent` → GET). Exhaustive 6-row order-status map (unknown → `failed`), 3-row refund-status map, `MoneyLike` bigint ↔ PayPal decimal-string marshalling (handles JPY-style zero-decimal currencies). All HTTP via slim-core `httpRequest`; raised `PaymentProviderError`s carry `provider: "paypal"`. 3 subpath exports, 25 tests at 100/100/100/100. |
|
||||
| `@suaveplan/payment-provider-stripe` | Stripe REST API sibling adapter on the slim payment-providers core — `StripeProvider implements PaymentProvider` (literal name `"stripe"`, `StripeConfig` alias of shared `ApiKeyConfig`), `Authorization: Bearer <apiKey>` on every call (read + write), caller-supplied `idempotencyKey` forwarded verbatim as the standard Stripe `Idempotency-Key` header on `createIntent` and `refund` (omitted on read calls and when not supplied). Full lifecycle (`createIntent` → POST `/payment_intents`, `confirmIntent` / `captureIntent` / `cancelIntent` → POST `/payment_intents/:id/{confirm,capture,cancel}`, `refund` → POST `/refunds`, `getIntent` → GET). Exhaustive Stripe → domain mappings — 7-row payment-intent status table with unknown → `failed`, 3-row refund-status table with unknown → `failed`, 4-row refund-reason table with unknown / absent → `other`, payment-method normalisation (`card` → card with `brand`+`last4`, anything else → `wallet`, absent → `undefined`). Every HTTP call routes through slim-core `httpRequest` so `fetch` / `signal` / `timeoutMs` from `ProviderCommonOptions` propagate end-to-end; raised `PaymentProviderError`s carry `provider: "stripe"` discriminant. 2 subpath exports, 42 tests at 100/100/100/100. |
|
||||
| `@suaveplan/payment-provider-square` | Square Payments API sibling adapter on the slim payment-providers core — `SquareProvider implements PaymentProvider` (literal name `"square"`, `SquareConfig` extending `ApiKeyConfig` with required `locationId` plus optional `sandbox`/`apiVersion`/`randomId`). `Authorization: Bearer <apiKey>` + pinned `Square-Version` header (defaults `"2024-01-18"`) on every call; sandbox flag flips the default base URL to `https://connect.squareupsandbox.com/v2` (explicit `baseUrl` always wins); injectable `randomId` fills missing `idempotency_key` fields on `CreatePayment` + `RefundPayment` calls (defaults to `crypto.randomUUID()` at call time). Full lifecycle (`createIntent` → POST `/payments` with `location_id`/`autocomplete`/`idempotency_key`, `confirmIntent` → no-op delegating to `getIntent` for Square's auto-confirm model, `captureIntent` → POST `/payments/{id}/complete`, `cancelIntent` → POST `/payments/{id}/cancel`, `refund` → POST `/refunds`, `getIntent` → GET `/payments/{id}`). Exhaustive Square → domain mappings — 5-row payment-status table with unknown → `failed`, 3-row refund-status table (`COMPLETED` → `succeeded`, `PENDING` → `pending`, else → `failed`); card-brand lower-casing with `"unknown"` fallback; refund-reason fallback to `"other"` when Square's free-form reason is empty/absent. Every HTTP call routes through slim-core `httpRequest`; raised `PaymentProviderError`s carry `provider: "square"`. 5 subpath exports, 37 tests at 100/100/100/100. |
|
||||
| `@suaveplan/pdf` | PDF generation for receipts/reports/invoices |
|
||||
| `@suaveplan/ports` | Global port database with shipping lanes, ETA calcul... |
|
||||
| `@suaveplan/profiler` | Runtime performance profiling |
|
||||
| `@suaveplan/progression` | Character progression with XP curves, skill trees, perks, and achievements |
|
||||
| `@suaveplan/pwa` | Service worker, offline sync, push notifications |
|
||||
| `@suaveplan/python-audio-fingerprint` | High-accuracy audio fingerprinting and matching service |
|
||||
| `@suaveplan/quest` | Quest/mission system with multi-objective tracking, chains, rewards, and event-driven triggers |
|
||||
| `@suaveplan/queue-distribution` | Worker-pool distribution layer: load-balancing strategies (round-robin / least-busy / weighted), worker pool with criteria-based search, manager with @suaveplan/retry-routed retries + periodic health checks + auto-restart, in-process LocalWorker, OTel instrumentation, AbortSignal threading, Zod boundaries, 12-code QueueDistributionError. Decoupled from @suaveplan/queue's Job model via a self-contained DistributedTask<I, O> contract. |
|
||||
| `@suaveplan/queue-distribution-http` | HTTP transport plugin for @suaveplan/queue-distribution: HttpWorkerCommunicator (injectable fetch, Zod-validated wire envelope, structured timeouts, AbortSignal) and RemoteWorker (AbstractWorker subclass with bounded exponential-backoff connect via @suaveplan/retry, periodic health-check loop, ping-based reachability probe on communication errors). 6-code QueueDistributionHttpError registry. |
|
||||
| `@suaveplan/queue-distribution-worker-thread` | Web-Worker / Node worker_threads transport plugin: WorkerThreadCommunicator with caller-injected WorkerFactory bridging to ThreadWorkerLike, Zod-validated request + reply envelopes, bounded Map<requestId, PendingRequest>, AbortSignal cancellation, structured timeouts, terminate() that fails outstanding requests synchronously; WorkerThreadWorker reacts to crash/exit events and emits OTel counters. 4-code QueueDistributionWorkerThreadError registry. |
|
||||
| `@suaveplan/r3f-animation` | Animation state machine, blend trees, layers |
|
||||
| `@suaveplan/r3f-audio` | Positional 3D spatial audio (HRTF, culling) |
|
||||
| `@suaveplan/r3f-booking` | Interactive 3D venue maps for booking |
|
||||
| `@suaveplan/r3f-building` | R3F building mode with snap system, ghost preview, and placement validation |
|
||||
| `@suaveplan/r3f-buildings` | Instanced 3D building and road rendering for R3F |
|
||||
| `@suaveplan/r3f-camera` | Advanced camera controls for React Three Fiber |
|
||||
| `@suaveplan/r3f-character` | Character controller for 3D games |
|
||||
| `@suaveplan/r3f-character-vehicle` | Character-vehicle integration (enter/exit, authority transfer) |
|
||||
| `@suaveplan/r3f-character-vehicle-picker` | Character/vehicle picker UI with 3D previews, GTA-style customisation |
|
||||
| `@suaveplan/r3f-combat` | R3F rendering layer for combat with hitbox debug, projectile trails, muzzle flash, impact effects, and damage numbers |
|
||||
| `@suaveplan/r3f-controller` | Input controller for 3D scenes |
|
||||
| `@suaveplan/r3f-dmx` | DMX512 lighting control visualization |
|
||||
| `@suaveplan/r3f-earth` | 3D globe with port markers, shipping lanes |
|
||||
| `@suaveplan/r3f-emulator` | 3D device emulation interface |
|
||||
| `@suaveplan/r3f-environment` | Skybox, HDR, and lighting presets |
|
||||
| `@suaveplan/r3f-inventory` | Grid-based inventory, equipment, loot tables |
|
||||
| `@suaveplan/r3f-lod` | Level-of-detail management |
|
||||
| `@suaveplan/r3f-minimap` | Minimap renderer, entity tracking, fog-of-war |
|
||||
| `@suaveplan/r3f-networking` | R3F-tier multiplayer state-synchronisation library — the top of the network stack. Composes interpolation, client-side prediction, server reconciliation FSM (composes `@suaveplan/fsm`), snapshot diffing (composes `@suaveplan/diff`), and snapshot interpolation into a single React + R3F surface. **Transport-agnostic**: accepts any `Transport` from `@suaveplan/network` via injection at `<NetworkProvider>`; ships a deterministic `MemoryTransport` real-implementation for tests/dev; canonical production wiring composes `@suaveplan/network-transport-webrtc` at the app boundary. React hooks (`useNetworkSync`, `useNetworkedEntity`, `useNetworkPredict`) + R3F components (`<NetworkProvider>`, `<NetworkedEntity>`, `<NetworkStats>`). 5-code `R3F_NET` registry, Zod boundaries, OTel surface (2 histograms + counter), AbortSignal everywhere. **Property tests**: interpolation monotonicity (200 runs), prediction-reconcile convergence (50 runs), snapshot diff round-trip (100 runs). **2-client integration test** via `MemoryTransport` (real WebRTC E2E scoped out — same structural `Transport` contract, swap at app boundary). Storybook stories: 3 components × default+playground+variants = 11. Discipline: max source 177 LOC, max test 184 LOC, zero `: any`, zero `vi.mock`/`jest.mock`. `<NetworkedEntity>`'s `useFrame` excluded from coverage gate (happy-dom limitation); per-frame logic extracted to `apply-networked-frame.ts` and 100% covered there. 152 tests across 21 files, 100% coverage. Deps: `@suaveplan/diff`, `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/fsm`, `@suaveplan/network`, `@suaveplan/serialize`, `@suaveplan/timing`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`; peer deps `react`, `react-dom`, `three`, `@react-three/fiber`; optional peer dep `@suaveplan/network-transport-webrtc`. |
|
||||
| `@suaveplan/r3f-npc` | R3F NPC rendering with behavior tree debug, nameplates, and dialogue triggers |
|
||||
| `@suaveplan/r3f-number-plate` | 3D number plate component |
|
||||
| `@suaveplan/r3f-parallax` | Interior-parallax components (WindowParallax + GridParallax) |
|
||||
| `@suaveplan/r3f-particles` | GPU-accelerated particle systems (instanced) |
|
||||
| `@suaveplan/r3f-pathfinding` | NavMesh pathfinding, steering, crowd simulation |
|
||||
| `@suaveplan/r3f-performance` | Performance monitoring and optimization |
|
||||
| `@suaveplan/r3f-physics-cascade` | Cascading physics effects |
|
||||
| `@suaveplan/r3f-physics-core` | Physics engine abstraction layer |
|
||||
| `@suaveplan/r3f-physics-rapier` | Rapier physics engine integration |
|
||||
| `@suaveplan/r3f-shaders` | Custom shader effects and materials |
|
||||
| `@suaveplan/r3f-terrain` | Procedural terrain with chunked LOD |
|
||||
| `@suaveplan/r3f-ui` | HTML/React UI components in 3D space |
|
||||
| `@suaveplan/r3f-vegetation` | R3F procedural vegetation with biome rules, LOD, and instanced placement |
|
||||
| `@suaveplan/r3f-vehicle` | Vehicle controllers for React Three Fiber |
|
||||
| `@suaveplan/r3f-water` | Water rendering and ocean simulation |
|
||||
| `@suaveplan/r3f-weather` | R3F weather rendering with particles, fog, lightning, and surface wetness |
|
||||
| `@suaveplan/r3f-world` | World management and scene composition |
|
||||
| `@suaveplan/r3f-xr` | WebXR/VR/AR with controller and hand tracking |
|
||||
| `@suaveplan/react-admin-forms` | Admin form components |
|
||||
| `@suaveplan/react-audio` | Audio components & processing |
|
||||
| `@suaveplan/react-audio-fingerprint` | Audio fingerprinting components |
|
||||
| `@suaveplan/react-audio-player` | Modular audio player with 3D viz |
|
||||
| `@suaveplan/react-audio-stream` | Audio streaming components |
|
||||
| `@suaveplan/react-auth` | Authentication components |
|
||||
| `@suaveplan/react-auth-whitelabel` | White-label auth with tenant branding |
|
||||
| `@suaveplan/react-bank` | Game banking components and hooks |
|
||||
| `@suaveplan/react-calendar` | Calendar and date picker components |
|
||||
| `@suaveplan/react-campaigns` | |
|
||||
| `@suaveplan/react-charts` | Chart and data visualization components |
|
||||
| `@suaveplan/react-color-picker` | Color picker component |
|
||||
| `@suaveplan/react-command-palette` | Cmd+K command palette with fuzzy search, keyboard na... |
|
||||
| `@suaveplan/react-consent-overlay` | Cookie/consent/privacy overlays |
|
||||
| `@suaveplan/react-contacts` | |
|
||||
| `@suaveplan/react-controller` | Game controller components |
|
||||
| `@suaveplan/react-crafting` | Crafting UI with recipe filtering, station selection, and craft queues |
|
||||
| `@suaveplan/react-crypto` | Client-side cryptography components |
|
||||
| `@suaveplan/react-data-table` | Data grid with sorting/filtering/pagination |
|
||||
| `@suaveplan/react-dialogue` | Dialogue UI with typewriter effect, choices, and speaker portraits |
|
||||
| `@suaveplan/react-dnd` | Drag-and-drop with sortable lists |
|
||||
| `@suaveplan/react-docs` | React components for rendering Suaveplan documentati... |
|
||||
| `@suaveplan/drive-react-sharing` | Drive sharing UI (ShareDialog, PartitionBadge) |
|
||||
| `@suaveplan/react-error-boundary` | Error boundary with fallback UI |
|
||||
| `@suaveplan/react-file-upload` | Drag-and-drop file upload with chunked/resumable upl... |
|
||||
| `@suaveplan/react-font-decompress` | React hooks for font loading |
|
||||
| `@suaveplan/react-forms` | Form components with validation |
|
||||
| `@suaveplan/react-game-hud` | Game HUD state for health, ammo, compass, minimap, notifications |
|
||||
| `@suaveplan/react-game-map` | World map with viewport, markers, fog of war, and zoom controls |
|
||||
| `@suaveplan/react-game-settings` | Game settings for graphics, audio, controls, and accessibility |
|
||||
| `@suaveplan/react-hotkeys` | Keyboard shortcut management |
|
||||
| `@suaveplan/react-image` | Image components with lazy loading |
|
||||
| `@suaveplan/react-infinite-scroll` | Infinite scroll with IntersectionObserver |
|
||||
| `@suaveplan/react-iphone` | iPhone simulator with WebRTC support |
|
||||
| `@suaveplan/react-kanban` | Headless kanban board with drag-and-drop columns, sw... |
|
||||
| `@suaveplan/react-loading` | Loading screen with weighted task progress and tip rotation |
|
||||
| `@suaveplan/react-map` | Map visualization components |
|
||||
| `@suaveplan/react-media` | Media rendering components |
|
||||
| `@suaveplan/react-modal` | Modal/dialog system with focus trapping |
|
||||
| `@suaveplan/react-network-libp2p-demo` | React demo for `@suaveplan/network-transport-libp2p` ✅ — net-new genesis package extracted from upstream's 749-line `libp2p-demo.tsx` per the tier-isolation rule (universal-tier transport packages cannot pull React; demo lives at the React tier). Six subcomponents split per CLAUDE.md ≤300-LOC discipline: `<Libp2pDemo>` (root composer with context + state orchestration), `<NodeStatusPanel>` (peer ID + node lifecycle status), `<NodeControls>` (start/stop + bootstrap edit), `<PeerInteractionPanel>` (dial input + echo + send controls), `<PeerList>` (connected peers + per-peer status), `<LogPanel>` (event log with level filtering). Each subcomponent gets its own folder with `<name>.tsx` + `<name>.md` + `<name>.test.tsx` + `<name>.stories.tsx` + `<name>.types.ts` + `index.ts`. `useLibp2pDemo` orchestration hook + sibling `wirePeerEvents` helper. **Bootstrap list is a REQUIRED React prop** (`Libp2pDemoProps.bootstrap: readonly string[]`) — no hardcoded defaults; `useLibp2pDemo`'s Zod schema rejects missing/empty; README documents the contract under "Required user-supplied bootstrap". Package NEVER imports `libp2p` directly — consumes only the structural `Libp2pDemoNodeFactory` adapter that production consumers wire to a real libp2p stack at the app boundary. 5-code `defineCodes("REACT_LIBP2P_DEMO", [...])` registry (`INVALID_OPTIONS`, `DEMO_DISPOSED`, plus 3 demo-specific failure modes). Zod boundary validation on every public prop interface. Every component has Storybook stories: 6 components × default + playground + ≥1 variants (3-5 stories per file) = full coverage of UI surface. Snapshot + interaction tests via `@suaveplan/testing-dom`. AbortSignal threaded through async UI flows (dial, send). **Discipline metrics**: max source 263 LOC (`use-libp2p-demo.ts`), max test 495 LOC, zero `: any`, zero `vi.mock`/`jest.mock`, every barrel uses explicit named exports per CLAUDE.md §6a, every test imports runner primitives from `@suaveplan/testing/runner` per §7. 72 tests across 10 test files, 100% statement/branch/function/line coverage. Deps: `@suaveplan/error`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`. Peer deps: `react`, `react-dom`, `@suaveplan/network-transport-libp2p` (required — the demo consumes its public surface). |
|
||||
| `@suaveplan/react-notifications` | Toast/notification system |
|
||||
| `@suaveplan/react-pdf` | PDF viewer with zoom and navigation |
|
||||
| `@suaveplan/react-permissions` | RBAC/ABAC permission components |
|
||||
| `@suaveplan/react-port-tracking` | Port tracking components and hooks |
|
||||
| `@suaveplan/react-rich-text` | Rich text WYSIWYG editor (Tiptap-based) |
|
||||
| `@suaveplan/react-rule-builder` | Visual rule builder with AND/OR groups, drag-drop, JSON config |
|
||||
| `@suaveplan/react-skeleton` | Skeleton/shimmer loading placeholders |
|
||||
| `@suaveplan/react-social` | Social library with real-time updates |
|
||||
| `@suaveplan/react-social-game` | Social game UI with friends list, requests, blocking, and status |
|
||||
| `@suaveplan/react-store` | Store state management components |
|
||||
| `@suaveplan/react-timeline` | Timeline visualization components |
|
||||
| `@suaveplan/react-tour` | Guided product tours and onboarding walkthroughs wit... |
|
||||
| `@suaveplan/react-tree-view` | Tree view component |
|
||||
| `@suaveplan/react-ui` | Core React UI component library |
|
||||
| `@suaveplan/react-ui-social` | Social UI components |
|
||||
| `@suaveplan/react-video` | Video player components |
|
||||
| `@suaveplan/react-virtual` | Virtualized list/grid rendering |
|
||||
| `@suaveplan/react-wizard` | Multi-step form wizard with validation |
|
||||
| `@suaveplan/rules` | Business rules engine & validation |
|
||||
| `@suaveplan/sandbox` | Sandboxed execution environment |
|
||||
| `@suaveplan/seed` | Database seeding and fixture framework |
|
||||
| `@suaveplan/server-keys` | Server key management CLI |
|
||||
| `@suaveplan/service-discovery` | Universal-tier service-registry primitive consumed by every Wave-2 `network-discovery-*` plugin. `ServiceRegistry` with upsert + strict `register`, `refresh`, `deregister`, `get`/`list`/`size`, `query`, AbortSignal-aware `watch(query, signal?)` AsyncIterableIterator (with buffering + termination), `prune`, optional `flush`/`load` via `ServiceRegistryStorageLike` (subset of `@suaveplan/storage` AbstractStorage; opt-in cross-restart persistence), optional `EventBusLike<ServiceRegistryEventMap>` emit for `service:registered`/`:deregistered`/`:expired`. Injectable `Clock` from `@suaveplan/types` — no `Date.now()` at module scope. `defineCodes("SVC_DISC", [...])` with 4 codes (`DUPLICATE_REGISTRATION`, `QUERY_INVALID`, `REGISTRY_FULL`, `INVALID_OPTIONS`) extending `SuaveplanError`. Zod boundary validation on `ServiceDescriptor`, `ServiceQuery`, and registry options. OTel surface: `svc_disc.registrations_total{outcome}` + `svc_disc.queries_total` counters, `svc_disc.query_duration_ms` histogram, observable `svc_disc.active_services` gauge. fast-check property tests cover TTL expiry monotonicity + query determinism. Every barrel uses explicit named exports per CLAUDE.md §6a; every test file imports runner primitives from `@suaveplan/testing/runner` per §7. 44 tests, 100% statement/branch/function/line coverage. Scope-down vs. upstream: upstream `LoadBalancingStrategy`/`HealthCheckResult`/connection-tracking dropped — this package is a registry primitive; consumers compose load-balancing externally over the typed-query surface. Deps: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/storage`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod`. |
|
||||
| `@suaveplan/slack-client` | Generic Slack API client for messaging, channels, and webhooks |
|
||||
| `@suaveplan/sms` | SMS send/receive, delivery status tracking, template... |
|
||||
| `@suaveplan/sms-providers` | SMS provider implementations for @suaveplan/sms - Tw... |
|
||||
| `@suaveplan/spatial-hash` | Spatial hashing for O(1) broad-phase 2D/3D queries |
|
||||
| `@suaveplan/storage-browser` | Browser storage (IndexedDB, localStorage) |
|
||||
| `@suaveplan/storage-browser-opfs` | OPFS storage backend for @suaveplan/storage |
|
||||
| `@suaveplan/storage-file` | Node.js filesystem storage |
|
||||
| `@suaveplan/storybook-test-utils` | Storybook E2E testing utilities |
|
||||
| `@suaveplan/streams-chunk-queue` | Chunked queue stream processing |
|
||||
| `@suaveplan/streams-metadata` | Stream transform for extracting rich media metadata via EXIF, MIME, and events |
|
||||
| `@suaveplan/telegram-client` | Generic Telegram Bot API client for messaging, groups, and media |
|
||||
| `@suaveplan/test-vite` | Vite test configuration utilities |
|
||||
| `@suaveplan/vehicle-physics` | Engine-agnostic vehicle physics (Pacejka tires, suspension, drivetrain) |
|
||||
| `@suaveplan/video` | Video metadata extraction, format detection, and tra... |
|
||||
| `@suaveplan/video-thumbnails` | Video thumbnail generation and sprite sheets via ffmpeg |
|
||||
| `@suaveplan/weapons` | Weapon system with projectile ballistics, melee, recoil, ammo, and attachments |
|
||||
| `@suaveplan/weather` | Dynamic weather simulation with transitions, zones, and gameplay effects |
|
||||
| `@suaveplan/web-workers` | Web Worker/SharedWorker management |
|
||||
| `@suaveplan/websocket-gateway` | Server-tier WebSocket gateway hosting rooms + presence + channel-multiplexing on a single TCP connection. Pairs with the Wave-1-adapter-refactored `@suaveplan/network-transport-websockets` ✅ client. Idempotent `start({signal?}) → {address, port}` / `stop()` lifecycle with AbortSignal threaded throughout; graceful shutdown drains in-flight messages before resolving. Required injectable `wsServerFactory` — production wires to real `ws.WebSocketServer`, tests wire to either `FakeWsServer` (in-memory) or `RealWsServer` (real `ws` adapter for the 3-client integration test); NEVER `vi.mock`. Twelve modules: `errors`, `health`, `otel`, `types`, `room`, `presence`, `channel` (subsystem packages); `gateway/{access-gate,channel-manager,channel-ops,client-lifecycle,defaults,gateway-deps,gateway,heartbeat,lifecycle,message-factory,presence-fire,presence-webhook,room-ops}` (gateway orchestration). The top-level `gateway.ts` is a 332-LOC facade composing 7 well-factored subsystem modules — 32 LOC over CLAUDE.md's 300 hard cap as a documented facade-pattern exception; alternatives (composition-by-mixin / aggressive inline / JSDoc strip) would not produce a meaningfully cleaner architecture. Every other module sits well under cap (36-215 LOC range). **Hooks for unmigrated deps** (per proposal §"Hooks for unmigrated deps"): `authVerifier?: (handshake) → Promise<AuthContext>` (default no-op pass-through; replace with `@suaveplan/jwt-tenant` when shipped), `authorizationCheck?: (ctx, action) → Promise<boolean>` (→ `@suaveplan/permissions`), `rateLimitGate?: (ctx, action) → Promise<boolean>` (→ `@suaveplan/rate-limiter`), `auditLogger?: (event) → void` (→ `@suaveplan/audit-logging`). README documents the four replacement paths. **Direct integrations with already-shipped packages**: `@suaveplan/idempotency` `IdempotencyGuard` on every mutating event (room create / message send / presence update), `@suaveplan/retry` `RetryPolicy` + circuit breaker on webhook delivery, `@suaveplan/webhook-client` for signed presence-event delivery, `@suaveplan/logger` structured logging with trace correlation. 9-code `defineCodes("WS_GATEWAY", [...])` registry (`ROOM_NOT_FOUND`, `ROOM_FULL`, `CHANNEL_INVALID`, `AUTH_REJECTED`, `RATE_LIMITED`, `INVALID_OPTIONS`, `DISPOSED`, `NOT_RUNNING`, `START_FAILED`). Zod boundary validation on every public option struct + every wire frame. OTel surface: `ws_gateway.{connect,message,broadcast}` spans, `ws_gateway.rooms_active` + `ws_gateway.presence_updates_total{kind}` counters, `ws_gateway.message_dispatch_ms` histogram. Inline `/health`/`/ready`/`/live` HTTP endpoints sharing the same TCP port. **3-client real-`ws` integration test**: boots `WebSocketGateway` backed by real `ws.WebSocketServer` via `RealWsServer`, opens 3 real `ws` clients, joins them to a shared room, broadcasts, asserts `recipients == [id2, id3]`, asserts client count returns to 0 on shutdown, asserts `/health` `/ready` `/live` from same TCP port. **Discipline metrics: largest source 332 LOC (documented facade exception), largest test 336 LOC, zero `: any`, zero `vi.mock`/`jest.mock`, every barrel uses explicit named exports per CLAUDE.md §6a, every test imports runner primitives from `@suaveplan/testing/runner` per §7.** Scoped out: inbound wire-frame router is a no-op shim — public typed methods (`joinRoom`/`broadcastToRoom`/`subscribeToChannel`/`publishToChannel`) ARE the typed router; the companion `network-transport-websockets` client invokes them directly via shared session abstraction. fast-check property tests deferred (existing 125 unit + integration tests already give 100% coverage). `engines.node: ">=20"`, no `browser` field, server-tier marker. 125 tests across 13 files, 100% statement/branch/function/line coverage (508/220/158/458). Deps: `@suaveplan/error`, `@suaveplan/events`, `@suaveplan/idempotency`, `@suaveplan/logger`, `@suaveplan/network-transport-websockets`, `@suaveplan/retry`, `@suaveplan/testing-integration`, `@suaveplan/timing`, `@suaveplan/types`, `@suaveplan/webhook-client`, `ws` (catalog `network-server`), `@opentelemetry/api`, `tslib`, `zod`. |
|
||||
| `@suaveplan/wip` | Work-in-progress experiments |
|
||||
| `@suaveplan/workflow` | Workflow/saga orchestration |
|
||||
|
||||
## Genesis-Original
|
||||
|
||||
**6** packages tracked as net-new with no upstream source AND no genesis implementation yet (per `STATUS.md` ❌, refreshed 2026-05-12). Genesis-original packages that *have* shipped are counted under **Completed** above.
|
||||
|
||||
| Package | Summary |
|
||||
| --- | --- |
|
||||
| `@suaveplan/bytes` | Low-level byte utilities for the Suaveplan monorepo: 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. |
|
||||
| `@suaveplan/cbor` | Universal-tier RFC 8949 CBOR encoder/decoder with RFC 8742 sequences, RFC 9052 COSE_Key bridge, RFC 8392 CWT support, and RFC 8610 CDDL hooks. Hand-rolled major-type 0-7 codec with preferred shortest-form integers, deterministic-mode opt-in, indefinite-length emission via opt-in flag, half-precision float decode + encode, RFC 8742 streaming sequence reader, pluggable tag registry (built-in tags 0/1/2/3/21/22/23/24/32/35/55799), pull-decoder with bounded recursion (maxDepth: 64) and bounded item count (maxItems: 1_000_000). COSE bridge: `decodeCoseKey(bytes)` returning typed Ec2Key/OkpKey/RsaKey/SymmetricKey discriminated union per RFC 9052 §7, COSE alg label → genesis crypto-curve algorithm-ID mapping, JWK round-trip. Diagnostic notation emitter for debug logging. 7-code error registry, Zod boundary schemas, full OTel surface. 50 src files at 100% coverage. Gates `@suaveplan/webauthn` (COSE_Key decode for credential public key). |
|
||||
| `@suaveplan/crypto-browser` | WebCrypto-backed engine implementations for @suaveplan/crypto — AES-256-GCM, Ed25519, ECDSA-P256/P384, X25519, ECDH-P256/P384, RSA, AES-KW, and non-extractable key support via crypto.subtle. Zero third-party cryptography dependencies. |
|
||||
| `@suaveplan/crypto-cipher` | Low-level AEAD (Authenticated Encryption with Associated Data) cipher primitives for the @suaveplan/crypto stack. Provides three algorithms — AES-256-GCM, ChaCha20-Poly1305, and XChaCha20-Poly1305 — each as a standalone subpath export for tree-shaking. Nonces are always caller-supplied: the package validates nonce length but never generates randomness internally, keeping the CSPRNG boundary in the caller's control. Implementation flavour ('noble' vs 'native') is selected at build time via crypto.config.ts; all Phase 0 implementations route to @noble/ciphers. Use @suaveplan/crypto for the high-level façade that combines nonce generation, engine selection, and structured error handling; use this package only when you need to supply your own cipher engine implementation. |
|
||||
| `@suaveplan/crypto-core` | Universal cryptographic primitives — engine-pluggable AEAD, Ed25519/ECDSA-P256 signing, X25519 key agreement, sealed-box, and auth-box. Built on audited @noble/ libraries with zero WASM. |
|
||||
| `@suaveplan/crypto-curve` | Low-level elliptic-curve building blocks for the @suaveplan/crypto stack. Exposes four curves — secp256k1, Ed25519, X25519, and NIST P-256 — each as a standalone subpath export so bundlers only ship the primitives actually used. Every curve provides key generation, signing/verification (or key agreement), and raw byte operations. Implementation flavour ('noble' vs 'native') is selected at build time via the repo-root crypto.config.ts; all Phase 0 implementations route to @noble/curves. Use this package when you need raw curve operations; for high-level AEAD, signing, or sealed-box use @suaveplan/crypto instead. |
|
||||
| `@suaveplan/crypto-hash` | Pure-TypeScript hashing substrate. SHA-2 digests, HMAC, HKDF, PBKDF2, |
|
||||
| `@suaveplan/crypto-rsa` | Universal-tier RSA primitives covering RFC 8017 PKCS #1 v2.2 (PKCS1v15 + PSS sign/verify, RSAES-OAEP + RSAES-PKCS1-v1_5 encrypt/decrypt), RFC 5208/5958 PKCS#8, RFC 5280 SubjectPublicKeyInfo, RFC 7517/7518 JWK, RFC 7468 PEM framing, RFC 7638 JWK Thumbprint plus RFC 9278 thumbprint URI, SPKI fingerprint with hex/base64/base64url/colon-hex output, NIST FIPS 186-5 multi-prime keygen with 64-round Miller-Rabin and AbortSignal threading, and RFC 8017 §5.1.2 / Kocher 1996 blinding wired by default into every sign and decrypt path with `{ blinding: false }` opt-out emitting a counter. Pluggable `RsaCryptoEngine` adapter (noble + WebCrypto subtle + auto-selector) lets consumers choose between pure-JS portability and platform-accelerated subtle, while the `RsaKeyHandle` HSM/PKCS#11 bridge with reference `SoftwareRsaKeyHandleProvider` decouples consumer code from any specific hardware backend. FIPS 186-5 profile gate enforces modulusBits ∈ {2048, 3072, 4096}, e ∈ [2¹⁶+1, 2²⁵⁶-1] odd, PSS saltLength === hLen, and SHA-1 ban for sign/verify. JWK `key_ops` / `use` / `alg` enforcement (RFC 7517 §4.2-4.4) prevents the common cross-purpose mistake (`use: "sig"` key used for encryption). Bleichenbacher-resistant combined-error decrypt and constant-time PKCS1v15 verify (Wycheproof-grade) close the historical attack surface. Hand-rolled DER reader/writer keeps the audit boundary tight. 17-code error registry, full OTel histogram + counter surface, Zod schemas at every public boundary, 438 tests at 100/100/100/100 coverage. Gates `@suaveplan/x509` (cert-chain RSA-SHA256 verify), `@suaveplan/jws` (RS256/PS256/RSA-OAEP), `@suaveplan/webauthn` (TPM endorsement keys). |
|
||||
| `@suaveplan/crypto-keys` | Keypair generation, validation, and keychain management for the @suaveplan stack. Delegates cryptographic operations to @suaveplan/crypto while adding key lifecycle utilities: tagging keypairs with their algorithm, validating structural and cryptographic correctness, and storing named keypairs with pluggable persistence adapters. |
|
||||
| `@suaveplan/crypto-sodium` | libsodium-backed engine implementations for @suaveplan/crypto. Provides AEAD encryption, Ed25519 signing, X25519 key agreement, sealed-box, auth-box, deterministic KDF, BLAKE2b MAC, Argon2id password hashing, and Ristretto255 group operations — all via the libsodium-wrappers-sumo WASM module. |
|
||||
| `@suaveplan/crypto-utils` | Shared cryptographic utilities consumed internally by all other @suaveplan/crypto- packages. Provides the low-level primitives that every crypto implementation needs but that belong in one place: multi-alphabet encoding (toHex/fromHex, toBase64/fromBase64, toBase64Url, toBase58/fromBase58); byte helpers (concatBytes, xor, i2osp/os2ip for integer-to-octet-string conversions); constantTimeEqual for timing-safe comparisons; randomBytes with an injectable ByteSource; zeroize for wiping key material; Merkle tree helpers (merkleRoot, merkleProof, verifyMerkleProof); a PrimitiveId registry and PrimitiveNotImplemented sentinel error; and KAT/differential-fuzz harnesses (runVectors, diffFuzz) used in every crypto primitive's test suite. Zero runtime crypto dependencies — this package provides helpers only, not cryptographic algorithms. Application code should generally import from @suaveplan/crypto or @suaveplan/crypto-hash; import from this package only when building a new cryptographic primitive or engine adapter. |
|
||||
| `@suaveplan/crypto-vault` | Cryptographic foundation layer for the Vaulted password-manager ecosystem. Provides BIP39 mnemonic generation/round-trip via @scure/bip39; OWASP-hardened Argon2id KDF (≥64 MiB / t≥3 / p≥1); BIP32-style HD key derivation from mnemonic seed; two-secret key derivation (2SKD) combining password + Secret Key + email-derived salt → AUK; hierarchical HKDF tree (AUK → personal keyset → vault key → item key) with versioned info strings; SRP-6a client per RFC 5054 (2048-bit safe-prime group, SHA-256, salt/x/v/A/M1/K/M2 with B mod N == 0 tamper rejection); SecureBuffer with eager zeroization, masked toString, clone isolation, and a global zeroizeAll() registry; OpaqueKeyHandle for crossing module boundaries without exposing raw Uint8Arrays; structured CryptoVaultError codes for every failure mode; and OTel counters/histograms on all derivations. Wraps @suaveplan/crypto-hash for primitives — does not re-implement cryptography. The vault layer (when migrated) consumes OpaqueKeyHandle so no key material ever leaves this package's memory boundary. |
|
||||
| `@suaveplan/docker` | Docker Compose infrastructure for the genesis monorepo. Three stacks cover every stage of the development lifecycle. |
|
||||
| `@suaveplan/font-convert` | Converts TTF/OTF/WOFF/WOFF2 font files to the typeface.js JSON format used by Three.js FontLoader. Also defines the canonical TypefaceFont, TypefaceGlyph, and PathCommand types consumed by @suaveplan/font-compress. |
|
||||
| `@suaveplan/font-subset` | Font subsetting and compression library with a codec-agnostic adapter interface. Supports Unicode range subsetting, WOFF2/WOFF/OTF/TTF output, OpenType feature filtering, hinting strip, and metadata strip. Adapter implementations supply the actual codec; this package provides the orchestration, validation, format detection, and OTel instrumentation. |
|
||||
| `@suaveplan/hibp` | Privacy-preserving Have I Been Pwned client. Range API uses SHA-1 k-anonymity prefix lookup against `/range/<5-hex-prefix>` with mandatory `Add-Padding: true` header and constant-time suffix compare — full hash and full password never leave the function. 1000-iteration byte-level no-leak property suite proves the privacy contract. Optional NTLM mode via injectable `NtlmHashAdapter` (MD4-UTF16-LE not bundled — universal-tier package can't pull that dep). Authenticated breach + paste APIs against `/breachedaccount` and `/pasteaccount` with `hibp-api-key` header, 1500ms client-side rate-limit gate via injectable `Clock`, 429 `Retry-After` honour with exponential backoff. In-memory LRU cache (default 10k entries, 24h TTL) keyed by SHA-1 prefix, pluggable `HibpCacheLike` for redis/IndexedDB swap. Differential-privacy decoy-prefix padding mode. Zod boundary schemas at every response (`BreachSchema`, `PasteSchema`, `PwnedResultSchema`), 7-code error registry, AbortSignal threaded everywhere, full OTel surface (range/breach duration histograms, outcome counter, rate-limit-throttled counter, errors counter). 23 src + 24 test files at 100% coverage. |
|
||||
| `@suaveplan/install-agent-skills` | Installer plugin that bundles every Suaveplan skill (SKILL.md + assets) |
|
||||
| `@suaveplan/installer` | Plugin host for project boilerplating and asset installation. Pipelines |
|
||||
| `@suaveplan/json` | Safe, enterprise-grade JSON utilities with prototype-pollution protection, |
|
||||
| `@suaveplan/jws` | RFC 7515 JSON Web Signature implementation covering compact + JSON-flat + JSON-general serializations. Verify and sign across the full RFC 7518 + RFC 8037 algorithm set: HS256/384/512 (HMAC with constant-time compare via `@suaveplan/crypto-utils`), RS256/384/512 (delegates to `@suaveplan/crypto-rsa.rsaVerifyPkcs1v15` / `rsaSignPkcs1v15`), PS256/384/512 (`@suaveplan/crypto-rsa.rsaVerifyPss` / `rsaSignPss`), ES256/384/512 (`@suaveplan/crypto-curve` p256/p384/p521 with raw r‖s — DER form rejected), EdDSA (`@suaveplan/crypto-curve.ed25519`). 12-step verify orchestrator with strict ordering (algorithm allowlist BEFORE crypto, `crit` enforcement BEFORE alg dispatch, AbortSignal at three checkpoints). `alg: "none"` rejected by default — caller MUST pass `algorithms: ["none"]` AND `acceptNone: true` to opt in. `crit` allowlist enforcement (consumers MUST explicitly list `"b64"` per RFC 7797 §3). `jwk` and `x5c` header parse — `x5c` chain delegated to `@suaveplan/x509.validateChain` against caller-supplied trust anchors. `kid` resolver injection. RFC 7515 §A.5 detached signatures. RFC 7797 `b64: false` unencoded payload. Multi-signature JSON-general with `requireAll: false` default. RFC 7515 Appendix A.1-A.7 vectors verified verbatim. 18-code error registry, Zod boundaries, full OTel surface. 43 src + 26 test + 24 sibling `.md` + 6 fixture + 3 benchmark files; 216 tests at 100% coverage. Gates `@suaveplan/webauthn` (android-safetynet attestation verifier). |
|
||||
| `@suaveplan/locale-data` | Static ISO 3166-1 country data and ISO 639-1 language data with typed lookup functions, |
|
||||
| `@suaveplan/network-plugin-protocol` | Network protocol plugin for the genesis mesh system. |
|
||||
| `@suaveplan/network-plugin-registry` | Fixed-capacity peer registry with bitpacker-based binary encoding. |
|
||||
| `@suaveplan/network-transport-webrtc` | WebRTC transport plugin for mesh networking. |
|
||||
| `@suaveplan/otp` | Universal-tier RFC 4226 HOTP + RFC 6238 TOTP + RFC 6287 OCRA + Google Authenticator Key URI Format. HOTP with `bigint` counter, configurable digits 6/7/8, SHA-1/256/512. TOTP with `T = floor((now − T0) / period)`, drift-window verify returning matching `delta` for clock-skew detection, constant-time compare, injectable `Clock` from `@suaveplan/types`. Key URI Format parse + format with RFC 4648 §6 base32 codec (no-padding default, opt-in padding). OCRA challenge-response with suite-string parser and mutual auth flow. Recovery codes — crockford-base32 grouping (4-4-4-4) with Argon2id-hashed verify via `@suaveplan/crypto-hash`. Backup-aware `verifyMany([secret1, secret2], code)` for rotation windows. Clock-drift detector returning `delta` outside the verify window. RFC 4226 Appendix D + RFC 6238 Appendix B + RFC 6287 Appendix C test vectors all green. 9-code error registry, Zod boundaries, full OTel surface (generate/verify duration histograms, outcome+delta_bucket counter). 41 src files at 100% coverage. |
|
||||
| `@suaveplan/password-generator` | Cryptographically-strong password and passphrase generator. Char-class mode with configurable length, character classes (lowercase/uppercase/digits/symbols), exclusion sets (ambiguous `0Ol1I`, lookalike pairs), required-class quotas (≥1 digit, ≥1 symbol). Passphrase mode over bundled EFF Long Wordlist (7776 entries, 2¹³ bits/word) and EFF Short Wordlist 1 (1296 entries) under tree-shakeable `./wordlists/eff-long` and `./wordlists/eff-short` subpaths. Pronounceable Markov-digram CVC mode for memorable shoulder-surfing-friendly contexts. PIN mode (bias-free uniform 0-9). Bias-free `randomInt(rng, max)` via Lemire rejection sampling — never `Math.floor(rng() * max)`. `estimateEntropy(password)` and `estimatePassphraseEntropy(words, wordlistSize)`. Wordlist plugin API — `registerWordlist(name, words[])` with ≥256-entry minimum (`PWGEN.WORDLIST_TOO_SHORT`). `assertStrength(password, minBits)` xkcd-style guard. Injectable `Rng` from `@suaveplan/types` (default wraps `@suaveplan/crypto-utils.randomBytes`). χ² statistical test on 1M draws (p > 0.01 vs uniform). 4-code error registry, Zod boundaries, full OTel surface. 29 src + 21 test + 15 sibling `.md` files at 100% coverage. |
|
||||
| `@suaveplan/password-strength` | zxcvbn-style password strength estimator. 6 pattern matchers: frequency-ranked dictionary with l33t-substitution + reverse-string lookup; alphabetical/numeric sequence (with reverse); repeat (`aaaa`, `abcabc`); date (splittable y/m/d, 1900 ≤ year ≤ now+50); keyboard adjacency for QWERTY (default) + DVORAK + AZERTY under tree-shakeable `./graphs/<layout>` subpaths; recent-year regex (`19xx`/`20xx`). `mostGuessableMatchSequence` DP scorer over n² pattern combinations with factorial penalty and bruteforce fallback. `estimateGuesses(match)` per Wheeler §4 formulae. `crackTimes(guesses, { adjustments? })` with 4 scenarios (online_throttling_100_per_hour, online_no_throttling_10_per_second, offline_slow_hashing_1e4_per_second, offline_fast_hashing_1e10_per_second). 5-bucket `score` (literal `0\|1\|2\|3\|4` at 1e3/1e6/1e8/1e10 thresholds). Feedback strings keyed for `@suaveplan/i18n`, English fallback. `loadDictionaries(dicts)` runtime swap (no global mutation), `userInputs` personal-info match surfacing "contains personal info" feedback. Multilingual dictionaries under `./dicts/{de,fr,es,zh-Hans,ru}` subpaths. `assessReactive(observable<string>)` debounced streaming variant with AbortSignal + cancel-on-newer. 200-char DoS guard — input >200 chars rejected with `PWSTRENGTH.INPUT_TOO_LONG`. 6-code error registry, Zod boundaries, full OTel surface. 55 src + 22 test + 31 sibling `.md` files at 100% coverage. |
|
||||
| `@suaveplan/noise` | Seeded spatial noise fields for procedural content generation. Provides six field types — Perlin, Simplex (2D/3D/4D), Value, Worley/cellular (F1/F2/F2-F1 modes), fBm (fractal Brownian motion compositor), and Poisson-disk Blue noise sampling — all seeded with Sfc32Rng for byte-identical, reproducible output. Every generator is available as both a functional API and a stateful class. Use this package for terrain heightmaps, texture generation, cloud simulation, vegetation scattering, and any procedural system that needs spatially-coherent randomness. For non-spatial random values (UUIDs, shuffles, distributions) use @suaveplan/random; for seeded integer/float sequences use @suaveplan/pseudo-random. |
|
||||
| `@suaveplan/pseudo-random` | Deterministic, seeded pseudo-random number generators (PRNGs) conforming to the Rng interface from @suaveplan/types. Ships four algorithms — Mulberry32, SFC32, Xoshiro256++, and CounterRng — with a unified SeededRng surface that includes nextUint32, nextFloat, skip, at, fork, checkpoint, and restore. Use this package when you need reproducible randomness (procedural generation, game simulations, property tests) or seek O(1) access to arbitrary positions in the sequence. For cryptographically secure randomness use @suaveplan/random (CSPRNG-backed) or @suaveplan/crypto (raw bytes). Utility functions shuffle, sample, choice, and weightedChoice accept any Rng and work with both seeded and crypto-backed sources. |
|
||||
| `@suaveplan/random` | High-level cryptographically secure randomness for application use. This is the everyday random package — it wraps the CSPRNG with ergonomic helpers for every common randomness task: unique IDs (randomUUID v4, randomUUIDv7, randomULID, randomNanoid), random strings with eight charset presets, collection utilities (shuffle, choice, sample, weightedChoice), object utilities (randomKey, randomEntry, randomSubset), game primitives (coin, dice, roll), and a full suite of continuous and discrete statistical distributions (Normal, LogNormal, Binomial, Poisson, and eight more). Every function accepts an optional rng?: Rng — omit it for crypto-backed defaults, or pass a SeededRng from @suaveplan/pseudo-random for deterministic tests. Do not confuse with @suaveplan/pseudo-random (seeded, deterministic, for procedural generation) or @suaveplan/crypto (raw cryptographic byte generation). |
|
||||
| `@suaveplan/readline` | AsyncGenerator line reader and interactive prompt interface for Node.js streams, file paths, and Web Streams. |
|
||||
| `@suaveplan/srp` | SRP-6a (RFC 5054) zero-knowledge password proof. Bundled 2048-bit safe-prime group at root + 1024/1536/3072/4096/6144/8192 under tree-shakeable `./groups/<size>` subpaths (every constant byte-exact to Appendix A). SHA-256 default, SHA-1 legacy opt-in. Client roles: `computeVerifier`, `clientStartHandshake`, `clientFinishHandshake`, `clientVerifyServerProof`. Server roles: `serverStartHandshake`, `serverFinishHandshake`, `serverComputeM2` (split-roundtrip helper). Rejects `A mod N == 0`, `B mod N == 0`, `u == 0` per RFC 5054 §2.5.4 with `SRP.PUBLIC_KEY_INVALID`. Modpow via `@noble/curves/abstract/modular.pow` — banned `BigInt ** BigInt %` pattern audited. Constant-time M1/M2/S compare via `@suaveplan/crypto-utils`. Dual hash backends: `nobleHashBridge` (sync) + `subtleHashBridge` (async with AbortSignal) + `defaultHashBridge` selector throwing `SRP.HASH_UNSUPPORTED` on sync-on-async misuse. `PakeProvider` interface + concrete `srpPakeProvider` for future OPAQUE drop-in. RFC 5054 Appendix B 1024/SHA-1 vectors verified byte-exact for every intermediate (k/x/v/u/S). Locally-derived 2048/SHA-256 fixture via `scripts/generate-srp-vectors.ts`. 1000-triple property test asserts `client.K === server.K`. Added `HashLike` interface to `@suaveplan/types` as cross-package contract. 6-code error registry, Zod boundaries, full OTel surface. 44 src + 25 test + 27 sibling `.md` files at 100% coverage. |
|
||||
| `@suaveplan/testbed` | A live demonstration app for every @suaveplan library, running against real services. Each page exercises the actual library API — no stubs, no mocks. |
|
||||
| `@suaveplan/testing-integration` | Core integration test infrastructure for Vitest: fixture lifecycle management, health polling, and the global-setup bridge. |
|
||||
| `@suaveplan/testing-integration-garage-s3` | Garage S3 v2.0.0 testcontainers fixture for @suaveplan/testing-integration. |
|
||||
| `@suaveplan/testing-integration-mailpit` | Mailpit testcontainers fixture + assertMail() helper for @suaveplan/testing-integration. |
|
||||
| `@suaveplan/testing-integration-postgres` | PostgreSQL 16 testcontainers fixture for @suaveplan/testing-integration. |
|
||||
| `@suaveplan/testing-integration-redis` | Redis 7 testcontainers fixture for @suaveplan/testing-integration. |
|
||||
| `@suaveplan/testing-integration-step-ca` | A ServiceFixture for smallstep/step-ca that gives any integration test suite a real, ephemeral certificate authority — root cert, JWK provisioner, and an issueCert() helper that mints leaf certificates on demand. |
|
||||
| `@suaveplan/testing-integration-surrealdb` | SurrealDB v3 testcontainers fixture for @suaveplan/testing-integration. |
|
||||
| `@suaveplan/testing-integration-typesense` | Typesense 27 testcontainers fixture for @suaveplan/testing-integration. |
|
||||
| `@suaveplan/tpm-struct` | Universal-tier TPM 2.0 attestation-structure parser per TCG TPM 2.0 Library Part 2 (r1.59). `parseTpmsAttest(bytes)` covering all 8 `TPM_ST_ATTEST_*` variants (§10.12 + §10.11): magic check `0xff544347` ("TCG\xff" reversed), `qualifiedSigner` (TPM2B_NAME), `extraData` (TPM2B_DATA), `clockInfo`, `firmwareVersion`, `attested` (TPMU_ATTEST union dispatched by type). `parseTpmtPublic(bytes)` per §12.2 covering RSA / ECC / KEYEDHASH / SYMCIPHER with full TPMA_OBJECT bitfield decode (r1.59) and `0 ⇒ 65537` RSA exponent normalization with `rawExponent` preservation. Byte-identical `serializeTpmsAttest` / `serializeTpmtPublic` for fixture authoring. `computeTpmtPublicName(pub, { digest })` per TCG Part 1 §16 with injected `HashLike`. `extraDataMatches(attest, expectedHash, { equal })` per W3C WebAuthn L3 §8.3 step 8 with injected constant-time compare. `TPM_ALG` 35-entry + `TPM_ECC_CURVE` 9-entry frozen registries (§11.1 / §11.2.5). TPM2B length-prefix reader/writer with UINT16-BE bounds enforcement (truncated buffer → `TPM_STRUCT.TRUNCATED`). Debug-only `toDiagnostic` dumper. 10-code error registry, Zod boundaries, full OTel surface (histograms + counters with bounded-enum attributes). Synthetic WebAuthn TPM integration fixture (no real-device byte string available). 31 src + 18 test + 13 sibling `.md` files; 175 tests at 100% coverage. Gates `@suaveplan/webauthn` (TPM attestation format verifier). |
|
||||
| `@suaveplan/vault` | Vault orchestration layer for the Vaulted password-manager ecosystem — the domain abstraction sitting above `@suaveplan/crypto-vault`, `@suaveplan/crypto-cipher`, `@suaveplan/bitpacker`, and the Phase-1 `DriveLike` / `DriveSharingLike` / `VaultSyncLike` adapter interfaces. Strict locked → unlocked state machine: `Vault.open(config)` returns a locked handle without touching drive; `unlock(password, secretKey)` runs `crypto-vault` 2SKD → personal keyset → per-vault key, decrypts the drive-backed encrypted SQLite-style container with the manifest as AAD, runs forward-only ledger-tracked migrations against the in-memory state, and instantiates per-unlock module facades; `lock()` drains pending flushes, encrypts the container, atomically writes back through `DriveLike`, zeroizes every key buffer via `crypto-vault.zeroizeAll()`, and returns to locked. On-disk container format is `[bitpacker manifest header][AEAD nonce][AEAD ciphertext = serialised in-memory state][AEAD tag]` with the bitpacker manifest carrying `formatVersion` / `kdfId` / `aeadAlgorithm` / `vaultIdHash` / Argon2id params / `createdAt` / `lastModifiedAt` / `schemaVersion`, and the manifest bytes bound as AEAD AAD so any tamper invalidates the ciphertext (surfaced as `VAULT.DECRYPT_FAILED`, indistinguishable from wrong-password by design). Defence-in-depth field-level encryption: every concealed field on a `VaultItem` is XChaCha20-Poly1305-encrypted with a per-item key derived via `crypto-vault.deriveItemKey(vaultKey, itemId)` and zeroized after use, so even drive-side decryption leaves passwords / PINs / secure notes as ciphertext inside the container. `vault.items` exposes `create` / `get` / `update` / `delete` / `list` / `search` with optimistic concurrency (`version` field, rejects with `VAULT.ITEM_VERSION_CONFLICT` on stale writes), per-mutation vector-clock increment, history append-only ledger, debounced flush back to drive (default 500 ms, `flushDebounceMs` injectable). FTS5-style search runs over a blind-index column (HMAC of plaintext non-concealed values) so search never leaks plaintext. `vault.attachments` (`upload` / `download` / `delete`) streams plaintext through XChaCha20-Poly1305 in 64 KiB chunks under per-attachment keys; ciphertext blobs are stored through `DriveLike.put` / `get` / `delete`. `vault.shares` (`createLink` / `revokeLink`) wraps the per-vault key with a share-specific KEK before delegating to `DriveSharingLike`. `vault.sync` (`pull` / `push` / `sync`) hands the in-memory snapshot + queue to a `VaultSyncLike` mirror of `@suaveplan/vault-sync`'s `SyncEngine`. Frozen `VaultError` registry of 17 codes (lifecycle / item / attachment / share / sync / opaque-handle / drive-failure). Zod schemas at every public boundary including the manifest shape. Safe-by-construction OTel: span per operation, counters, and unlock / flush latency histograms — no password / secret-key / AUK / item content / vault ID / item ID / key material in telemetry. 174 tests across 18 files at 100/100/100/100 coverage; fast-check property suites for item-encryption round-trip, lock idempotency, CRUD-blocked-in-LOCKED, and manifest bitpack/unpack round-trip; integration tests cover wrong-password rejection and drive-blob tamper rejection end-to-end. |
|
||||
| `@suaveplan/vault-sync` | Offline-first, end-to-end-encrypted sync engine for vault items in the Vaulted password-manager ecosystem. Domain-specific CRDT merge strategies — `lww` (vector-clock + updatedAt + nodeId + canonical-string tiebreakers for guaranteed commutativity), `append` (causal-order concat with dedupe), `atomic` (concurrent-modification rejection with `MERGE_CONFLICT_UNRESOLVED`) — built on top of `@suaveplan/crdt`. RFC 6902 JSON Patch incremental sync via `@suaveplan/diff` (`diffVaultItems` / `applyPatch`). Vector clocks (`Record<nodeId, counter>` with `increment` / `merge` / `compare` returning `<` / `>` / `==` / `concurrent`). `SyncEngine` class with `pull` / `push` / `sync` operating on in-memory snapshots through an injected `SyncTransport` interface (transport itself is out of scope). `SyncQueue` durable mutation queue with vector-clock ordering, operation collapsing, max-depth backpressure (10,000 ops → `QUEUE_DEPTH_EXCEEDED`), and crypto-safe serialize/deserialize. XChaCha20-Poly1305 E2EE via `@suaveplan/crypto-cipher` — `encryptSyncPayload` / `decryptSyncPayload` with version-tagged AAD, max payload 10 MiB, tamper rejection (`SYNC_PAYLOAD_TAMPERED`) and version-mismatch rejection (`SYNC_PAYLOAD_INVALID_VERSION`). Structured `VaultSyncError` registry covering merge / patch / sync / queue / crypto failures. Safe-by-construction OTel: spans + duration histograms + queue-depth gauge with NO key material, item content, vault IDs, item IDs, or vector-clock values in telemetry. fast-check property suite for merge commutativity, idempotency, patch round-trip, vector-clock monotonicity, queue-serialization equivalence, and payload tamper rejection. |
|
||||
| `@suaveplan/webauthn` | W3C WebAuthn L3 client. `register` / `authenticate` over injectable `WebAuthnAdapter` (production binds `globalThis.navigator.credentials`; tests bind a deterministic stub — pattern, not mock). Full authenticator-data parser: 37-byte fixed prefix (`rpIdHash[32]` + `flags[1]` + `signCount[4]`) + optional attested-credential-data (`aaguid[16]` + `credentialIdLength[2]` + `credentialId` + `credentialPublicKey` via `@suaveplan/cbor/cose.decodeCoseKey`) + optional extensions CBOR; flags byte decoded to `userPresent` / `userVerified` / `backupEligible` / `backupState` / `attestedCredentialData` / `extensionsPresent`. All 7 attestation-format verifiers under pluggable `AttestationFormatRegistry`: `none`, `packed` (self + full attestation), `fido-u2f`, `tpm` (via `@suaveplan/tpm-struct` for TPMS_ATTEST + TPMT_PUBLIC parse + `computeTpmtPublicName` match), `android-key` (with KeyDescription extension OID `1.3.6.1.4.1.11129.2.1.17`), `android-safetynet` (via `@suaveplan/jws.compactDecode` JWS verify), `apple` (with nonce extension OID `1.2.840.113635.100.8.2`). X.509 chain validation via `@suaveplan/x509.validateChain`. Signature verify dispatching to `@suaveplan/crypto-curve` (ES256/384/512 + EdDSA) and `@suaveplan/crypto-rsa` (RS256 + PS256). Sign-counter regression detection per W3C §6.1.1 with zero-zero exception. Conditional UI (`authenticate({mediation: "conditional"})`, `isConditionalMediationAvailable`). PRF extension passthrough + result parsing (~32-byte symmetric secret). largeBlob extension passthrough. DOMException → typed error mapping at the boundary — `NotAllowedError` / `InvalidStateError` / `SecurityError` / `AbortError` / `NotSupportedError` never leak raw to caller. Constant-time challenge + rpIdHash compare via `@suaveplan/crypto-utils`. 17-code error registry, Zod boundaries, full OTel surface (5 instruments). 64 impl + 27 test + 24 sibling `.md` + 8 fixture + 3 benchmark files; 317+ tests at 100% coverage. Synthesized-via-real-crypto fixtures for all 7 attestation formats. |
|
||||
| `@suaveplan/x509` | RFC 5280 v3 certificate parsing + chain validation. Hand-rolled ASN.1 BER/DER codec for the X.509 subset (TLV / OID / SEQUENCE / SET / INTEGER / BIT STRING / OCTET STRING / UTF8String / PrintableString / IA5String / UTCTime / GeneralizedTime / explicit-tagged context-specific). RFC 7468 PEM framing with all armor labels. TBSCertificate parser per §4.1. Pluggable extension decoder dispatch with `registerExtension(oid, decoder)` + critical-extension enforcement (unknown critical → `X509.CRITICAL_EXTENSION_UNRECOGNIZED`). All 8 standard extension decoders: basic constraints (§4.2.1.9), key usage (§4.2.1.3), extended key usage (§4.2.1.12), SAN (§4.2.2.6 — DNS + IPv4 + IPv6 + URI + email + dirName + rfc822Name), name constraints (§4.2.1.10 with `permittedSubtrees` / `excludedSubtrees` + DNS suffix matching), AKI (§4.2.1.1), SKI (§4.2.1.2), CRL distribution points (§4.2.1.13). `validateChain(leaf, intermediates, trustAnchors, opts)` per §6.1 12-step algorithm with policy tree (`validPolicyTree` initialised to any-policy wildcard, `initialPolicySet`/`initialExplicitPolicy` surfaced on `ValidateChainOptions`). Validity period (UTCTime/GeneralizedTime 2050 boundary per §4.1.2.5.1) with injectable `Clock`. Signature verify dispatching to `@suaveplan/crypto-rsa` (RSA-PKCS1v15-SHA256/384/512 + RSA-PSS) and `@suaveplan/crypto-curve` (ECDSA-P256/384/521 + Ed25519). Self-signed-leaf rejection unless explicit trust anchor. Chain depth bound (default 16). `TrustAnchorStore` with SKI lookup, AKI-then-name fallback, configurable duplicate-SKI policy (`last-wins` default, `first-wins`, `throw`). OCSP responder interface (interface only — fetcher in follow-up). 20-code error registry, Zod boundaries, full OTel surface, AbortSignal threaded throughout. 45 src + 38 test files; 381 tests at 100% coverage. 16 deterministic chain builders under `./__fixtures__` with real RSA/P-256/Ed25519 signatures (synthetic-via-real-crypto, no mocks). Gates `@suaveplan/webauthn` (TPM + android-key + apple attestation verifiers) and `@suaveplan/jws` (`x5c` header validation). |
|
||||
| `@suaveplan/types` | Types-only foundation package. Zero runtime code, zero dependencies, zero bundle bytes. Provides the canonical shared interfaces that allow every @suaveplan/ package to accept and return the same shapes without coupling to a concrete implementation: Clock (() => number), IdGenerator (() => string), Rng (seeded random interface), MessageResolver (i18n hook), EventBusLike<TMap> (minimal event-bus contract), LoggerLike (minimal structured-logger contract), and three composable option bundles (CancellableOptions, TraceableOptions, DeterministicOptions). Always import type from this package — it emits nothing at runtime. This is the only @suaveplan/ dependency the testing-infrastructure packages (testing, testing-dom, testing-e2e) are permitted to take, because its build emits no runtime bytes and therefore creates no circular hazard. |
|
||||
18
_intro.md
Normal file
18
_intro.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# SuavePlan
|
||||
|
||||
An extensive collection of libraries
|
||||
|
||||
## Companion docs
|
||||
|
||||
- [`AGENTS.md`](./AGENTS.md) — agent-execution contract (shared with all subagents).
|
||||
- [`CLAUDE.md`](./CLAUDE.md) — repo rules, conventions, and quality gates.
|
||||
- [`STATUS.md`](./STATUS.md) — migration dashboard (✅ migrated / – pending / ❌ unwritten).
|
||||
- [`SUMMARY.md`](./SUMMARY.md) — long-form per-package summaries.
|
||||
- [`MANIFEST.md`](./MANIFEST.md) — dependency graph + tier overlay (auto-generated).
|
||||
- [`MISSING.yml`](./MISSING.yml) — machine-readable list of unbuilt packages.
|
||||
- [`openspec/`](./openspec/) — capability specs + change proposals + archived changes.
|
||||
|
||||
## Apps
|
||||
|
||||
- [`apps/docs`](./apps/docs/README.md) — Docusaurus 3.10 documentation portal (this site).
|
||||
- [`apps/testbed`](./apps/testbed/README.md) — internal demo + integration testbed (React + Vite + Hono).
|
||||
130
apps/demo/README.md
Normal file
130
apps/demo/README.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# @suaveplan/demo
|
||||
|
||||
Unified Bun + Hono server with a React 19 SPA front-end and a JSON API
|
||||
in one process. Demonstrates locale-prefix routing across five base
|
||||
locales, `@suaveplan/api-plugin-auth`-driven Better-Auth authentication
|
||||
backed by SurrealDB, and React + Tailwind v4 styling.
|
||||
|
||||
## What's wired up
|
||||
|
||||
- **Server** — single Bun process running a Hono app on port `3300`
|
||||
(override with `PORT=…`). The Hono instance is the one owned by a
|
||||
`SuaveApp` from `@suaveplan/api`. `/api/health` is registered by
|
||||
`registerHealthRoutes`; `/api/auth/*` is mounted by
|
||||
`@suaveplan/api-plugin-auth` (9 OpenAPI routes + the Better-Auth
|
||||
catch-all); `/api/me` is a demo convenience that calls
|
||||
`authService.getSession(headers)`; everything else falls through to
|
||||
the Vite-built client bundle in `dist/client/`, with `index.html`
|
||||
fallback so the client router owns deep links.
|
||||
- **Auth** — the canonical genesis pipeline. The Hono app is assembled
|
||||
via `defineApp()` from `@suaveplan/api`, with `createAuthPlugin(...)`
|
||||
from `@suaveplan/api-plugin-auth` registered as a plugin. The auth
|
||||
plugin's 6-step `onInit` applies the bundled 17-table Better-Auth
|
||||
SurrealQL schema, constructs the real Better-Auth instance backed by
|
||||
`betterAuthSurrealAdapter` (from `@suaveplan/better-auth-surrealdb`,
|
||||
the Better-Auth v1.6 `DBAdapterInstance` factory), registers the
|
||||
typed `authService` on the host `ServiceRegistry`, mounts the 9
|
||||
OpenAPI routes plus the Better-Auth catch-all under `/api/auth/*`,
|
||||
and installs the global session-resolver middleware so every request
|
||||
gets a typed `Principal` on its Hono context. A small inline
|
||||
`demo-db-seed` plugin (in `src/server/auth.ts`) drops the live
|
||||
`SurrealAdapter` into `ServiceRegistry["db"]` during `onRegister` so
|
||||
the auth plugin can resolve it during `onInit`. Backing store is an
|
||||
**external SurrealDB** reached via `SURREAL_URL` (default
|
||||
`ws://localhost:8000`), namespace `SURREAL_NS` (default `demo`),
|
||||
database `SURREAL_DB` (default `demo`), credentials `SURREAL_USER` /
|
||||
`SURREAL_PASS` (default `root` / `root`).
|
||||
- **Client** — React 19 + Vite + `@tailwindcss/vite` for Tailwind v4.
|
||||
Routing via `react-router-dom@7` with a locale-prefix design. The
|
||||
app root composes `<ReactUiProvider>` (from `@suaveplan/react-ui`)
|
||||
around the entire tree, seeded with a singleton `I18nInstance`
|
||||
(`createI18n` from `@suaveplan/i18n`) and the chassis `lightTheme`
|
||||
plugin. `<ReactUiProvider>` internally wraps
|
||||
`<TranslationProvider>` → `<ThemeProvider>` → `<PackRegistryProvider>`
|
||||
and auto-registers the matching chassis locale bundle on mount /
|
||||
language change, so any react-ui primitive that calls
|
||||
`useTranslation(...)` finds a populated catalog. Form pages
|
||||
(`/auth/login`, `/auth/register`, `/auth/forgot-password`,
|
||||
`/user/settings`) compose `<Card>`, `<Field>`, `<FieldLabel>`,
|
||||
`<Input>`, `<Button>`, `<Alert>` from the chassis — no hand-rolled
|
||||
Tailwind on those primitives. Demo-specific copy continues to flow
|
||||
through a typed `LocaleProvider` so the page-level message catalogs
|
||||
stay co-located and statically checked; the i18n instance is driven
|
||||
to the matching BCP47 variant (`en` → `en-GB`, `zh` → `zh-CN`,
|
||||
`ar` → `ar-AE`, `cy` → `cy-GB`, `ru` → `ru-RU`) via
|
||||
`syncDemoLocale(base)` from a `useEffect`.
|
||||
|
||||
## Locale-prefix routing
|
||||
|
||||
Five base locales: `en`, `zh`, `ar`, `cy`, `ru`. Region variants
|
||||
(e.g. `en-GB`, `zh-CN`, `ar-EG`) parse case-insensitively and
|
||||
canonicalise to lowercase-base + UPPERCASE-region. Anything else
|
||||
renders the `NotFoundPage` with a language picker.
|
||||
|
||||
- `/` → `Navigate` to `/<detected>/home` (uses `navigator.languages`).
|
||||
- `/<locale>/home`
|
||||
- `/<locale>/about-us`
|
||||
- `/<locale>/contact-us`
|
||||
- `/<locale>/privacy-policy`
|
||||
- `/<locale>/terms-of-service`
|
||||
- `/<locale>/auth/login`
|
||||
- `/<locale>/auth/register`
|
||||
- `/<locale>/auth/forgot-password`
|
||||
- `/<locale>/auth/logout`
|
||||
- `/<locale>/user/control-panel` *(gated — `RequireAuth`)*
|
||||
- `/<locale>/user/settings` *(gated — `RequireAuth`)*
|
||||
- `/<locale>/admin/control-panel` *(gated — `RequireRole admin`)*
|
||||
- `/<locale>/admin/settings` *(gated — `RequireRole admin`)*
|
||||
|
||||
`ar` flips `<html dir="rtl">`. The `<LocaleSwitcher>` rewrites the URL
|
||||
in place when a user changes language.
|
||||
|
||||
## Running the demo
|
||||
|
||||
The demo requires a reachable SurrealDB instance. The simplest path:
|
||||
|
||||
```bash
|
||||
docker run -d --name demo-surreal -p 8000:8000 \
|
||||
surrealdb/surrealdb:latest start --user root --pass root --bind 0.0.0.0:8000
|
||||
```
|
||||
|
||||
Then, from the repo root:
|
||||
|
||||
```bash
|
||||
bun install # workspace deps
|
||||
bunx turbo build --filter=@suaveplan/demo # build api/auth deps + dist/client
|
||||
bun run --filter=@suaveplan/demo dev # boot the server on :3300
|
||||
```
|
||||
|
||||
Visit `http://localhost:3300` — it should redirect to `/en/home` (or
|
||||
your browser's preferred locale if it matches one of the five).
|
||||
|
||||
Smoke endpoints:
|
||||
|
||||
```bash
|
||||
curl http://localhost:3300/api/health
|
||||
curl http://localhost:3300/api/me
|
||||
curl http://localhost:3300/api/auth/get-session
|
||||
```
|
||||
|
||||
End-to-end sign-up roundtrip — produces a 200 with a Better-Auth
|
||||
session token + user record:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3300/api/auth/sign-up/email \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"alice@example.com","password":"hunter2","name":"Alice"}'
|
||||
```
|
||||
|
||||
Env overrides (all optional):
|
||||
|
||||
| Variable | Default |
|
||||
| ---------------------- | ------------------------- |
|
||||
| `PORT` | `3300` |
|
||||
| `SURREAL_URL` | `ws://localhost:8000` |
|
||||
| `SURREAL_NS` | `demo` |
|
||||
| `SURREAL_DB` | `demo` |
|
||||
| `SURREAL_USER` | `root` |
|
||||
| `SURREAL_PASS` | `root` |
|
||||
| `BETTER_AUTH_URL` | `http://localhost:3300` |
|
||||
| `BETTER_AUTH_SECRET` | demo placeholder — set in prod |
|
||||
86
apps/demo/e2e/smoke.test.ts
Normal file
86
apps/demo/e2e/smoke.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Smoke test — boots the built app and asserts:
|
||||
*
|
||||
* 1. `/api/health` returns 200 JSON with `ok: true`.
|
||||
* 2. `/` redirects to `/<locale>/home`.
|
||||
* 3. Each public page (`/en/home`, `/en/about-us`,
|
||||
* `/en/contact-us`, `/en/privacy-policy`, `/en/terms-of-service`)
|
||||
* returns 200 with an HTML body containing the SPA root element.
|
||||
*
|
||||
* Run with `bun run e2e/smoke.test.ts`. Requires `bun run build`
|
||||
* to have produced `dist/client/index.html` first.
|
||||
*/
|
||||
import { spawn } from "node:child_process";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
|
||||
const PORT = 3399;
|
||||
const BASE = `http://localhost:${PORT}`;
|
||||
|
||||
async function waitForReady(): Promise<void> {
|
||||
for (let i = 0; i < 30; i++) {
|
||||
try {
|
||||
const res = await fetch(`${BASE}/api/health`);
|
||||
if (res.ok) return;
|
||||
} catch {
|
||||
// not up yet
|
||||
}
|
||||
await delay(500);
|
||||
}
|
||||
throw new Error("server failed to start within 15s");
|
||||
}
|
||||
|
||||
function assert(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) throw new Error(`SMOKE FAIL: ${message}`);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const child = spawn("bun", ["run", "src/server/index.ts"], {
|
||||
env: { ...process.env, PORT: String(PORT), NODE_ENV: "test" },
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
try {
|
||||
await waitForReady();
|
||||
|
||||
const health = await (await fetch(`${BASE}/api/health`)).json();
|
||||
// `@suaveplan/api`'s `registerHealthRoutes` emits
|
||||
// `{ status, version, state, timestamp }` — accept either the new
|
||||
// shape (`status: "healthy"`) or the legacy `{ ok: true }` shape so
|
||||
// the smoke remains forward-compatible if the route is rewritten.
|
||||
const healthy = health.status === "healthy" || health.ok === true;
|
||||
assert(healthy, `health probe not healthy: ${JSON.stringify(health)}`);
|
||||
|
||||
const root = await fetch(`${BASE}/`, { redirect: "manual" });
|
||||
// Server sends the SPA shell, client-side router handles the redirect.
|
||||
// We accept either a 200 (shell) or a 3xx redirect.
|
||||
assert(root.status === 200 || (root.status >= 300 && root.status < 400),
|
||||
`/ expected 200 or 3xx, got ${root.status}`);
|
||||
|
||||
const paths = [
|
||||
"/en/home",
|
||||
"/en/about-us",
|
||||
"/en/contact-us",
|
||||
"/en/privacy-policy",
|
||||
"/en/terms-of-service",
|
||||
];
|
||||
for (const path of paths) {
|
||||
const res = await fetch(`${BASE}${path}`);
|
||||
assert(res.ok, `${path} returned ${res.status}`);
|
||||
const body = await res.text();
|
||||
assert(body.includes('id="root"'), `${path} body missing #root`);
|
||||
}
|
||||
|
||||
// Auth endpoint reachable.
|
||||
const session = await fetch(`${BASE}/api/auth/get-session`);
|
||||
assert(session.status === 200, `/api/auth/get-session status ${session.status}`);
|
||||
|
||||
console.log("SMOKE OK");
|
||||
} finally {
|
||||
child.kill("SIGTERM");
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
12
apps/demo/index.html
Normal file
12
apps/demo/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Suaveplan Demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/client/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
37
apps/demo/package.json
Normal file
37
apps/demo/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@suaveplan/demo",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/server/index.ts",
|
||||
"build": "vite build",
|
||||
"start": "bun run src/server/index.ts",
|
||||
"smoke": "bun run e2e/smoke.test.ts",
|
||||
"typecheck": "bunx tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@suaveplan/api": "workspace:^",
|
||||
"@suaveplan/api-plugin": "workspace:^",
|
||||
"@suaveplan/api-plugin-auth": "workspace:^",
|
||||
"@suaveplan/better-auth-surrealdb": "workspace:^",
|
||||
"@suaveplan/i18n": "workspace:^",
|
||||
"@suaveplan/react-ui": "workspace:^",
|
||||
"@suaveplan/surrealdb": "workspace:^",
|
||||
"better-auth": "catalog:auth",
|
||||
"hono": "catalog:",
|
||||
"react": "catalog:react",
|
||||
"react-dom": "catalog:react",
|
||||
"react-router-dom": "catalog:react"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "catalog:build",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:react",
|
||||
"@types/react-dom": "catalog:react",
|
||||
"@vitejs/plugin-react": "catalog:build",
|
||||
"tailwindcss": "catalog:build",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:build"
|
||||
}
|
||||
}
|
||||
139
apps/demo/src/client/app.tsx
Normal file
139
apps/demo/src/client/app.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Demo app shell.
|
||||
*
|
||||
* Top-level structure:
|
||||
*
|
||||
* - `BrowserRouter`
|
||||
* - `SessionProvider` — polls `/api/auth/get-session`
|
||||
* - `ReactUiProvider` — composes
|
||||
* `<TranslationProvider>` → `<ThemeProvider>` →
|
||||
* `<PackRegistryProvider>` for the entire app tree, seeded with
|
||||
* the demo's singleton `I18nInstance` and the chassis
|
||||
* `lightTheme` plugin from `@suaveplan/react-ui`.
|
||||
* - `Routes`
|
||||
* - `/` → redirect to `/<detected>/home`
|
||||
* - `/:locale/*` → LocaleProvider + Layout
|
||||
* - everything else → NotFoundPage in default-locale context
|
||||
*/
|
||||
import { lightTheme, ReactUiProvider } from "@suaveplan/react-ui";
|
||||
import React from "react";
|
||||
import {
|
||||
BrowserRouter,
|
||||
Navigate,
|
||||
Outlet,
|
||||
Route,
|
||||
Routes,
|
||||
} from "react-router-dom";
|
||||
import { Layout } from "./components/layout.js";
|
||||
import { getDemoI18n } from "./i18n/i18n-instance.js";
|
||||
import { LocaleProvider } from "./i18n/locale-context.js";
|
||||
import { DEFAULT_LOCALE, parseLocaleSegment } from "./i18n/locales.js";
|
||||
import { RequireAuth, RequireRole } from "./lib/guards.js";
|
||||
import { SessionProvider } from "./lib/session-context.js";
|
||||
import {
|
||||
AdminControlPanelPage,
|
||||
AdminSettingsPage,
|
||||
UserControlPanelPage,
|
||||
UserSettingsPage,
|
||||
} from "./pages/account-pages.js";
|
||||
import {
|
||||
ForgotPasswordPage,
|
||||
LoginPage,
|
||||
LogoutPage,
|
||||
RegisterPage,
|
||||
} from "./pages/auth-pages.js";
|
||||
import { NotFoundPage } from "./pages/not-found-page.js";
|
||||
import {
|
||||
AboutPage,
|
||||
ContactPage,
|
||||
HomePage,
|
||||
PrivacyPage,
|
||||
TermsPage,
|
||||
} from "./pages/public-pages.js";
|
||||
|
||||
function detectLocale(): string {
|
||||
if (typeof navigator === "undefined") return DEFAULT_LOCALE;
|
||||
for (const candidate of navigator.languages ?? [navigator.language]) {
|
||||
const parsed = parseLocaleSegment(candidate);
|
||||
if (parsed !== null) return parsed.base;
|
||||
}
|
||||
return DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
function RootRedirect(): React.JSX.Element {
|
||||
return <Navigate to={`/${detectLocale()}/home`} replace />;
|
||||
}
|
||||
|
||||
function LocalisedShell(): React.JSX.Element {
|
||||
return (
|
||||
<LocaleProvider>
|
||||
<Outlet />
|
||||
</LocaleProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function App(): React.JSX.Element {
|
||||
const i18n = getDemoI18n();
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<SessionProvider>
|
||||
<ReactUiProvider
|
||||
i18n={i18n}
|
||||
themes={[lightTheme]}
|
||||
defaultTheme="light"
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<RootRedirect />} />
|
||||
<Route path="/:locale" element={<LocalisedShell />}>
|
||||
<Route element={<Layout />}>
|
||||
<Route index element={<Navigate to="home" replace />} />
|
||||
<Route path="home" element={<HomePage />} />
|
||||
<Route path="about-us" element={<AboutPage />} />
|
||||
<Route path="contact-us" element={<ContactPage />} />
|
||||
<Route path="privacy-policy" element={<PrivacyPage />} />
|
||||
<Route path="terms-of-service" element={<TermsPage />} />
|
||||
<Route path="auth/login" element={<LoginPage />} />
|
||||
<Route path="auth/register" element={<RegisterPage />} />
|
||||
<Route path="auth/forgot-password" element={<ForgotPasswordPage />} />
|
||||
<Route path="auth/logout" element={<LogoutPage />} />
|
||||
<Route
|
||||
path="user/control-panel"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<UserControlPanelPage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="user/settings"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<UserSettingsPage />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/control-panel"
|
||||
element={
|
||||
<RequireRole role="admin">
|
||||
<AdminControlPanelPage />
|
||||
</RequireRole>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/settings"
|
||||
element={
|
||||
<RequireRole role="admin">
|
||||
<AdminSettingsPage />
|
||||
</RequireRole>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
</ReactUiProvider>
|
||||
</SessionProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
89
apps/demo/src/client/components/layout.tsx
Normal file
89
apps/demo/src/client/components/layout.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Shared layout — top nav with locale picker + auth menu, content
|
||||
* area, footer. Renders an `<Outlet>` for nested routes.
|
||||
*/
|
||||
import React from "react";
|
||||
import { Link, NavLink, Outlet } from "react-router-dom";
|
||||
import { LOCALE_ENTRIES, useLocale } from "../i18n/locale-context.js";
|
||||
import type { BaseLocale } from "../i18n/locales.js";
|
||||
import { useSession } from "../lib/session-context.js";
|
||||
|
||||
function LocaleSwitcher(): React.JSX.Element {
|
||||
const { base, setLocale, availableLocales } = useLocale();
|
||||
return (
|
||||
<select
|
||||
aria-label="Language"
|
||||
value={base}
|
||||
onChange={(e) => setLocale(e.target.value as BaseLocale)}
|
||||
className="rounded-md border border-[color:var(--color-border)] bg-[color:var(--color-surface)] px-2 py-1 text-sm"
|
||||
data-testid="locale-switcher"
|
||||
>
|
||||
{availableLocales.map((loc) => (
|
||||
<option key={loc} value={loc}>
|
||||
{LOCALE_ENTRIES[loc].nativeName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
function NavItem({ to, label }: { readonly to: string; readonly label: string }): React.JSX.Element {
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
className={({ isActive }) =>
|
||||
`rounded-md px-3 py-2 text-sm font-medium transition ${
|
||||
isActive
|
||||
? "bg-[color:var(--color-accent)] text-[color:var(--color-accent-fg)]"
|
||||
: "hover:bg-[color:var(--color-surface)]"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
export function Layout(): React.JSX.Element {
|
||||
const { base, messages } = useLocale();
|
||||
const { session } = useSession();
|
||||
const p = (sub: string): string => `/${base}${sub}`;
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<header className="border-b border-[color:var(--color-border)] bg-[color:var(--color-surface)]">
|
||||
<div className="mx-auto flex max-w-6xl flex-wrap items-center gap-4 px-4 py-3">
|
||||
<Link to={p("/home")} className="text-lg font-semibold no-underline">
|
||||
{messages.siteName}
|
||||
</Link>
|
||||
<nav className="flex flex-wrap items-center gap-1">
|
||||
<NavItem to={p("/home")} label={messages.nav.home} />
|
||||
<NavItem to={p("/about-us")} label={messages.nav.about} />
|
||||
<NavItem to={p("/contact-us")} label={messages.nav.contact} />
|
||||
<NavItem to={p("/privacy-policy")} label={messages.nav.privacy} />
|
||||
<NavItem to={p("/terms-of-service")} label={messages.nav.terms} />
|
||||
</nav>
|
||||
<div className="ms-auto flex items-center gap-2">
|
||||
<LocaleSwitcher />
|
||||
{session === null ? (
|
||||
<>
|
||||
<NavItem to={p("/auth/login")} label={messages.nav.login} />
|
||||
<NavItem to={p("/auth/register")} label={messages.nav.register} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NavItem to={p("/user/control-panel")} label={messages.nav.userPanel} />
|
||||
<NavItem to={p("/auth/logout")} label={messages.nav.logout} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="mx-auto w-full max-w-6xl flex-1 px-4 py-8">
|
||||
<Outlet />
|
||||
</main>
|
||||
<footer className="border-t border-[color:var(--color-border)] py-4 text-center text-xs text-[color:var(--color-muted)]">
|
||||
{messages.siteName} • {base}
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
apps/demo/src/client/components/page-shell.tsx
Normal file
29
apps/demo/src/client/components/page-shell.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Re-usable page shell — heading + lead paragraph block consumed by
|
||||
* every public-information page so titles, headings, and the lead
|
||||
* line are kept consistent.
|
||||
*/
|
||||
import React from "react";
|
||||
import { useDocumentTitle } from "../i18n/locale-context.js";
|
||||
|
||||
export interface PageShellProps {
|
||||
readonly title: string;
|
||||
readonly heading: string;
|
||||
readonly lead: string;
|
||||
readonly children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function PageShell({ title, heading, lead, children }: PageShellProps): React.JSX.Element {
|
||||
useDocumentTitle(title);
|
||||
return (
|
||||
<article className="mx-auto max-w-3xl">
|
||||
<h1 className="text-3xl font-semibold" data-testid="page-heading">
|
||||
{heading}
|
||||
</h1>
|
||||
<p className="mt-3 text-base leading-relaxed text-[color:var(--color-muted)]" data-testid="page-lead">
|
||||
{lead}
|
||||
</p>
|
||||
{children !== undefined ? <div className="mt-6">{children}</div> : null}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
67
apps/demo/src/client/i18n/i18n-instance.ts
Normal file
67
apps/demo/src/client/i18n/i18n-instance.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Build the demo's `I18nInstance`.
|
||||
*
|
||||
* The instance is consumed by `<ReactUiProvider>` and any
|
||||
* `@suaveplan/react-ui` primitive that calls `useTranslation()` (e.g.
|
||||
* `<Button>` for icon-only-button fallback, `<Input>` for placeholder
|
||||
* resolution).
|
||||
*
|
||||
* Locale strategy: the URL carries the language subtag only (`/en/home`,
|
||||
* `/zh/home`, etc.). The chassis bundles in `@suaveplan/react-ui` are
|
||||
* keyed by full BCP47 — `en-GB`, `zh-CN`, `ar-AE`, `cy-GB`, `ru-RU`. We
|
||||
* therefore canonicalise each demo base locale to its closest BCP47
|
||||
* variant and use THAT as the i18n locale. The demo's typed `Messages`
|
||||
* catalog continues to be consumed via `useLocale()` keyed by base, so
|
||||
* the demo never depends on react-i18n's `t(key)` for its own copy.
|
||||
*
|
||||
* The chassis bundles themselves are auto-registered by
|
||||
* `<ReactUiProvider>` (its internal `ReactUiBundleLoader` effect imports
|
||||
* the matching JSON and merges it into the catalog on mount / language
|
||||
* change), so this module deliberately does NOT preload them.
|
||||
*/
|
||||
import { createI18n, type I18nInstance } from "@suaveplan/i18n/plugin";
|
||||
import type { BaseLocale } from "./locales.js";
|
||||
|
||||
export type DemoBcp47 = "en-GB" | "zh-CN" | "ar-AE" | "cy-GB" | "ru-RU";
|
||||
|
||||
/** Map URL base locale -> canonical BCP47 variant fed to the i18n core. */
|
||||
export const BASE_TO_BCP47: Readonly<Record<BaseLocale, DemoBcp47>> = {
|
||||
en: "en-GB",
|
||||
zh: "zh-CN",
|
||||
ar: "ar-AE",
|
||||
cy: "cy-GB",
|
||||
ru: "ru-RU",
|
||||
};
|
||||
|
||||
/**
|
||||
* Singleton i18n instance for the demo. Created lazily so the i18n core
|
||||
* (which is module-side-effect free) does not run during static module
|
||||
* graph evaluation in tests or SSR snapshots.
|
||||
*/
|
||||
let cached: I18nInstance | undefined;
|
||||
|
||||
export function getDemoI18n(): I18nInstance {
|
||||
if (cached !== undefined) return cached;
|
||||
// Use the default `string` generic so the cached singleton matches
|
||||
// `<ReactUiProvider>`'s `i18n: I18nInstance` (without the
|
||||
// narrowed locale generic).
|
||||
const instance: I18nInstance = createI18n({
|
||||
defaultLocale: BASE_TO_BCP47.en as string,
|
||||
fallbackLocale: BASE_TO_BCP47.en as string,
|
||||
});
|
||||
cached = instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drive the i18n instance to the BCP47 variant matching the URL base.
|
||||
* Safe to call from a `useEffect` — `changeLanguage(...)` short-circuits
|
||||
* when the locale already matches.
|
||||
*/
|
||||
export async function syncDemoLocale(base: BaseLocale): Promise<void> {
|
||||
const i18n = getDemoI18n();
|
||||
const target = BASE_TO_BCP47[base];
|
||||
if (i18n.locale !== target) {
|
||||
await i18n.changeLanguage(target);
|
||||
}
|
||||
}
|
||||
113
apps/demo/src/client/i18n/locale-context.tsx
Normal file
113
apps/demo/src/client/i18n/locale-context.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Typed locale context for the demo app.
|
||||
*
|
||||
* Holds the resolved `BaseLocale`, the canonical BCP47 tag (e.g. `en`,
|
||||
* `en-GB`, `zh-CN`), the active `Messages` catalog, and a `setLocale`
|
||||
* setter that rewrites the URL prefix and updates `<html lang>` +
|
||||
* `<html dir>` so RTL flips correctly for Arabic.
|
||||
*
|
||||
* Kept deliberately small — full @suaveplan/i18n integration (with
|
||||
* backends, formatters, plugin registry) is overkill for a five-page
|
||||
* demo. The provider shape mirrors react-i18n's `useLocale` /
|
||||
* `useI18n` so a future swap is mechanical.
|
||||
*/
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { syncDemoLocale } from "./i18n-instance.js";
|
||||
import {
|
||||
BASE_LOCALES,
|
||||
type BaseLocale,
|
||||
DEFAULT_LOCALE,
|
||||
LOCALE_ENTRIES,
|
||||
parseLocaleSegment,
|
||||
RTL_BASE_LOCALES,
|
||||
} from "./locales.js";
|
||||
import { CATALOGS, type Messages } from "./messages.js";
|
||||
|
||||
export interface LocaleContextValue {
|
||||
readonly base: BaseLocale;
|
||||
readonly canonical: string;
|
||||
readonly direction: "ltr" | "rtl";
|
||||
readonly messages: Messages;
|
||||
readonly setLocale: (next: BaseLocale | string) => void;
|
||||
readonly availableLocales: readonly BaseLocale[];
|
||||
}
|
||||
|
||||
const LocaleContext = createContext<LocaleContextValue | null>(null);
|
||||
|
||||
export function useLocale(): LocaleContextValue {
|
||||
const ctx = useContext(LocaleContext);
|
||||
if (ctx === null) {
|
||||
throw new Error(
|
||||
"useLocale must be used inside <LocaleProvider> — wrap your routes."
|
||||
);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function useMessages(): Messages {
|
||||
return useLocale().messages;
|
||||
}
|
||||
|
||||
export interface LocaleProviderProps {
|
||||
readonly children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function LocaleProvider({ children }: LocaleProviderProps): React.JSX.Element {
|
||||
const params = useParams<{ locale?: string }>();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const parsed = parseLocaleSegment(params.locale);
|
||||
const base: BaseLocale = parsed?.base ?? DEFAULT_LOCALE;
|
||||
const canonical = parsed?.canonical ?? base;
|
||||
const direction = RTL_BASE_LOCALES.has(base) ? "rtl" : "ltr";
|
||||
const messages = CATALOGS[base];
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = canonical;
|
||||
document.documentElement.dir = direction;
|
||||
document.title = messages.home.title; // updated per page via useDocumentTitle
|
||||
// Drive the singleton I18nInstance to the BCP47 variant matching the
|
||||
// current URL base so any @suaveplan/react-ui chassis lookup sees a
|
||||
// catalog populated for the active locale.
|
||||
void syncDemoLocale(base);
|
||||
}, [base, canonical, direction, messages]);
|
||||
|
||||
const setLocale = useCallback(
|
||||
(next: BaseLocale | string) => {
|
||||
// Strip the existing locale segment then prepend the new one.
|
||||
const path = location.pathname.replace(/^\/[^/]+/, "");
|
||||
navigate(`/${next}${path === "" ? "/home" : path}${location.search}${location.hash}`);
|
||||
},
|
||||
[location.hash, location.pathname, location.search, navigate]
|
||||
);
|
||||
|
||||
const value = useMemo<LocaleContextValue>(
|
||||
() => ({
|
||||
base,
|
||||
canonical,
|
||||
direction,
|
||||
messages,
|
||||
setLocale,
|
||||
availableLocales: BASE_LOCALES,
|
||||
}),
|
||||
[base, canonical, direction, messages, setLocale]
|
||||
);
|
||||
|
||||
return <LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>;
|
||||
}
|
||||
|
||||
export function useDocumentTitle(title: string): void {
|
||||
useEffect(() => {
|
||||
document.title = title;
|
||||
}, [title]);
|
||||
}
|
||||
|
||||
export { LOCALE_ENTRIES };
|
||||
49
apps/demo/src/client/i18n/locales.ts
Normal file
49
apps/demo/src/client/i18n/locales.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Locale registry for the demo app.
|
||||
*
|
||||
* Five canonical base locales — `en`, `zh`, `ar`, `cy`, `ru` — plus a
|
||||
* permissive `<base>-<region>` resolver so URLs like `/en-GB/home` or
|
||||
* `/zh-CN/home` collapse onto the base catalog. RTL is tracked on the
|
||||
* base code.
|
||||
*/
|
||||
export const BASE_LOCALES = ["en", "zh", "ar", "cy", "ru"] as const;
|
||||
export type BaseLocale = (typeof BASE_LOCALES)[number];
|
||||
|
||||
export const DEFAULT_LOCALE: BaseLocale = "en";
|
||||
|
||||
export const RTL_BASE_LOCALES: ReadonlySet<BaseLocale> = new Set(["ar"]);
|
||||
|
||||
export interface LocaleEntry {
|
||||
readonly base: BaseLocale;
|
||||
readonly name: string;
|
||||
readonly nativeName: string;
|
||||
readonly direction: "ltr" | "rtl";
|
||||
}
|
||||
|
||||
export const LOCALE_ENTRIES: Readonly<Record<BaseLocale, LocaleEntry>> = {
|
||||
en: { base: "en", name: "English", nativeName: "English", direction: "ltr" },
|
||||
zh: { base: "zh", name: "Chinese", nativeName: "中文", direction: "ltr" },
|
||||
ar: { base: "ar", name: "Arabic", nativeName: "العربية", direction: "rtl" },
|
||||
cy: { base: "cy", name: "Welsh", nativeName: "Cymraeg", direction: "ltr" },
|
||||
ru: { base: "ru", name: "Russian", nativeName: "Русский", direction: "ltr" },
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalise a raw URL segment into a canonical `<base>` or
|
||||
* `<base>-<REGION>` BCP47 tag, or `null` if the base does not match
|
||||
* one of {@link BASE_LOCALES}.
|
||||
*/
|
||||
export function parseLocaleSegment(segment: string | undefined): {
|
||||
readonly base: BaseLocale;
|
||||
readonly canonical: string;
|
||||
} | null {
|
||||
if (!segment) return null;
|
||||
const [rawBase, rawRegion] = segment.toLowerCase().split("-");
|
||||
const base = (BASE_LOCALES as readonly string[]).includes(rawBase ?? "")
|
||||
? (rawBase as BaseLocale)
|
||||
: null;
|
||||
if (!base) return null;
|
||||
if (!rawRegion) return { base, canonical: base };
|
||||
const region = rawRegion.toUpperCase();
|
||||
return { base, canonical: `${base}-${region}` };
|
||||
}
|
||||
337
apps/demo/src/client/i18n/messages.ts
Normal file
337
apps/demo/src/client/i18n/messages.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* Static message catalogs for the demo app.
|
||||
*
|
||||
* `en` is fully translated. The other four locales (`zh`, `ar`, `cy`,
|
||||
* `ru`) ship with canonical translations for page titles, headings,
|
||||
* and the lead paragraph, and fall back to the English copy for
|
||||
* secondary microcopy. Every key has a non-empty value in every
|
||||
* locale so render-time fallback never silently picks up `en`.
|
||||
*/
|
||||
import type { BaseLocale } from "./locales.js";
|
||||
|
||||
export interface Messages {
|
||||
// Site chrome
|
||||
readonly siteName: string;
|
||||
readonly nav: {
|
||||
readonly home: string;
|
||||
readonly about: string;
|
||||
readonly contact: string;
|
||||
readonly privacy: string;
|
||||
readonly terms: string;
|
||||
readonly login: string;
|
||||
readonly register: string;
|
||||
readonly logout: string;
|
||||
readonly userPanel: string;
|
||||
readonly userSettings: string;
|
||||
readonly adminPanel: string;
|
||||
readonly adminSettings: string;
|
||||
};
|
||||
// Pages
|
||||
readonly home: { readonly title: string; readonly heading: string; readonly lead: string };
|
||||
readonly about: { readonly title: string; readonly heading: string; readonly lead: string };
|
||||
readonly contact: { readonly title: string; readonly heading: string; readonly lead: string };
|
||||
readonly privacy: { readonly title: string; readonly heading: string; readonly lead: string };
|
||||
readonly terms: { readonly title: string; readonly heading: string; readonly lead: string };
|
||||
readonly login: {
|
||||
readonly title: string;
|
||||
readonly heading: string;
|
||||
readonly lead: string;
|
||||
readonly emailLabel: string;
|
||||
readonly passwordLabel: string;
|
||||
readonly submit: string;
|
||||
readonly linkToRegister: string;
|
||||
readonly linkToForgot: string;
|
||||
};
|
||||
readonly register: {
|
||||
readonly title: string;
|
||||
readonly heading: string;
|
||||
readonly lead: string;
|
||||
readonly emailLabel: string;
|
||||
readonly passwordLabel: string;
|
||||
readonly confirmLabel: string;
|
||||
readonly submit: string;
|
||||
readonly linkToLogin: string;
|
||||
};
|
||||
readonly forgot: {
|
||||
readonly title: string;
|
||||
readonly heading: string;
|
||||
readonly lead: string;
|
||||
readonly emailLabel: string;
|
||||
readonly submit: string;
|
||||
readonly sentNotice: string;
|
||||
readonly linkToLogin: string;
|
||||
};
|
||||
readonly logout: {
|
||||
readonly title: string;
|
||||
readonly heading: string;
|
||||
readonly lead: string;
|
||||
};
|
||||
readonly userPanel: { readonly title: string; readonly heading: string; readonly lead: string };
|
||||
readonly userSettings: { readonly title: string; readonly heading: string; readonly lead: string };
|
||||
readonly adminPanel: { readonly title: string; readonly heading: string; readonly lead: string };
|
||||
readonly adminSettings: { readonly title: string; readonly heading: string; readonly lead: string };
|
||||
readonly notFound: { readonly title: string; readonly heading: string; readonly lead: string };
|
||||
readonly unauthorized: { readonly title: string; readonly heading: string; readonly lead: string };
|
||||
}
|
||||
|
||||
const en: Messages = {
|
||||
siteName: "Suaveplan Demo",
|
||||
nav: {
|
||||
home: "Home",
|
||||
about: "About us",
|
||||
contact: "Contact us",
|
||||
privacy: "Privacy policy",
|
||||
terms: "Terms of service",
|
||||
login: "Log in",
|
||||
register: "Register",
|
||||
logout: "Log out",
|
||||
userPanel: "User panel",
|
||||
userSettings: "User settings",
|
||||
adminPanel: "Admin panel",
|
||||
adminSettings: "Admin settings",
|
||||
},
|
||||
home: {
|
||||
title: "Suaveplan Demo — Home",
|
||||
heading: "Welcome to the Suaveplan Demo",
|
||||
lead: "A unified Bun + Hono server with React-rendered pages and a JSON API in one process, wired up with @suaveplan/react-ui and @suaveplan/react-i18n.",
|
||||
},
|
||||
about: {
|
||||
title: "About us",
|
||||
heading: "About Suaveplan",
|
||||
lead: "Suaveplan is a bottom-up rebuild of an enterprise platform, packaged as a Turborepo monorepo of focused libraries.",
|
||||
},
|
||||
contact: {
|
||||
title: "Contact us",
|
||||
heading: "Get in touch",
|
||||
lead: "Drop us a line through any of the channels listed below. We read every message.",
|
||||
},
|
||||
privacy: {
|
||||
title: "Privacy policy",
|
||||
heading: "Privacy policy",
|
||||
lead: "This demo collects no personal data beyond what the better-auth memory adapter holds for the lifetime of the process.",
|
||||
},
|
||||
terms: {
|
||||
title: "Terms of service",
|
||||
heading: "Terms of service",
|
||||
lead: "This is a demo application provided as-is for illustrative purposes; no warranty is offered or implied.",
|
||||
},
|
||||
login: {
|
||||
title: "Log in",
|
||||
heading: "Log in to your account",
|
||||
lead: "Sign in with your email address and password.",
|
||||
emailLabel: "Email",
|
||||
passwordLabel: "Password",
|
||||
submit: "Log in",
|
||||
linkToRegister: "Need an account? Register",
|
||||
linkToForgot: "Forgot your password?",
|
||||
},
|
||||
register: {
|
||||
title: "Create an account",
|
||||
heading: "Create your account",
|
||||
lead: "Sign up with your email address and a password of at least 6 characters.",
|
||||
emailLabel: "Email",
|
||||
passwordLabel: "Password",
|
||||
confirmLabel: "Confirm password",
|
||||
submit: "Create account",
|
||||
linkToLogin: "Already have an account? Log in",
|
||||
},
|
||||
forgot: {
|
||||
title: "Forgot password",
|
||||
heading: "Reset your password",
|
||||
lead: "Enter your email and we'll send you a password-reset link.",
|
||||
emailLabel: "Email",
|
||||
submit: "Send reset link",
|
||||
sentNotice: "If an account exists for that email, a reset link is on its way.",
|
||||
linkToLogin: "Back to log in",
|
||||
},
|
||||
logout: {
|
||||
title: "Logging out",
|
||||
heading: "Logging you out…",
|
||||
lead: "Clearing your session — you'll be redirected home in a moment.",
|
||||
},
|
||||
userPanel: {
|
||||
title: "User control panel",
|
||||
heading: "Control panel",
|
||||
lead: "An overview of your account, sessions, and recent activity.",
|
||||
},
|
||||
userSettings: {
|
||||
title: "User settings",
|
||||
heading: "Settings",
|
||||
lead: "Manage your profile, locale preference, and security options.",
|
||||
},
|
||||
adminPanel: {
|
||||
title: "Admin control panel",
|
||||
heading: "Admin control panel",
|
||||
lead: "Administrative dashboard — restricted to users with the admin role.",
|
||||
},
|
||||
adminSettings: {
|
||||
title: "Admin settings",
|
||||
heading: "Admin settings",
|
||||
lead: "Configure global application settings — restricted to admins.",
|
||||
},
|
||||
notFound: {
|
||||
title: "Not found",
|
||||
heading: "404 — page not found",
|
||||
lead: "The page you asked for does not exist. Try one of the languages below or return home.",
|
||||
},
|
||||
unauthorized: {
|
||||
title: "Sign in required",
|
||||
heading: "Sign in required",
|
||||
lead: "You need to be signed in to view this page.",
|
||||
},
|
||||
};
|
||||
|
||||
// Lightweight translations: page-title, heading, lead per locale.
|
||||
// Secondary microcopy falls through to English so the demo always
|
||||
// renders intelligibly while making the per-locale shape obvious.
|
||||
const zh: Messages = {
|
||||
...en,
|
||||
nav: {
|
||||
...en.nav,
|
||||
home: "首页",
|
||||
about: "关于我们",
|
||||
contact: "联系我们",
|
||||
privacy: "隐私政策",
|
||||
terms: "服务条款",
|
||||
login: "登录",
|
||||
register: "注册",
|
||||
logout: "退出",
|
||||
userPanel: "用户面板",
|
||||
userSettings: "用户设置",
|
||||
adminPanel: "管理面板",
|
||||
adminSettings: "管理设置",
|
||||
},
|
||||
siteName: "Suaveplan 演示",
|
||||
home: {
|
||||
title: "Suaveplan 演示 — 首页",
|
||||
heading: "欢迎来到 Suaveplan 演示",
|
||||
lead: "在单一进程中提供统一的 Bun + Hono 服务器、React 渲染页面与 JSON API,使用 @suaveplan/react-ui 和 @suaveplan/react-i18n。",
|
||||
},
|
||||
about: { title: "关于我们", heading: "关于 Suaveplan", lead: en.about.lead },
|
||||
contact: { title: "联系我们", heading: "联系我们", lead: en.contact.lead },
|
||||
privacy: { title: "隐私政策", heading: "隐私政策", lead: en.privacy.lead },
|
||||
terms: { title: "服务条款", heading: "服务条款", lead: en.terms.lead },
|
||||
login: { ...en.login, title: "登录", heading: "登录您的帐户", lead: "使用电子邮件地址和密码登录。" },
|
||||
register: { ...en.register, title: "创建帐户", heading: "创建您的帐户", lead: "使用电子邮件和至少 6 个字符的密码注册。" },
|
||||
forgot: { ...en.forgot, title: "忘记密码", heading: "重置密码", lead: "输入您的电子邮件,我们将发送重置链接。" },
|
||||
logout: { title: "退出登录", heading: "正在退出登录...", lead: en.logout.lead },
|
||||
userPanel: { title: "用户控制面板", heading: "控制面板", lead: en.userPanel.lead },
|
||||
userSettings: { title: "用户设置", heading: "设置", lead: en.userSettings.lead },
|
||||
adminPanel: { title: "管理控制面板", heading: "管理控制面板", lead: en.adminPanel.lead },
|
||||
adminSettings: { title: "管理设置", heading: "管理设置", lead: en.adminSettings.lead },
|
||||
notFound: { title: "未找到", heading: "404 — 找不到页面", lead: en.notFound.lead },
|
||||
unauthorized: { title: "需要登录", heading: "需要登录", lead: en.unauthorized.lead },
|
||||
};
|
||||
|
||||
const ar: Messages = {
|
||||
...en,
|
||||
nav: {
|
||||
...en.nav,
|
||||
home: "الرئيسية",
|
||||
about: "من نحن",
|
||||
contact: "اتصل بنا",
|
||||
privacy: "سياسة الخصوصية",
|
||||
terms: "شروط الخدمة",
|
||||
login: "تسجيل الدخول",
|
||||
register: "تسجيل",
|
||||
logout: "تسجيل الخروج",
|
||||
userPanel: "لوحة المستخدم",
|
||||
userSettings: "إعدادات المستخدم",
|
||||
adminPanel: "لوحة الإدارة",
|
||||
adminSettings: "إعدادات الإدارة",
|
||||
},
|
||||
siteName: "عرض Suaveplan التوضيحي",
|
||||
home: { title: "Suaveplan — الرئيسية", heading: "مرحبًا بك في عرض Suaveplan التوضيحي", lead: en.home.lead },
|
||||
about: { title: "من نحن", heading: "حول Suaveplan", lead: en.about.lead },
|
||||
contact: { title: "اتصل بنا", heading: "تواصل معنا", lead: en.contact.lead },
|
||||
privacy: { title: "سياسة الخصوصية", heading: "سياسة الخصوصية", lead: en.privacy.lead },
|
||||
terms: { title: "شروط الخدمة", heading: "شروط الخدمة", lead: en.terms.lead },
|
||||
login: { ...en.login, title: "تسجيل الدخول", heading: "تسجيل الدخول إلى حسابك", lead: "سجّل الدخول باستخدام بريدك الإلكتروني وكلمة المرور." },
|
||||
register: { ...en.register, title: "إنشاء حساب", heading: "إنشاء حسابك", lead: "سجّل ببريدك الإلكتروني وكلمة مرور لا تقل عن 6 أحرف." },
|
||||
forgot: { ...en.forgot, title: "نسيت كلمة المرور", heading: "إعادة تعيين كلمة المرور", lead: en.forgot.lead },
|
||||
logout: { title: "جاري تسجيل الخروج", heading: "جاري تسجيل خروجك…", lead: en.logout.lead },
|
||||
userPanel: { title: "لوحة تحكم المستخدم", heading: "لوحة التحكم", lead: en.userPanel.lead },
|
||||
userSettings: { title: "إعدادات المستخدم", heading: "الإعدادات", lead: en.userSettings.lead },
|
||||
adminPanel: { title: "لوحة تحكم الإدارة", heading: "لوحة تحكم الإدارة", lead: en.adminPanel.lead },
|
||||
adminSettings: { title: "إعدادات الإدارة", heading: "إعدادات الإدارة", lead: en.adminSettings.lead },
|
||||
notFound: { title: "غير موجود", heading: "404 — الصفحة غير موجودة", lead: en.notFound.lead },
|
||||
unauthorized: { title: "تسجيل الدخول مطلوب", heading: "تسجيل الدخول مطلوب", lead: en.unauthorized.lead },
|
||||
};
|
||||
|
||||
const cy: Messages = {
|
||||
...en,
|
||||
nav: {
|
||||
...en.nav,
|
||||
home: "Hafan",
|
||||
about: "Amdanom ni",
|
||||
contact: "Cysylltwch â ni",
|
||||
privacy: "Polisi preifatrwydd",
|
||||
terms: "Telerau gwasanaeth",
|
||||
login: "Mewngofnodi",
|
||||
register: "Cofrestru",
|
||||
logout: "Allgofnodi",
|
||||
userPanel: "Panel defnyddiwr",
|
||||
userSettings: "Gosodiadau defnyddiwr",
|
||||
adminPanel: "Panel gweinyddol",
|
||||
adminSettings: "Gosodiadau gweinyddol",
|
||||
},
|
||||
siteName: "Demo Suaveplan",
|
||||
home: { title: "Suaveplan — Hafan", heading: "Croeso i Demo Suaveplan", lead: en.home.lead },
|
||||
about: { title: "Amdanom ni", heading: "Am Suaveplan", lead: en.about.lead },
|
||||
contact: { title: "Cysylltwch â ni", heading: "Cysylltu â ni", lead: en.contact.lead },
|
||||
privacy: { title: "Polisi preifatrwydd", heading: "Polisi preifatrwydd", lead: en.privacy.lead },
|
||||
terms: { title: "Telerau gwasanaeth", heading: "Telerau gwasanaeth", lead: en.terms.lead },
|
||||
login: { ...en.login, title: "Mewngofnodi", heading: "Mewngofnodi i'ch cyfrif", lead: "Mewngofnodi gyda'ch e-bost a'ch cyfrinair." },
|
||||
register: { ...en.register, title: "Creu cyfrif", heading: "Creu eich cyfrif", lead: "Cofrestrwch gyda'ch e-bost a chyfrinair o leiaf 6 nod." },
|
||||
forgot: { ...en.forgot, title: "Anghofio cyfrinair", heading: "Ailosod eich cyfrinair", lead: en.forgot.lead },
|
||||
logout: { title: "Allgofnodi", heading: "Allgofnodi…", lead: en.logout.lead },
|
||||
userPanel: { title: "Panel rheoli defnyddiwr", heading: "Panel rheoli", lead: en.userPanel.lead },
|
||||
userSettings: { title: "Gosodiadau defnyddiwr", heading: "Gosodiadau", lead: en.userSettings.lead },
|
||||
adminPanel: { title: "Panel rheoli gweinyddol", heading: "Panel rheoli gweinyddol", lead: en.adminPanel.lead },
|
||||
adminSettings: { title: "Gosodiadau gweinyddol", heading: "Gosodiadau gweinyddol", lead: en.adminSettings.lead },
|
||||
notFound: { title: "Heb ddod o hyd", heading: "404 — tudalen heb ei ganfod", lead: en.notFound.lead },
|
||||
unauthorized: { title: "Mewngofnodi sy'n ofynnol", heading: "Mae angen mewngofnodi", lead: en.unauthorized.lead },
|
||||
};
|
||||
|
||||
const ru: Messages = {
|
||||
...en,
|
||||
nav: {
|
||||
...en.nav,
|
||||
home: "Главная",
|
||||
about: "О нас",
|
||||
contact: "Контакты",
|
||||
privacy: "Политика конфиденциальности",
|
||||
terms: "Условия обслуживания",
|
||||
login: "Войти",
|
||||
register: "Регистрация",
|
||||
logout: "Выйти",
|
||||
userPanel: "Панель пользователя",
|
||||
userSettings: "Настройки пользователя",
|
||||
adminPanel: "Панель администратора",
|
||||
adminSettings: "Настройки администратора",
|
||||
},
|
||||
siteName: "Демо Suaveplan",
|
||||
home: { title: "Suaveplan — Главная", heading: "Добро пожаловать в демо Suaveplan", lead: en.home.lead },
|
||||
about: { title: "О нас", heading: "О Suaveplan", lead: en.about.lead },
|
||||
contact: { title: "Контакты", heading: "Свяжитесь с нами", lead: en.contact.lead },
|
||||
privacy: { title: "Политика конфиденциальности", heading: "Политика конфиденциальности", lead: en.privacy.lead },
|
||||
terms: { title: "Условия обслуживания", heading: "Условия обслуживания", lead: en.terms.lead },
|
||||
login: { ...en.login, title: "Войти", heading: "Войти в учетную запись", lead: "Войдите, используя email и пароль." },
|
||||
register: { ...en.register, title: "Создать учетную запись", heading: "Создайте учетную запись", lead: "Зарегистрируйтесь с email и паролем не менее 6 символов." },
|
||||
forgot: { ...en.forgot, title: "Забыли пароль", heading: "Сбросить пароль", lead: en.forgot.lead },
|
||||
logout: { title: "Выход", heading: "Выход из системы…", lead: en.logout.lead },
|
||||
userPanel: { title: "Панель управления пользователя", heading: "Панель управления", lead: en.userPanel.lead },
|
||||
userSettings: { title: "Настройки пользователя", heading: "Настройки", lead: en.userSettings.lead },
|
||||
adminPanel: { title: "Панель управления администратора", heading: "Панель управления администратора", lead: en.adminPanel.lead },
|
||||
adminSettings: { title: "Настройки администратора", heading: "Настройки администратора", lead: en.adminSettings.lead },
|
||||
notFound: { title: "Не найдено", heading: "404 — страница не найдена", lead: en.notFound.lead },
|
||||
unauthorized: { title: "Требуется вход", heading: "Требуется вход", lead: en.unauthorized.lead },
|
||||
};
|
||||
|
||||
export const CATALOGS: Readonly<Record<BaseLocale, Messages>> = {
|
||||
en,
|
||||
zh,
|
||||
ar,
|
||||
cy,
|
||||
ru,
|
||||
};
|
||||
87
apps/demo/src/client/lib/auth-client.ts
Normal file
87
apps/demo/src/client/lib/auth-client.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Tiny browser auth client that talks directly to the better-auth
|
||||
* HTTP endpoints mounted by the server at `/api/auth/*`. Keeps the
|
||||
* demo client free of `better-auth/client` so the React build stays
|
||||
* lean.
|
||||
*/
|
||||
export interface SessionUser {
|
||||
readonly id: string;
|
||||
readonly email: string;
|
||||
readonly name?: string | null;
|
||||
readonly role?: string | null;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
readonly user: SessionUser;
|
||||
readonly expiresAt?: string;
|
||||
}
|
||||
|
||||
export async function fetchSession(): Promise<Session | null> {
|
||||
const res = await fetch("/api/auth/get-session", {
|
||||
credentials: "include",
|
||||
headers: { accept: "application/json" },
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const body = (await res.json()) as { user?: SessionUser; session?: { expiresAt?: string } } | null;
|
||||
if (body === null || body.user === undefined) return null;
|
||||
return {
|
||||
user: body.user,
|
||||
...(body.session?.expiresAt !== undefined ? { expiresAt: body.session.expiresAt } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function signInEmail(args: {
|
||||
readonly email: string;
|
||||
readonly password: string;
|
||||
}): Promise<{ ok: true } | { ok: false; error: string }> {
|
||||
const res = await fetch("/api/auth/sign-in/email", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(args),
|
||||
});
|
||||
if (res.ok) return { ok: true };
|
||||
const body = (await res.json().catch(() => ({}))) as { message?: string };
|
||||
return { ok: false, error: body.message ?? `Sign-in failed (${res.status})` };
|
||||
}
|
||||
|
||||
export async function signUpEmail(args: {
|
||||
readonly email: string;
|
||||
readonly password: string;
|
||||
readonly name?: string;
|
||||
}): Promise<{ ok: true } | { ok: false; error: string }> {
|
||||
const res = await fetch("/api/auth/sign-up/email", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ...args, name: args.name ?? args.email.split("@")[0] }),
|
||||
});
|
||||
if (res.ok) return { ok: true };
|
||||
const body = (await res.json().catch(() => ({}))) as { message?: string };
|
||||
return { ok: false, error: body.message ?? `Sign-up failed (${res.status})` };
|
||||
}
|
||||
|
||||
export async function signOut(): Promise<void> {
|
||||
await fetch("/api/auth/sign-out", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: "{}",
|
||||
}).catch(() => undefined);
|
||||
}
|
||||
|
||||
export async function forgetPassword(args: {
|
||||
readonly email: string;
|
||||
}): Promise<{ ok: true } | { ok: false; error: string }> {
|
||||
const res = await fetch("/api/auth/forget-password", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ...args, redirectTo: "/auth/login" }),
|
||||
});
|
||||
if (res.ok) return { ok: true };
|
||||
// better-auth returns 200 even for unknown emails to avoid enumeration;
|
||||
// we still surface non-2xx for transport-level errors.
|
||||
const body = (await res.json().catch(() => ({}))) as { message?: string };
|
||||
return { ok: false, error: body.message ?? `Request failed (${res.status})` };
|
||||
}
|
||||
57
apps/demo/src/client/lib/guards.tsx
Normal file
57
apps/demo/src/client/lib/guards.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Route guards — wrap a child element with `<RequireAuth>` or
|
||||
* `<RequireRole role="admin">`. Both rely on `useSession()` so they
|
||||
* react to live sign-in / sign-out without a hard reload.
|
||||
*/
|
||||
import React from "react";
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
import { useLocale } from "../i18n/locale-context.js";
|
||||
import { useSession } from "./session-context.js";
|
||||
|
||||
export function RequireAuth({ children }: { readonly children: React.ReactNode }): React.JSX.Element {
|
||||
const { session, loading } = useSession();
|
||||
const { base } = useLocale();
|
||||
const location = useLocation();
|
||||
if (loading) {
|
||||
return <div className="p-8 text-sm text-muted">Loading session…</div>;
|
||||
}
|
||||
if (session === null) {
|
||||
return (
|
||||
<Navigate
|
||||
to={`/${base}/auth/login?return=${encodeURIComponent(location.pathname)}`}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export function RequireRole({
|
||||
role,
|
||||
children,
|
||||
}: {
|
||||
readonly role: string;
|
||||
readonly children: React.ReactNode;
|
||||
}): React.JSX.Element {
|
||||
const { session, loading } = useSession();
|
||||
const { base, messages } = useLocale();
|
||||
if (loading) {
|
||||
return <div className="p-8 text-sm text-muted">Loading session…</div>;
|
||||
}
|
||||
if (session === null) {
|
||||
return <Navigate to={`/${base}/auth/login`} replace />;
|
||||
}
|
||||
if (session.user.role !== role) {
|
||||
return (
|
||||
<main className="mx-auto max-w-2xl p-8">
|
||||
<h1 className="text-2xl font-semibold" data-testid="page-heading">
|
||||
{messages.unauthorized.heading}
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-muted">
|
||||
Required role: <code>{role}</code>
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
53
apps/demo/src/client/lib/session-context.tsx
Normal file
53
apps/demo/src/client/lib/session-context.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Session context — polls `/api/auth/get-session` on mount and exposes
|
||||
* `useSession()` and a `refresh()` to forms.
|
||||
*/
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { fetchSession, type Session } from "./auth-client.js";
|
||||
|
||||
export interface SessionContextValue {
|
||||
readonly session: Session | null;
|
||||
readonly loading: boolean;
|
||||
readonly refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
const SessionContext = createContext<SessionContextValue | null>(null);
|
||||
|
||||
export function useSession(): SessionContextValue {
|
||||
const ctx = useContext(SessionContext);
|
||||
if (ctx === null) throw new Error("useSession must be inside <SessionProvider>");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function SessionProvider({ children }: { children: React.ReactNode }): React.JSX.Element {
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const next = await fetchSession();
|
||||
setSession(next);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const value = useMemo<SessionContextValue>(
|
||||
() => ({ session, loading, refresh }),
|
||||
[session, loading, refresh]
|
||||
);
|
||||
|
||||
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user