Skip to main content
RELEASE / NOTES

Changelog

Every release of KyberStation, oldest version to most recent. Mirrors the CHANGELOG.md on GitHub.

v0.23.1

2026-05-17

P0 patch. Fixes a critical white-out regression in the blade visualizer that landed silently with v0.23.0's default render-mode flip.

Fixed

  • CRITICAL: Blade canvas rendered pure white regardless of preset color (#357) — every preset load and every "Surprise Me" generation produced a max-white blade in the editor after v0.23.0 shipped. Root cause: InOutTrLTemplate.getColor() in @kyberstation/template-eval returned {255, 255, 255} (WHITE) for the stable-on branch, intending to encode "alpha = 255 = blade fully visible." The Layers compositor uses each upper layer's max-channel as an alpha mask and alpha-blends downward, so alphaBlend(base, WHITE, 255) reduced every LED to pure white as soon as ignition latched. The bug was dormant while parameter-engine was the default render mode; PR #352's flip to template-eval as default in v0.23.0 exposed it on every preset every frame. Fix: make InOutTrL a no-op in the per-frame render (return BLACK, alpha 0) — the visible ignition / retraction wipe is owned by BladeCanvas via engine.extendProgress, and BladeEngine.update() never invokes template-eval when the blade is OFF, so the layer is structurally redundant at the per-LED level. Internal isIgniting / isRetracting / wasOn state tracking stays intact; getInteger() returns the proper 0..PROFFIE_MAX mask scalar for any debug / UI consumer that wants the transition progress. 4 new regression tests pin the full codegen → engine → LED-buffer pipeline (configured base color must dominate, per-frame variance proves animation) plus a Layers<Rgb<base>, InOutTrL<>> composite test that catches the exact failure mode.

v0.23.0

2026-05-16

Visualizer Upgrade Release. Closes the entire docs/VISUALIZER_UPGRADE_PLAN.md Phases 2C, 2D, and 3 in a single sprint. The headline change is the default render mode flips from the hand-written parameter engine to the template-eval interpreter — what users see in the visualizer now is, by construction, the exact LED output their saber will produce when running KyberStation's codegen-emitted ProffieOS template. 5 PRs landed across two sub-areas: 3D scene polish (mouse interaction + post-processing) and the render pipeline architectural shift.

3D scene completion

  • Phase 2C — 3D mouse interaction (#348) — All 4 gestures wired on the 3D blade renderer shipped in PR #301. Click on blade → engine.triggerEffect('clash', { position: ledIndex / ledCount }). Pointer-down + hold 400ms → engine.triggerEffect('lockup') (releases on pointer-up). Drag tip→hilt with sustained 0.25 UV-Y delta → engine.retract() (with 0.05 tip-bounce reset so zig-zags don't accidentally retract). OrbitControls rotateSpeed: 0.7, swing-velocity feed verified through the existing useMouseSwing hook. +28 tests.
  • Phase 2D — 3D post-processing (#349) — 3 post-processing passes added under apps/web/components/editor/blade3d/postprocessing/:
  • UnrealBloom — HDR halo via @react-three/postprocessing Bloom, polycarbonate-tuned (intensity 1.8, threshold 0.1, 6 mip levels)
  • PolycarbonateDiffusion — custom postprocessing.Effect with luminance-thresholded 9-tap Gaussian blur (diffuses bright LED pixels only; hilt + non-emissive geometry stays sharp)
  • BladeMotionBlur — custom postprocessing.Effect with directional 7-tap streak driven by engine.motion.swingSpeed + engine.motion.bladeAngle each frame
  • Perf gating: centralized in resolvePostProcessingConfig. Low quality → composer unmounted; medium / mobile (&lt; 600px) → motion blur off + diffusion halved; prefers-reduced-motion → motion blur off + bloom 40%; reduceBloom user pref → bloom 40%
  • Legacy BladeBloom.tsx preserved for backward compat (deprecate in a follow-up after soak). +40 tests.

Render pipeline architectural shift

  • Phase 3 Step 1 — perf benchmark + gallery coverage check (#351) — Built packages/engine/perf/templateEvalBench.mjs (pnpm bench:template-eval). Initial results: template-eval runs at ~78,000 fps (Apple Silicon Node v24.11.1), ~400× headroom over the 60fps target. But 0 of 455 gallery presets parsed cleanly — three missing template-name registrations (SaberBase::LOCKUP_NORMAL, FireConfig, SaberBase::LOCKUP_DRAG) blocked the entire library. Findings at docs/research/TEMPLATE_EVAL_PERF_BENCHMARK_2026-05-16.md.
  • Phase 3 Step 2 — register missing names + flip default (#352) — Closes the 0/455 gap and ships the architectural shift:
  • 36 new template entries registered in packages/template-eval/src/registry.ts: 14 lockup-type tags (7 SaberBase::LOCKUP_* + 7 unqualified aliases), 21 effect-type tags (EFFECT_CLASH, EFFECT_PREON, EFFECT_STAB, etc.), and 1 FireConfig structured leaf
  • LockupTrLTemplate constructor extended to accept optional 5th arg (lockup-type tag) — filters activation by matching effects.lockupType. 4-arg shape preserved.
  • StyleFireTemplate constructor extended to accept optional 5th arg (FireConfig) — scales the heat-map simulation by the supplied Cooling/Heating/IntensityBase triple. 4-arg shape preserved.
  • BladeEngine.renderMode default flipped to 'template-eval' (was 'proffie' = parameter engine). Parameter-engine fall-through preserved as safety net when no template code is supplied — never silently breaks an in-flight design.
  • Gallery coverage: 0/455 → 455/455 parse cleanly through codegen → template-eval → renderFrame
  • Bench results post-fix: worst-case p95 = 0.062 ms (~260× under the 16.67ms budget) on the slowest real preset (Obi-Wan Padawan stable with full LockupTrL × 4 + InOutTrL wrap). 40/40 measured templates pass 60fps p95.
  • +50 tests (template-eval 180 → 230)

Renderer foundation work

  • Inline render path extraction (#350) — Four regions of inline render logic extracted from BladeCanvas.tsx into testable modules under apps/web/lib/blade/:
  • ignitionFlash.ts (ignition radial burst, was BladeCanvas L1320-1331)
  • motionGhost.ts (motion-trail ghosting, was L1210-1273)
  • topologyOverlay.ts (crossguard + triple-fan side blades, was L1561-1686)
  • ambientLumaCoupling.ts (mip-2 luma → halo brightness, was L1346-1383)
  • Net 113 lines removed from BladeCanvas (3026 → 2913). Pure refactor — existing 29 renderer-golden-hash cases pass byte-identical. +25 new pixel tests, expanding the suite from 29 → 54 cases.

Engine impact

  • BladeEngine.renderMode default change is the most behaviorally meaningful entry in this release. For users who never touched renderMode: every preset now renders through template-eval by default, which means visualizer output = expected hardware output by construction for the codegen → template-eval round-trip path. Parameter-engine fall-through preserved when a preset arrives without a codegen-emittable representation.

What was deliberately deferred to a future release

  • Visualizer Phase 3 perceptual validation: the bench proves codegen → template-eval works for all 455 gallery presets at 60fps, but a per-frame perceptual diff between parameter-engine and template-eval render is not yet automated. Eyeball-level testing in dev passed; rigorous diff pinning is a follow-up.
  • BladeBloom.tsx deprecation: replaced by BladePostProcessing.tsx; legacy component kept in place during soak.
  • Mobile-shell migration to Sidebar + MainContent: still open per POST_LAUNCH_BACKLOG.md.

v0.22.1

2026-05-16

Polyglot Audit Sprint. A single-session audit pass that lifted three downstream surfaces to the rigor standard the v0.22.0 emit↔parser audit doc set: gallery preset accuracy, multi-board codegen field coverage, and editor messaging integration. Shipped 16 PRs in one session (#331#346). Test count went from 3565 → 8605+ across the workspace.

Gallery accuracy + description quality (455 presets)

  • Audit (#332) — 4 parallel audit agents enumerated PASS / FLAG status for every preset in packages/presets/src/characters/*. Output: 296 PASS / 159 FLAG across the 455-preset library. Audit docs landed at docs/research/GALLERY_AUDIT_2026-05-16_*.md.
  • 6 real bugs fixed across 4 era files:
  • #333 creative-community.ts: Companion Cube colors (orange/blue → pink/gray; Portal portal colors had been used by mistake); Dueling Banjos ID/name realignment; 4 affiliation corrections (edgelord concepts → neutral).
  • #335 pop-culture: Spartan Laser ignition: 'pulseWave''pulse-wave' (registry uses dash-case; camelCase was silently falling back); Lucky the Leprechaun style: 'aurora''gradient' (gradientStops only renders on the gradient style).
  • #336 legends.ts: bulk screenAccurate: false pass (68 presets had it unset); 9 missing hiltNotes added (Sion, Jaina Solo, Caedus, Nomi Sunrider, both Ulic Qel-Dromas, Talon, Cade Skywalker, Vitiate); Darth Bane style→unstable per canon; Atris violet per KOTOR II; Arcann + Thexan affiliation→sith.
  • #338 canon-era: showcase-plasma-coil retraction 'flicker-out''flickerOut' (registry camelCase); eu-loden-greatstorm-blue-yellow (config was canonical yellow); eu-elzar-mann-purple-blue (config was canonical blue); eu-burryaga-green blade recolored to canonical green; 69-line author: 'on-screen' bulk pass across animated + ext-univ + sequel-era.
  • 60+ stub descriptions enriched to the Obi-Wan Kenobi (ANH) voice anchor across 5 parallel PRs:
  • #341 canon-era trilogy (7 prequel: Anakin EP I+II, Eeth Koth, Stass Allie, Darth Maul, Plo Koon, Luminara)
  • #343 animated + ext-univ (3 animated + 8 ext-univ including all 4 Cal Kestis variants, Keeve Trennis, Loden Greatstorm, Orla Jareni, Burryaga)
  • #344 creative-community (15: rave/Christmas/Power-Grayskull/Companion Cube/Pixel Miner/Digital Rain + Custom cluster)
  • #345 pop-culture (18: 8 Marvel — all 6 Infinity Stones + Mjolnir + Gungnir; 10 DC — 7 Lantern Corps emotional-spectrum + Godkiller + Doctor Fate's Ankh + Swamp Thing Green)
  • #346 legends (15: Revan/Bastila/Sion/Malak/Mara Jade/Kyle Katarn/etc., all KOTOR-era + NJO + Legacy lore-anchored)

Multi-board codegen field coverage

  • Audit (#332, #337) — 2 parallel agents (B1 Proffie compile-flash + runtime; B2 Xenopixel + CFX + Golden Harvest) traced every BladeConfig field through every emitter and catalogued PRESERVED / LOSSY / N/A / UNVERIFIED per cell. Reconciled into docs/research/MULTI_BOARD_FIELD_COVERAGE_2026-05-16.md — the authoritative matrix for 103 BladeConfig fields × 5 boards.
  • Xenopixel emitter-class adoption (#339) — The dormant XenopixelEmitter.ts class had superior style/ignition mappings, but the live ZIP export path used inline generators with weaker coverage. 27 of 33 KyberStation styles silently degraded to Steady (1); 8 ignitions silently fell back to Standard (0). The fix wires the class's mappings into the live path: 21 explicit style mappings (5 direct + 16 explicit degradation arms with notes), 20 ignition mappings (12 direct + 8 explicit degradation arms), ignitionMs/retractionMs clamping (100-800 / 200-1000), optional XENOPIXEL_NOTES.txt at ZIP root listing degradations. 71 new tests in packages/codegen/tests/xenopixelLivePathCoverage.test.ts.

Editor learnings integration

  • Renderer-level golden-hash coverage (#334) — drawWorkbenchBlade test matrix expanded from 9 to 29 cases (more styles, colors, state transitions, LED-count edge cases, shimmer extremes). Plus docs/QA_TESTING_CHECKLIST.md (476-line manual QA checklist for post-release polish). Rebased replacement for the original PR #315 which had unresolvable conflicts after the backlog restructure.
  • Sound-font silent-failure pre-flash check (#340) — Closes EMIT_PARSER_AUDIT §I. Before: a preset referencing font=mace_v2 could silently fall back to default font on activation if the folder wasn't on the card. After: CardWriter's direct-write path now enumerates SD card font folders, cross-references against preset fontName (case-insensitive, mirroring ProffieOS firmware), and raises SoundFontWarningModal listing missing folders + a "did you mean?" prefix hint. User can Cancel write or Continue anyway. Mirrors the EngineOnlyWarningModal pattern from PR #320.
  • HARDWARE_FIDELITY_PRINCIPLE.md (#332) — New audit-history entry "Encoding contracts at the emit↔parser boundary (smoking-gun saga)" capturing the 6h bench session, the actual cause (Color16 vs Color8 mismatch in RgbArg<> parser at styles/rgb_arg.h:41), and the drift sentinel (any new emit path crossing a parser boundary needs wire-format fixture tests).
  • POST_LAUNCH_BACKLOG.md re-prioritization (#332) — Runtime Presets demoted from "open #1" to "shipped/validated." New top priorities: multi-board codegen matrix completion, sound-font pre-check, gallery accuracy. Hardware Profiles MVP retained as v0.17 fallback path for DIY builds.
  • EMIT_PARSER_AUDIT.md closeout (#342) — Sections F (share URL), G (saber profile migration), and I (sound-font existence pre-check) flipped from ⚠ Unverified → ✅ Verified. Suggested follow-up section refreshed.
  • CardWriter audit-doc cross-reference (#340) — Added user-visible link to EMIT_PARSER_AUDIT.md at the bottom of the DeliverabilityPanel. Closes the C2 "cross-references in UI" plan item.

Bench validation tooling

  • Curated 15-preset bench validation generator (#331) — scripts/hardware-test/build-bench-validation-presets.mjs + TTS callout generator + SD deploy script. Empirically validated 15 Phase C presets at full brightness on 89sabers V3.9-BT 2026-05-16 (hilt-mounted).

v0.22.0

2026-05-16

Runtime Presets Release. Replaces the originally-planned v0.21.1 "Polyglot Release" patch tag. The May–mid-May sprint cycle that landed v0.21.1 internally (Xenopixel V3, Fredrik Style Editor, template-eval, 3D blade renderer, Fett263 Prop Editor Level 1, Hardware Profiles, audit Waves 0-4) now ships alongside the ProffieOS Runtime — SD card export pipeline: design presets in KyberStation, drop presets.ini on your saber's SD card, reboot, presets appear. No firmware flashing. Plus a smoking-gun 1/257-brightness RGB encoding fix discovered on the bench, and an engine→ProffieOS codegen port that brings 32 of 33 engine styles to real hardware.

Runtime Presets Path

  • ProffieOS Runtime — SD card export (#325) — New proffie_runtime board emitter generates presets.ini, the runtime preset file ProffieOS firmware with SAVE_PRESET enabled reads at boot. ZIP bundle is presets.ini + KYBERSTATION_README.txt at root — no font folders (factory firmware already has them). Phase A scope: reorder / rename / duplicate factory presets + reassign fonts. style=builtin N M references the firmware's compiled preset bank.
  • install_time auto-discovery (#325) — runtimePresetIO.parseInstallTime() reads the SD card's existing presets.ini to match the firmware's compile-time install_time so the emitted file is accepted on first reboot.
  • detectRuntimePresetSupport() probe (#325) — Separate SD-card detector (avoids conflating with config.h board detection) gates whether the runtime-presets export path is offered.
  • Preset-count cap warning (#325) — CardWriter surfaces a warning when the user's library exceeds 16 entries (firmware limit on most chassis).
  • Phase C "Custom styles" via advanced verb (#325) — Phase C lets users push custom color overrides through the runtime parser path (not just factory builtins). Marked deliverable after the brightness fix below.
  • Phase B color overrides + Phase C parameterized verbs beyond advanced deferred to a future release — they need per-chassis schema validation against real hardware.
  • Format spec (#325) — docs/research/PROFFIEOS_RUNTIME_PRESET_FORMAT.md. Verified byte-for-byte against ProffieOS v7.12 source (common/current_preset.h Write / Read / CreateINI / ValidatePresets).

Xenopixel V3 Full Board Support

  • Xenopixel V3 second-board support (#287) — 8 blade effect styles, 10 ignition animations, board-aware renderMode on BladeEngine, real fontconfig.ini + config.ini generation, firmware version awareness (5 versions: 1.0 / 1.2 / 1.2.5 / 1.3.1 / 1.4.0), Proffie→Xenopixel compat mapping, SD card import via parseXenoFontConfig / parseXenoConfigIni / importXenoSdCard. 60 files, +8,025 lines.
  • Xenopixel wizard + onboarding (#282, #283, #286) — Xenopixel V3 + V2 added to wizard board picker, honest design-reference export with KYBERSTATION_README.txt, refined CFX + Golden Harvest taglines.
  • Xenopixel coverage extensions (#289, #290) — Blaster / Force pickers, config preview, SD card import wiring, Scavenger / Hunter ignition coverage, ID fix, SSR contract tests for Batch 2+3 components.
  • Xenopixel doc refresh (#288, #297) — README, CHANGELOG, CLAUDE.md, backlog updates after audit pass.

Fredrik Style Editor Integration (Phases 1-7)

  • Variant cycling — Phases 1-4 (#298) — getChildren() tree walking on all template-eval classes, ColorChange variant cycling with setVariant(index) / variantCount API, VariantCycler UI component ([ ◀ ] N / M [ ▶ ]), mouse swing + time-scale control integration.
  • Template tree panel — Phase 5A + 5D (#299) — Read-only AST-to-tree renderer with annotations, then inline editing for template tree panel.
  • Layer controls — Phase 5C (#302) — Layer controls for Layers<> children + 6 template-eval function additions + 8 alias registrations (199 → 213).
  • Style transformations + insertion palette — Phase 6 + 7 (#304) — Style transformation pipeline + template insertion palette.
  • Phase 5F tests (#306) — 32 tests for TemplateInsertionPalette.

Template-Eval Interpreter

  • Phase 1 foundation (#295) — New packages/template-eval workspace package implementing a direct ProffieOS style template evaluator. Parses real ProffieOS C++ template syntax into evaluable ASTs.
  • Phase 2 engine bridge (#296) — Wires template-eval into BladeEngine as a new 'template-eval' render mode. Enables direct evaluation of ProffieOS style code in the visualizer.
  • Template registry expansion 153 → 372 (#303) — Waves 3-7 added 38+37 named colors, 12+9+8+10 template classes, 32+5+aliases, plus EffectIncrement template. Covers the full Fett263 corpus.

Visualizer Upgrade Phase 1-2

  • 3D blade renderer + Hardware Preview mode (#301) — Three.js / React-Three-Fiber blade mesh with emissive LED material, orbit controls, 3D hilt geometry (Phase 2B), mouse swing simulation (Phase 2C), HDR bloom post-processing via EffectComposer (Phase 2D), template-eval-driven Hardware Preview pipeline. Hardware Preview enabled by default (Phase 3). Closes Phases 1-2 of the Visualizer Upgrade Plan.
  • Visualizer Upgrade Plan doc (#300) — Multi-phase plan written ahead of implementation.

Fett263 Prop File Editor Level 1

  • Prop file editor — Level 1 (#305) — Toggle panel for ~30-40 Fett263 #defines. Covers ~90% of Proffie users' prop customization without authoring a full prop file. Level 2 (button routing sub-tab) and Level 3 (full custom prop generation) tracked in docs/POST_LAUNCH_BACKLOG.md.

Hardware Profiles

  • Phase 1 — package scaffolding + seed profiles (#316) — New @kyberstation/hardware-profiles workspace package + 2 seed chassis profiles.
  • Phase 2 — codegen adapter + chassis picker + export guards (#319) — Per-chassis pin maps wired into codegen, chassis picker in the export flow, validation guards.
  • Custom-paste passthrough (#318) — Lets users paste their own pin map for unprofiled vendors so the export pipeline isn't blocked on a profile landing first.
  • Sabertrio Standard chassis profile (#321) — Third built-in profile (experimental).
  • Engine-only export warning modal (#320) — Styled modal replaces window.confirm() when exporting a config that uses engine-only style fallbacks.
  • Engine style parity CI guard (#322) — Regression test that fails CI if a style without a codegen emitter is added without explicit fallback annotation.

Engine Style Codegen Parity

  • 32 of 33 engine styles ported to ProffieOS (#325) — Two waves of style emitters: 6 styles (helix / candle / ember / dataStream / shatter / neutron) + 8 more, bringing the codegen path to 32/33 engine-style parity. Closes the "blade you see is the blade you get" gap for the runtime-presets export.

Fixes (PR #325 includes the smoking-gun)

  • Phase C runtime presets RGB 1/257 brightness bug (#325 commit 45737f2) — KyberStation's proffie_runtime Phase C advanced verb was emitting RGB color values in 0-255 (8-bit) range. ProffieOS's RgbArg<> runtime arg parser at styles/rgb_arg.h:41 stores parsed values directly as Color16(r, g, b) — which expects 0-65535 per channel. Every Phase C blade was rendering at ~0.4% photon output. The compile-time Rgb<R,G,B> template auto-scaled via Color16(Color8(R,G,B)) × 0x101, so the compile-flash path was unaffected. Fix scales each channel × 257 and clamps to 0-65535. Empirical verification on the 89sabers V3.9-BT bench (set_style1 advanced 60395,4626,36494 matches factory Vader luminance).
  • Xenopixel Candy + Flashing blade effects unreachable (#293) — XENO_BLADE_EFFECTS entries for IDs 4 (Candy) and 7 (Flashing) had kyberStyle: null, making them unreachable through the UI despite complete engine implementations.
  • Xenopixel 11 audit findings resolved (#289) — Orphaned XenoDesignPorter.tsx deleted, segment overflow in XenoEffectPicker fixed, missing ignition ID mappings, motion/settings panel state moved to Zustand, config.ini round-trip for crossguard/double-blade.
  • Sub-1024 brand drift + status bar BOARD-BOARD dedup (#277) — Tablet rendering fixes.
  • Lexer EASYBLADE + ronin under-close recovery (#279) — Closes 2 LEXER_INCOMPATIBLE fixtures.
  • Import banner mobile vertical stack + auto-scroll (#280).
  • Mobile Analyze button hide + rgb-luma rail removed on compact (#272).

Audit Waves 0-4

  • Wave 0 — public-facing counts corrected (#309) — Landing + features pages now show 33 styles, 22 effects, 455+ presets. Fixes metadataBase Next.js warning.
  • Wave 1 — documentation + SSR hygiene (#310) — HelpTooltip SSR warning fixed, dead doc references, stale CLAUDE.md counts.
  • Wave 3 — dead code removed (#311) — performanceStore + 28 tests deleted, uiStore.performanceBarHeight removed, stale TODOs cleaned.
  • Wave 4 — CLAUDE.md compressed 3,043 → 573 lines (#312) — 44 historical docs archived to docs/archive/, 7 cross-references fixed.

Presets

  • Preset Cartography — 40 new presets across 4 franchises (#307) — KOTOR-adjacent, animated series deep-cuts, Halo / SWTOR / Clone Wars creative-community designs. Library at 455 (ALL_PRESETS.length, runtime-verified).
  • Renderer-level golden-hash card snapshot tests (#307) — node-canvas-driven pixel regression tests for the card-snapshot drawer pipeline (drawBladePreview, drawColorChip, drawBackdrop).
  • import_failure issue template + Report-a-failing-config affordance (#273).

Other

  • Slow-motion mode (#294) — 1× / ½× / ¼× time scale playback toggle.
  • Mouse-driven swing simulation (#291) — Pointer movement over the blade canvas drives swing speed + blade angle; one-pole low-pass filter, exponential decay on pointer leave.
  • Inline live blade previews on /features (#276).
  • Board compatibility roadmap doc (#284).
  • Persisted recentImportBatch across page reloads (#271).
  • Prop file editor tiered roadmap (#292) — backlog doc, no code.

Test count delta (post-PR-#325 merge — pending final verification)

PR #325's body reports 8,434 tests passing post-merge (web 3,588 / codegen 2,895 / engine 1,219 / boards 278 / template-eval 180 / presets 138 / hardware-profiles 74 / sound 62). Final delta vs v0.20.3 will be tallied when the tag is cut.


v0.21.1

2026-05-12

Polyglot Release. 118 commits since v0.20.3 consolidate the May sprint cycle: Xenopixel V3 full board support, Fredrik Style Editor Integration Phases 1–7, the template-eval interpreter, Visualizer 3D blade + Hardware Preview, Fett263 Prop File Editor Level 1, 40 new presets, and a comprehensive 4-wave audit. KyberStation's pipeline is now multi-board, multi-engine, and multi-style-system.

Added

  • getChildren() tree walking on all template-eval classes — 40+ template classes across colors, styles, effects, transitions, functions, and wrappers now expose their child StyleTemplate references via getChildren(). Enables recursive DFS through arbitrary template nesting depths for variant cycling, future AST editing, and style transforms.
  • ColorChange variant cyclingTemplateEvalBridge exposes variantCount, currentVariant, and setVariant(index) API backed by recursive walkForColorChange() tree walk. EFFECT_CHANGE events cycle through color variants in templates using ColorChange<>.
  • ChangeEffect in engine effects registry — no-op effect entry for the 'change' EffectType, allowing the engine's effect system to dispatch color-change meta-events to the template evaluator.
  • VariantCycler UI component — inline action-bar control ([ ◀ ] N / M [ ▶ ]) for cycling through ColorChange template variants. Only renders when variantCount > 0. 3 public accessors on BladeEngine (variantCount, currentVariant, setVariant) delegate to the template-eval bridge. Full a11y with role="group", aria-label, aria-live="polite".
  • 6 new template-eval classesPulsingFTemplate, VolumeLevelTemplate, EffectPulseFTemplate, ModFTemplate, BendTimePowXTemplate, TrCenterWipeInSparkTemplate. Closes all 7 known registry gaps from Fett263 corpus fixtures (PulsingL was already aliased). Registry count: 147 → 153.
  • Mouse-driven swing simulationuseMouseSwing hook tracks pointer movement over the blade canvas: horizontal velocity → swing speed (0-1), vertical position → blade angle (-1 to 1). One-pole low-pass filter, exponential decay on pointer leave, tuning constants exported for testability. Gated by mouseSwingEnabled in accessibilityStore (default on). 46 tests across 7 describe blocks.
  • Time-scale controlBladeEngine.timeScale multiplier (0.1x–4.0x) scales all animation timing. TimeScaleControl.tsx inline toolbar component with compact/expanded modes and presets [0.25, 0.5, 1, 2]. Keyboard shortcuts: [ slower, ] faster. 9 engine tests + 12 UI tests.
  • 93 new tests total — 13 engine variant cycling, 12 VariantCycler UI, 25 template registry gaps, 22 mouse swing (net new), 9 time-scale engine, 12 TimeScaleControl UI.
  • ProffieOS template interpreter — Phase 1 foundation (PR #295) — New packages/template-eval workspace package implementing a direct ProffieOS style template evaluator. Parses real ProffieOS C++ template syntax into evaluable ASTs.
  • ProffieOS template interpreter — Phase 2 engine bridge (PR #296) — Wires template-eval into BladeEngine as a new 'template-eval' render mode, enabling direct evaluation of ProffieOS style code in the visualizer.
  • Visualizer 3D blade renderer + Hardware Preview mode (PR #301) — Three.js / React-Three-Fiber blade mesh with emissive LED material, orbit controls, and a template-eval-driven Hardware Preview pipeline. Closes Phases 1–2 of the Visualizer Upgrade Plan. Phase 2C (mouse interaction) and 2D (post-processing) remain.
  • Template tree panel + inline editing (PRs #299, #302, #304, #306) — Fredrik Style Editor Integration Phases 5A+5D (AST-to-tree renderer + inline editing), 5C (layer controls for Layers<> children), 5F (32 TemplateInsertionPalette test cases), Phase 6 (style transformations), and Phase 7 (template insertion palette). Closes Phases 1–7 of the integration plan.
  • Template registry expansion 153 → 372 entries (PR #303) — Waves 3–7 added 38 named colors + 9 function templates (wave 3), 12 function templates + 32 aliases (wave 4), 8 template classes + 5 aliases (wave 5), 37 named colors + 10 template classes + aliases (wave 6), and EffectIncrement template (wave 7). Covers the full Fett263 corpus.
  • Fett263 Prop File Editor Level 1 (PR #305) — Toggle panel for ~30–40 Fett263 #defines. Covers ~90% of Proffie users' prop customization without authoring a full prop file. Level 2 (button routing sub-tab) and Level 3 (full custom prop generation) tracked in docs/POST_LAUNCH_BACKLOG.md.
  • 40 new presets across 4 franchises (PR #307) — Preset Cartography wave covering KOTOR-adjacent characters, animated series deep-cuts, and creative-community designs. Brings library to 455 (ALL_PRESETS.length, runtime-verified).
  • Renderer-level golden-hash card snapshot tests (PR #307) — node-canvas-driven pixel regression tests for the card-snapshot drawer pipeline (drawBladePreview, drawColorChip, drawBackdrop). Full BladeCanvas pipeline coverage remains TBD — prerequisite for Visualizer Phase 2D.

Fixed

  • Xenopixel Candy + Flashing blade effects unreachable (PR #293) — XENO_BLADE_EFFECTS entries for IDs 4 (Candy) and 7 (Flashing) had kyberStyle: null, making them unreachable through the UI despite having complete engine implementations. Fixed to 'candy' / 'flashing'. Also corrected 5 documentation inaccuracies in CHANGELOG.md.
  • Xenopixel 11 audit findings resolved (PR #289) — Deleted orphaned XenoDesignPorter.tsx, fixed segment overflow in XenoEffectPicker, added missing ignition ID mappings in XenoIgnitionPicker, moved motion/settings panel state from inline useState to Zustand stores, and fixed config.ini parser + emitter round-trip for crossguard/double-blade settings.

Changed

  • Xenopixel SSR contract tests (PR #290) — Added SSR rendering tests for Batch 2+3 Xenopixel UI components ensuring server-side rendering compatibility.
  • CLAUDE.md compressed 3,043 → 573 lines (PR #312) — Wave 4 structural audit archived 44 historical docs to docs/archive/, fixed 7 cross-references to relocated files, kept only current-state context. Detailed per-session history preserved in git.
  • Dead code removed (PR #311) — Wave 3 deleted performanceStore + its 28 tests, removed uiStore.performanceBarHeight, cleaned stale TODOs left from earlier refactors.
  • Documentation + SSR hygiene (PR #310) — Wave 1 fixed HelpTooltip SSR warning, dead doc references, and stale CLAUDE.md counts.
  • Public-facing counts corrected (PR #309) — Wave 0 fixed style/effect/preset counts on landing + features pages (33 styles, 22 effects, 455+ presets) and resolved the metadataBase Next.js warning.
  • Backlog + handoff documentation refreshed (PRs #297, #300, #308, #313) — Docs-only updates: Xenopixel post-audit documentation refresh, Visualizer Upgrade Plan written, backlog + handoff refreshed twice as state evolved.

Test count delta

| Package | v0.20.3 | v0.21.1 | Delta | |---------|---------|---------|-------| | Web | 2,867 | 3,552 | +685 | | Codegen | 2,562 | 2,854 | +292 | | Engine | 957 | 1,219 | +262 | | Boards | 260 | 278 | +18 | | Template-eval | — (new) | 180 | +180 | | Presets | 138 | 138 | 0 | | Sound | 62 | 62 | 0 | | Total | 6,846 | 8,283 | +1,437 |

All 13 workspace packages typecheck + 7 test packages green.


v0.21.0

2026-05-07

Xenopixel V3 — Full Board Support. KyberStation now has complete second-board support for Xenopixel V3, the most popular budget lightsaber controller. Visual effect picker, accurate blade previews, real SD card config generation, firmware version awareness, and a Proffie-to-Xenopixel compatibility dialog — all in one PR (#287, 12 commits, 60 files, +8,025 lines).

Added

  • 8 Xenopixel blade effect styles in the engineXenoFireStyle, XenoSteadyStyle, XenoUnstableStyle, XenoRainbowStyle, XenoCandyStyle, XenoCrackStyle, XenoPulseStyle, XenoFlashingStyle at packages/engine/src/styles/xenopixel/. Each approximates what the real Xenopixel V3 firmware produces on hardware.
  • 10 Xenopixel ignition animationsXenoStandardIgnition, XenoVelocityIgnition, XenoTorchIgnition, XenoStackIgnition, XenoFoldTileIgnition, XenoWordIgnition, XenoFaserIgnition, XenoScavengerIgnition, XenoHunterIgnition, XenoBrokenIgnition at packages/engine/src/ignition/xenopixel/.
  • Board-aware renderMode on BladeEngine'proffie' | 'xenopixel' flag switches the style and ignition registries so the visualizer shows what the user's actual saber will produce. setRenderMode() method + renderMode getter on BladeEngine.
  • Real fontconfig.ini + config.ini generationXenopixelEmitter rewritten from placeholder JSON to actual INI file output matching the Xenopixel V3 SD card format: fontN=(R,G,B),bladeEffect,blasterEffect,forceEffect,lockupEffect,defaultEffect,ignitionStyle,ignitionSpeed,retractionSpeed[,inTime,outTime[,customFunction]].
  • Firmware version awareness (XenoFirmwareVersion type) — 5 firmware versions (1.0, 1.2, 1.2.5, 1.3.1, 1.4.0) with cumulative capability flags (perFontConfig, motorChamber, btToggle, knockPoke, lightningBlock, meltEffect, configurableInOutTime, customFunction). Emitter adjusts output format based on selected version. getXenoFirmwareFeatures() + XENO_FIRMWARE_FEATURES exported from @kyberstation/boards.
  • Xenopixel board profile enrichmentXENO_BLADE_EFFECTS (8 effects with kyberStyle mappings), XENO_IGNITION_STYLES (12 styles with categories), XENO_BLASTER_EFFECTS (3), XENO_FORCE_EFFECTS (2), all exported as typed const arrays from packages/boards/src/profiles/xenopixel.ts. configFormat corrected from 'json' to 'ini-txt'.
  • Board-gated UI componentsXenoEffectPicker (visual 8-card grid for blade effects), XenoIgnitionPicker (12-card grid for ignition styles), XenoBlasterPicker (3-card blaster effect grid), XenoForcePicker (2-card force effect grid), XenoMotionPanel (toggle + sensitivity for stab/twist/swing/pull), XenoSettingsPanel (volume, clash sensitivity, blade modes, countdown, blade length, crossguard), XenoConfigPreview (live fontconfig.ini + config.ini preview), XenoImportPanel (SD card config paste-and-parse). All render only when active board is Xenopixel; ProffieOS surfaces hidden automatically.
  • Proffie-to-Xenopixel compatibility analysisgetXenopixelCompat() in apps/web/lib/xenopixelCompat.ts maps each ProffieOS style/ignition to the closest Xenopixel equivalent with a human-readable degradation note. mapStyleToXenoEffect() + mapIgnitionToXenoStyle() handle direct matches and approximate mappings with 25+ style entries.
  • SD card importparseXenoFontConfig() + parseXenoConfigIni() + importXenoSdCard() reconstruct KyberStation BladeConfig entries from existing Xenopixel SD card files. detectFirmwareVersion() heuristic detects firmware version from file structure clues.
  • Compatibility scoringgetXenopixelCompat() computes compatibility analysis on-the-fly (not stored as preset metadata) with bladeEffect, ignitionStyle, and degradationNote fields. scoreCompatibility() enriched to handle Xenopixel capability constraints.

Changed

  • Emitter registry expanded — getEmitter() / hasEmitter() / listEmitterBoards() unchanged API, but the XenopixelEmitter behind them now produces real config files instead of placeholder JSON.
  • packages/boards exports expanded with 6 new Xenopixel-specific type exports and 4 new const exports.
  • packages/codegen exports expanded with XenoEmitterFirmwareVersion type export.

Test count delta

| Package | Before | After | Delta | |---------|--------|-------|-------| | Engine | 957 | 1,057 | +100 | | Codegen | 2,562 | 2,654 | +92 | | Web | 2,867 | 3,005 | +138 | | Boards | 260 | 278 | +18 | | Total | 6,846 | 7,194 | +348 |

All 10 packages typecheck + test green.


v0.20.0

2026-05-02

Sprint 5E — per-preset switcher in the import banner. When a user imports a multi-preset config.h via Sprint 5D's "Import N Presets" flow, all presets land in their library. Today the user has to navigate to My Presets sidebar to flip the visualizer between them. v0.20.0 adds a switcher dropdown directly inside the import banner so they can preview any imported preset without leaving the OUTPUT panel.

Added

  • Per-preset switcher dropdown in ImportStatusBanner (PR #268):
  • Renders only when recentImportBatch.length > 1 AND the active config name matches one of the batch entries
  • "Preset N of M" indicator showing current position
  • Native select listing all batch entries by name in source order
  • onChange calls bladeStore.loadPreset for the selected preset's stored config
  • Disappears when the user navigates to a non-import preset or clicks Convert to Native
  • uiStore.recentImportBatch — ephemeral list of {id, name} for the most recently imported batch. Set by CodeOutput's import handler (only when batch.length > 1); cleared by setRecentImportBatch(null) from the banner's Convert-to-Native wrapper. Not persisted across page reloads — saved presets remain in My Presets.

Changed

  • CodeOutput's "Import N Presets" handler now loads the FIRST extracted preset's reconstructed config into the visualizer instead of the previous fallback (cppResult was a partial parse of the full pasted blob, which often defaulted to blue). Preset 1 now renders accurately on first paint.
  • ImportStatusBanner's Convert to Native button wrapped in a handler that also clears recentImportBatch so the switcher disappears together with the banner.

Test count delta

+7 tests in importStatusBanner.test.tsx (18 → 25). All 10 packages typecheck + test green. Web 2750+ tests passing.


v0.19.0

2026-05-02

Sprint 5C reconstructor patterns. Three new ConfigReconstructor recognitions lifted from the v0.18 corpus expansion. The visualizer now correctly classifies Fett263's most common base-style shape and surfaces multi-phase + effect-event metadata on every imported config.

Added

  • Hyper Responsive Rotoscope detection (PR #266) — Mix<HoldPeakF<SwingSpeed<...>>, base, alt> is Fett263's signature swing-reactive base style. Previously fell through to custom with 0.2 confidence (visualizer defaulted to blue). Now classified as rotoscope with 0.85 confidence so the visualizer renders the imported preset correctly. Also catches the SwingAcceleration variant. Browser-verified: pasting a Hyper Responsive sample with base color Rgb<140,30,200> lands the visualizer at #8c1ec8 instead of default blue.
  • Multi-phase ColorChange extraction (PR #266) — ColorChange<TR, A, B, C, ...>, ColorSelect<F, TR, A, B, C, ...>, and ColorChangeL<F, A, B, C, ...> now surface their alt-phase colors via a new altPhaseColors[] field on ReconstructedConfig. Previously the OS7 ColorChange family unwrapped to its first color and the rest were silently lost. Per-shape offset for "where the color list starts" handled correctly (1 / 2 / 1 respectively).
  • Effect-id detection across the whole AST (PR #266) — new detectedEffectIds[] field surfaces every EFFECT_* event the imported style references. Whitelist of 33 known IDs (Force / Boot / Preon / Quote / User1-8 / Battery / Volume / PowerSave / etc.) covers OS7's full set. Previously only EFFECT_PREON and EFFECT_STAB were detected. Future visualizer overlays can render flash effects per-event without re-walking the AST.
  • __test seam exports extendedKNOWN_EFFECT_IDS, resolveAltPhaseColors, detectEffectIds now exposed for unit testing of the new pattern helpers in isolation.

Note on scope

The two new fields (altPhaseColors, detectedEffectIds) are returned on ReconstructedConfig but not yet threaded into BladeConfig or surfaced in the editor UI. That's a future sprint — this release gives downstream consumers (visualizer, future per-phase UI affordances, effect-overlay renderer) the data they need to act on it. The user-facing improvement that ships today is the Hyper Responsive Rotoscope classification fix.

Test count delta

+20 tests in reconstructorSprint5C.test.ts. Codegen 2542 → 2562. All 10 packages typecheck + test green.


v0.18.0

2026-05-02

Fett263 import coverage push (Sprint 5). Builds on v0.17.0's preserve-and-tweak foundation. The headline user-facing win: paste your full Fett263-generated config.h and every preset becomes its own entry in your library, ready to flash with the original code preserved. The previous release made single-style imports work; this release closes the actual user workflow — bringing forward whole saved configs.

Test count delta: +291 web tests (2452 → 2743). +6 PRs across the sprint. All 10 workspace packages typecheck + test green.

Added

  • Multi-preset extraction from full config.h (PR #263) — paste a complete Preset presets[] = { ... } array and KyberStation extracts each preset into its own userPresetStore entry, preserving:
  • User-facing display label as the entry name (e.g. "Obi-Wan ANH", "Darth Maul Sith Saber")
  • The preset's font name in importedSource (e.g. "Pasted ProffieOS C++ — ObiWanANH")
  • The per-preset style block as importedRawCode — each preset exports its OWN style on re-flash, not the entire pasted file
  • Standard reconstructed BladeConfig fields for the visualizer preview
  • Toast: "Imported N presets to your library"
  • Apply button label flips from "Apply to Editor" to "Import N Presets" when N > 1
  • findStylePtrBlocks (angle-bracket-balanced multi-block walker) + extractPresets (per-preset metadata via enclosing brace + string-literal extraction) + helpers in apps/web/lib/import/configShapeDetect.ts
  • 14 new unit tests + 5 integration tests in multiPresetImportFlow.test.ts
  • Full config.h shape detection + first-preset extraction (PR #260) — when users paste a wrapped config (#ifdef CONFIG_TOP, #define, Preset presets[], BladeConfig blades[]), KyberStation detects the shape, extracts the first preset's style template for the visualizer reconstruction, and surfaces a "Detected full config.h with N presets" notice. Before this, the visualizer would default to pure blue when it couldn't find a recognizable style template at the wrapped root.
  • detectConfigShape heuristic (no full parse — #ifdef CONFIG_PRESETS / Preset presets[] / BladeConfig blades[] / CONFIGARRAY(...) markers)
  • extractFirstStylePtr walks angle-bracket-balanced to pull the first style block
  • 20 unit tests for naked / wrapped / multi-preset / edge-case inputs
  • Template registry expansion to 265+ templates — Sprint 5A in two parts:
  • PR #259 (5A partial): +32 color templates + named-color expansion for OS7 sister-form aliases (PulsingX, PulsingL, Pixelate, etc.)
  • PR #264 (5A rest): +8 layer templates (OnSparkL, BlinkingL, PulsingL, SparkleL, StrobeL, TransitionLoopWhileL, TransitionPulse, MultiTransitionEffectL), +7 transitions (TrBlink, TrCenterWipe/-X, TrCenterWipeSpark/-X, TrCenterWipeInSpark/-X, TrWipeInSparkTipX), +31 functions (math primitives, blaster props, audio sources, geometric helpers, blast functions, pin reading, blink functions, counters), +4 wrapper signature corrections (IgnitionDelay, RetractionDelay, InOutHelperX, InOutTr), Parser NAMED_PRIMITIVES +7, VARIADIC_TEMPLATES +18, classifyNode +28
  • Combined: registry covers most of Fett263's OS7 generator output + ProffieOS 7.x stdlib
  • Corpus expansion to 63 fixtures (PR #261) — grew the test corpus from 21 to 63 real Fett263 fixtures scraped from public GitHub configs (farzahn/proffie, profezzorn/ProffieOS, NoSloppy/ProffieOS3-eval, FrankKerschbaumer3/proffie_configs). Spans 13 niche generator-path categories: Crystal Chamber accent, Multi-blade AUX, Multi-Phase 4/10/2-color, Hyper Responsive Rotoscope, Special Abilities EFFECT_USER1, Battery/charging, Power Save, Force/Boot/Preon, Quote/Music/Track, Lockup variants, Stab/drag/melt customization, Off-behavior, Legacy ProffieOS variants. Each fixture has a // Source: GitHub URL header.
  • findEnclosingBraceBlock + extractStringLiterals helpers (PR #263) — backbone of the per-preset metadata extraction; each style block walks backward to its enclosing { ... } block and pulls quoted string literals (first → fontName, second → track, last → displayLabel).

Changed

  • applyReconstructedConfig now optionally accepts rawCode + source parameters (carried from v0.17.0; reused by Sprint 5D's per-preset call site) — when supplied, the returned BladeConfig carries importedRawCode / importedAt / importedSource so the export path emits the original code verbatim with provenance header.
  • CodeOutput.tsx Apply handler branches on cppConfigShape.styleCount > 1 — multi-preset path calls extractPresets, parses + reconstructs each, saves each via useUserPresetStore.savePreset, then loads preset 1 into the visualizer.
  • fett263CorpusRoundTrip.test.ts floor changed from toBe(21) to toBeGreaterThanOrEqual(21) — Sprint 5B's expansion would otherwise break this test on merge.

Fixed

  • CodeQL "incomplete string escaping" warning on match[0].replace('<', '') (PR #260 follow-up) — switched to match[0].slice(0, -1) (deterministic, drops the trailing literal < from the regex match). Functionally identical, quiets the security analyzer.

Honest scope

Coverage is much higher than v0.17.0 but still not "every Fett263 library template." The raw-code preservation safety net continues to do load-bearing work — anything KyberStation can't fully reconstruct still flashes byte-identically to the original. Send failing snippets via GitHub issue and they go directly into the next corpus expansion sprint.


v0.17.0

2026-05-02

Fett263 import sprint. Closes the import gap that the first GitHub user feedback flagged: pasting a real Fett263 OS7 Style Library config used to drop ~30 templates per fixture as "unknown" and silently strip anything KyberStation's reconstructor didn't fully understand. After this sprint, the same fixtures parse with 0 unknown-template warnings, the original ProffieOS code is preserved verbatim on export (so it's flashable as-is), and a UI banner explains the contract with "★ Save Preset" and "Convert to Native" actions wired right where users land after importing.

Test count delta: +260 tests across the sprint (engine 14, codegen 79, web 167). All 10 workspace packages typecheck + test green.

Added

  • Import preservation schema (PR #252) — BladeConfig gains importedRawCode / importedAt / importedSource optional fields plus a migrateImportFields(value) helper. When set, the codegen export path emits the original code verbatim with a provenance header instead of regenerating from BladeConfig. 14 contract tests pin the migration shape.
  • Verbatim raw-code export path (PR #253) — generateStyleCode early-returns the imported raw code with a 3-line provenance header (// Imported from {source} / // Imported at: {ISO date} / // Original ProffieOS code preserved verbatim — regenerated structure suppressed.) when BladeConfig.importedRawCode is non-empty. Modulation-binding warning surfaces when bindings are present alongside the raw path. +21 tests covering edge cases (empty / null / whitespace rawCode, non-finite importedAt, multi-line nested bracket preservation).
  • ProffieOS template registry expansion (PR #254) — registry grows from 46 → 219 templates to cover the OS7 Style Library staples Fett263 emits: full Responsive*L family (ResponsiveBlastL, ResponsiveStabL, ResponsiveDragL, ResponsiveMeltL, ResponsiveBlastFadeL, ResponsiveBlastWaveL, ResponsiveLightningBlockL), flicker color templates (BrownNoiseFlicker, RandomFlicker, RandomPerLEDFlicker), OS7 Color Change machinery (ColorChange, ColorSelect, Variation, ColorChangeL, ColorSequence), the function-parameterised X transitions (TrFadeX, TrWipeX, TrJoin, TrLoop, TrColorCycle, TrSparkX, TrWaveX, TrCenterWipeX), legacy Style*Ptr wrappers, and the full EFFECT_* / LOCKUP_* / *_COLOR_ARG enum-token surface. Parser.ts VARIADIC_TEMPLATES upgraded from Set to Map<name, minArgs> for proper variadic-arg validation; NAMED_PRIMITIVES expanded for legacy ALL-CAPS color macros + EASYBLADE / using tokens.
  • ConfigReconstructor pattern expansion (PR #255) — recognises 10 new Fett263-specific patterns: OS7 layer-sandwich shape (slot-recognition for ResponsiveBlast/Stab/Drag/Melt/LightningBlock), responsive-effect color extraction, two-form TransitionEffectL (OS7 2-arg + pre-OS7 4-arg), ARG_SLOT_IDS catalog of OS7 Edit Mode arg names, flicker-wrapper base-color extraction, ColorChange / ColorSelect / ColorChangeL recognition with default-color extraction, EffectSequence preservation, legacy Style*Ptr wrappers (StyleNormalPtr / StyleStrobePtr / StyleFirePtr / StyleRainbowPtr), Tr*X function-driven transition duration extraction, EFFECT / LOCKUP leaf-token handling. +35 tests covering each pattern + variants + cross-pattern integration.
  • <ImportStatusBanner> (PR #256, expanded in PR #257) — warn-toned (badge-creative palette, role=status) banner renders in CodeOutput above the generated code section whenever config.importedRawCode is set. Shows source label + relative time + plain-language explanation that visualizer edits update the preview only until you convert. Two action buttons: ★ Save Preset (PR #257 — discoverability fix) opens window.prompt, persists current config including importedRawCode to userPresetStore (IndexedDB-backed), button flips to ✓ Saved with status-ok green for 2.4s. Convert to Native clears the 3 import fields via the new store action.
  • bladeStore.convertImportToNative() (PR #256) — strips importedRawCode / importedAt / importedSource via destructure-and-spread (immutable update, fresh object reference). One-way operation surfaced through the banner's Convert button.
  • Fett263 fixture corpus (PR #254) — 21 real-world Fett263 OS7 / OS6 / legacy style snippets at apps/web/tests/fixtures/fett263-imports/, scraped from public GitHub configs. Spans Baylan Skoll, Kylo Ren, Mace Windu, Revan, Marrok, Cal Kestis, Ronin, multi-phase 5-color, legacy StyleNormalPtr, StyleStrobePtr, StyleFirePtr, StyleRainbowPtr shapes. Each file carries a // Source: attribution comment.
  • Corpus round-trip suite (PR #257) — apps/web/tests/fett263CorpusRoundTrip.test.ts exercises the full Phase 1–2B journey across the 21-fixture corpus: parse → reconstruct → apply (with raw code) → export (verbatim with header) → tweak baseColor (raw still preserved) → convertImportToNative + regen (no header, structurally diverged). 116/118 passing across 19 parseable fixtures × 6 assertions + 2 aggregate stats; 2 fixtures it.todo for documented lexer-incompatible shapes.
  • Save+import preservation tests (PR #257) — 8 tests in userPresetStoreImportFields.test.ts covering save → hydrate → restore round-trip through a mocked IndexedDB layer. Multi-line raw code with embedded brackets round-trips byte-identical; native (no-import) configs stay clean; duplicatePreset preserves import fields.

Changed

  • Apply to Editor passes raw code (PR #256) — applyReconstructedConfig now accepts rawCode + source parameters. CodeOutput's "Apply to Editor" handler wires the textarea content + "Pasted ProffieOS C++" label so imported configs land in the store with the import-preservation fields populated automatically.

Fixed

  • __research__ directory excluded from web tsconfig (PR #252) — pre-existing typecheck error from apps/web/tests/cardSnapshotGoldenHash/__research__/pixelmatch-prototype.ts referencing the unbundled pixelmatch dep. The file's own header explicitly says "THIS IS NOT A TEST" — it's a manual vite-node script. Excluding the directory pattern from the project glob honors that intent.

v0.16.1

2026-05-01

Post-launch polish wave. ~30 PRs landed in the day after v0.16.0's public launch, covering: the full mobile UX overhaul (Phases 4.1 → 4.5 feature-complete on main), Wave 8 modulation routing (8 aux/gesture modulator plates), Saber Card + Kyber Glyph audit fixes (the header "Share Kyber Code" button now actually shares Kyber Glyphs!), CommunityGallery wired to a real fetch, 28× speedup on GIF hilt rendering, 16+ new presets (Acolyte, Maul lifecycle, Star Wars Visions Vol 1), and the marketing site /community page.

Test count at tag: ~5,500 workspace-wide. Typecheck clean across all 10 packages.

Added

  • Mobile UX overhaul — feature complete — All 5 phases of the StickyMiniShell handoff shipped to main:
  • Phase 4.1 sticky shell foundation + drawer + auto-ignite (PRs #199 + #200)
  • Phase 4.2 sticky mini-shell + section tabs + scrollable status strip (PR #203)
  • Phase 4.3 Color-tab QuickControls + ColorRail (PR #205)
  • Phase 4.4 ParameterSheet 3-stop primitive (PR #207)
  • Phase 4.4.x ParameterSheet long-press integration via global store (cascade-merged PR #211)
  • Phase 4.5 Inspect mode + zoom HUD on blade canvas (cascade-merged PR #211)
  • Wave 8 — Button + gesture modulators — 8 new aux/gesture modulator plates: aux-click, aux-hold, aux-double-click, gesture-twist, gesture-stab, gesture-swing, gesture-clash, gesture-shake. Engine layer (PR #222) ships registry + sampler with event-trigger latching. UI shell (PR #227) adds 6 category sections (MOTION / AUDIO / POWER / STATE / BUTTON / GESTURE) inside one ModulatorPlateBar with bespoke CSS-keyframe glyphs.
  • CommunityGallery real fetch (PR #221) — replaces hardcoded placeholderStyles with a fetch from ${basePath}/community-gallery.json on GitHub Pages. 3-branch fallback chain: fresh fetch → 30-min localStorage cache → in-memory placeholder + soft warning.
  • Marketing site /community page (PR #219) — 5/5 marketing pages now shipped (/features, /showcase, /changelog, /faq, /community).
  • Saber GIF Sprint 4 (PR #218) — 3 new effect-specific GIF variants in the My Crystal panel: blast-deflect (~600ms), stab-tip-flash (~500ms), swing-response (~2s).
  • Star Wars Visions Vol 1 presets (PR #228) — 8 new presets from Disney+ animated anthology.
  • Acolyte + Maul lifecycle presets (PR #224) — extends Preset Cartography with the 2024 Acolyte series sabers + Maul's full character arc.
  • Darksaber preset audit (PR #225) — normalizes Darksaber config fields with a drift sentinel.
  • Card snapshot golden-hash test infrastructure (PR #217) — FNV-1a drift sentinel + region-masked hashing utility (the layout × theme matrix path turned out non-viable cross-runner; see Changed/Fixed below).
  • Card snapshot v2 research (PR #223) — docs/research/CARD_SNAPSHOT_V2_PERCEPTUAL_DIFF.md documenting why pixelmatch perceptual diff doesn't work for cardSnapshot regression tests.
  • Gradient editor extraction (PR #215) — lib/gradient/ shared module; GradientBuilder consumer-migration stub retired.

Changed

  • Share emitters use Kyber Glyph (PR #229) — 5 share sites migrated from the legacy base64 hash format to the modern ?s=<glyph> URL: ShareButton, GalleryColumnB, GalleryDetailModal, CodeOutput, PresetBrowser. The header "Share Kyber Code" button now actually shares Kyber Glyphs. lib/configUrl.ts retained as decode-only for backward compatibility with existing legacy URLs.
  • Archetype detection uses preset metadata (PR #229) — replaced regex-based isCanonicalPreset with a check against screenAccurate === true AND continuity === 'canon' from the preset library. Coverage delta: ~14 → 87 canon presets correctly detect as CNO. Pop-culture presets (LOTR, Marvel, Zelda, etc.) correctly fall through to JED/SIT/SPC.
  • Toast on shared URL load (PR #229) — Loaded crystal: JED.aBcD…Loaded ${publicName ?? blade.name ?? 'crystal'}.
  • GIF hilt cached outside frame loop (PR #230) — prerenderHilt helper renders the SVG hilt once into a buffer canvas, blitted per frame. ~28× per-frame speedup on hilt rendering (5.07ms → 0.18ms in the 60-frame swing-response variant).
  • Font-loaded gate before card render (PR #230) — await document.fonts.load("700 28px Orbitron") in both renderCardSnapshot and renderCardGif, with defensive guards for SSR + jsdom test environments.
  • Backlog audit pass (PR #214) — 8 silently-shipped items moved to ✅: useAudioEngine singleton, lib/blade/* extraction, BLADE_LENGTHS lift, Strip Config thickness, Topology Triple/Inquisitor, Saber GIF Sprint 3, OG hero, favicon, CANONICAL_DEFAULT_CONFIG drift-sentinel.

Fixed

  • Wizard auto-open on /editor?wizard=1 (PR #197) — query-param-driven wizard opening was silently broken.
  • Marketing footer community link (PR #220) — /community link in the marketing footer was 404'ing.
  • /dev/card-preview stale dev artifact (PR #230) — header comment said "deleted before commit" but the file was still in the repo. Now actually deleted.

Negative findings (worth carrying forward)

  • Hash-based cardSnapshot regression testing is not viable cross-runner (closed PR #226). Even platform-specific golden files (cardSnapshot.linux.test.ts.snap + cardSnapshot.darwin.test.ts.snap) fail because GitHub's macos-latest runner produces different hashes than typical local Macs (different macOS versions / font sets / node-canvas binaries). Approach A (region-masked hashing) and pixelmatch perceptual-diff both ruled out via empirical research. Recommendation for future regression coverage: component-level assertions on individual drawers (drawHilt, drawMetadata, drawQr) instead of pixel comparison.

v0.16.0

2026-04-30

KyberStation v1.0 public launch. First publicly released version. Ships as a visual blade design tool first: generate your ProffieOS config in the browser, compile with arduino-cli, flash with dfu-util. Web-based flashing is shipped as experimental behind a 3-checkbox disclaimer with mandatory backup acknowledgement. 43 PRs landed since v0.15.0 across 5 waves: critical bug fixes, v1 launch features, overnight refinement, launch-posture lock, and pre-launch UX polish + hardware-fidelity tightening.

Test count at tag: ~5,000 workspace-wide (web ~1,950 / engine 796 / codegen 1,859 / boards 260 / sound 62 / presets 47 + 9 renderer golden-hash). Typecheck clean across all 10 packages.

Added

  • Save Preset v1 (PR #134) — SAVE button in action bar prompts for a name, snapshots config to IndexedDB-backed userPresetStore. "My Presets" section in gallery sidebar with click-to-load, delete, color swatches.
  • Add to Queue v1 (PR #136) — Queue button in action bar. One-click adds current config to the active saber profile's card queue with auto-generated name + toast feedback. Auto-creates "My Saber" profile if none exists.
  • Battery selector with manufacturer-spec warnings (PR #163) — battery catalog with manufacturer-rated discharge specs, dropdown picker in HardwarePanel, automatic safety warning banner when LED count exceeds the selected cell's continuous-discharge headroom. Covers common 18650 + 21700 cells used in Proffie sabers.
  • Gallery grid view as default + detail modal (PR #164) — galleryView field on uiStore (default 'grid'), grid view component + view toggle in GalleryPage header, click-to-open detail modal. +12 contract tests.
  • Profile rename + notes/description (PR #165) — inline rename UI in workbench, optional description field per saber profile, notes panel for private workbench-only commentary, click-to-rename in card preset queue. New renameProfile + setProfileMeta store actions.
  • Unique blade-style thumbnails (PR #157) — gradient + 9 duplicate-thumbnail styles each got a unique hand-authored MiniGalleryPicker trigger. Closes the visual collision where multiple styles shared one thumbnail.
  • Vendor-reality blade length captions + LED divergence warning (PR #159) — Hardware panel callouts when configured LED count diverges from typical vendor practice for a given inch length. Captions match what saber vendors actually ship.
  • Lane A UX cleanup (PR #158) — All States row click-to-set state, gallery nav prune, sidebar group rename for clarity.
  • UX polish batch (PR #160) — density toggle threading, header standardization deltas, BETA badges on experimental surfaces, Crystal panel hidden by default behind opt-in setting (advanced/experimental surface; surfaceable from Settings).
  • Batch preview MiniSaber + Save/Queue button polish (PR #162) — saver/queue action-bar buttons get crisper visual states + the batch-preview thumbnail uses MiniSaber so saved presets render with engine accuracy in the gallery sidebar.
  • CommandPalette audit — 17 new commands (PR #141) — NAVIGATE +9 (every SectionId reachable from ⌘K), EDIT +3 (Save Preset, Add to Queue, Surprise Me), TOGGLE +3 (Pause, Reduce Bloom, Reduce Motion with dynamic title). +18 tests.
  • Audio waveform analysis layer (PR #140) — new audio-waveform layer in AnalysisRail. audioAnalyserStore with first-publisher-wins pattern. AnalyserNode tap after masterGain so mute silences both audio and waveform. Default off, opt-in. +26 tests.
  • 26 compact 24×24 SVG thumbnails (PR #125) for crisp MiniGalleryPicker triggers, with compactThumbnail infrastructure (PR #117).
  • Modulation sampler progress fields (PR #123) — preonProgress / ignitionProgress / retractionProgress added to StyleContext. Closes v1.1 sampler TODOs.
  • FLASH_GUIDE.md (PR #145) — canonical end-user flash guide (~290 lines, 13 sections). Covers dfu-util install, ProffieOS setup, KyberStation config export, arduino-cli compile, DFU mode entry, mandatory firmware backup, vendor-customized board warnings (89sabers, KR Sabers, Saberbay, Vader's Vault), recovery, troubleshooting, FAQ.
  • Renderer-level golden-hash harness (PR #147) — 9 test cases hashing drawWorkbenchBlade output via FNV-1a. Complements engine-level golden-hash (PR #112). canvas@^3.0.0 devDependency for Node 24+.
  • Modern Proffie sound categories (PR #122) — recognize 12 additional sound event categories in the font parser. +19 sound tests.
  • Twist ignition user guide (PR #139) — documents all 18 ignition styles + twist modulator behavior.
  • Backlog ground-truth audit (PR #142) — 5 stale entries cleared from 18 audited.

Fixed

  • Retraction animation progress (PR #132) — FadeoutRetraction + ImplodeRetraction had inverted progress (double-inversion with engine's 1→0 convention, making retractions look like ignitions). +43 retraction tests covering 9 types.
  • BladeCanvas alignment drift (PR #133) — replaced inline piecewise ternary with shared inferBladeInches(). Pointed blade tip fixed by removing tipExtension. Emitter glow when OFF gated on extendProgress > 0.05. +379 alignment test cases.
  • Visual bug batch (PR #161) — RadialGauge arc geometry corrected; modulator plate grid no longer reflows on hover; gallery filter labels truncate cleanly at narrow widths.
  • Wizard button label visibility at desktop (PR #156) — Wizard label was being hidden by the responsive breakpoint cascade at certain desktop widths; surfaces correctly now.
  • Header button standardization (PR #131) — extracted <HeaderButton> primitive, normalized 5 buttons + ShareButton + FPSCounter + UndoRedo to h-7 / rounded-interactive / focus-visible ring.
  • Pause suspends audio (PR #130) — useAudioEngine watches isPaused from uiStore → ctx.suspend() / ctx.resume(). Independent of mute state.
  • Light-theme card export (PR #143) — replaced ad-hoc lightBackdrop boolean with bladeComposite: GlobalCompositeOperation field on CardTheme. LIGHT_THEME = 'source-over', dark themes = 'lighter'. +9 drift-sentinel tests.
  • Wizard polish (PR #137) — fixed stale 132→144 LED default, stale "3 steps" tooltip, added aria-pressed / aria-label on color + vibe buttons. +7 tests.
  • Surprise Me extended (PR #135) — randomizer now picks from full 18-ignition + 12-retraction catalogs, generates 1–2 modulation bindings, adds clashDecay, theme-aware HSL colors.
  • Audio engine fixes — SmoothSwing speed broadcast + hum hot-swap on font change (PR #128), ignition/retraction sound dispatch swap (PR #127), shared mute state via Zustand store (PR #124), Brave FSA flag warning (PR #118).
  • Blend mode tightened (PR #116) — BlendMode narrowed from 5-mode union to single 'normal' literal. The 4 non-normal modes were visualizer-only fakes with no ProffieOS codegen emission.

Changed

  • FlashPanel launch posture (PR #145) — EXPERIMENTAL badge in header. Disclaimer refactored from single boolean to 3-key object (responsibility / backup / recovery). All three checkboxes must be checked before Proceed. Vendor-customized board warning section inside the disclaimer.
  • README beta posture (PR #145) — replaced misleading "validated WebUSB" hardware table with honest "dfu-util first, WebUSB experimental" framing. Credits section rewritten per LAUNCH_PLAN humble-tone guidance.
  • Crystal panel default visibility (PR #160) — Crystal/Share panel hidden by default; surfaceable via Settings opt-in. Treats the Three.js renderer as a power-user feature for v1.0.
  • Deploy plumbing — GitHub Pages basePath + canonical URL + nojekyll wired correctly (PR #154); production CSP allows the inline scripts Next.js requires (PR #155); v0.16.0 release-notes scaffolding (PR #153).

v0.15.0

2026-04-27

Codename: Modulation Routing v1.1 Core. Bundles the Modulation v1.1 Core overnight wave (PRs #57-#65) with the Saber GIF Sprint 1 infrastructure (PR #67), the vertical Saber Card layout polish (PR #36), the v0.14.0 left-rail overhaul recap (PR #68 docs), the launch-prep SEO infrastructure (PR #69), the post-launch backlog index (PR #70), and the blade render rewrite + left-rail overhaul + Saber Wizard hardware step that landed across 2026-04-22 → 2026-04-27.

Hardware-validated on 89sabers Proffieboard V3.9 / macOS / Brave (Chromium WebUSB). Cross-OS + cross-board sweeps remain post-launch per docs/POST_LAUNCH_BACKLOG.md.

Saber GIF Sprint 1 — animated share-card export (2026-04-27)

First cut of animated saber GIF infrastructure per docs/SABER_GIF_ROADMAP.md. Sprint 1 ships two GIF variants from the My Crystal panel: Idle (1 s steady-state shimmer loop) and Ignition cycle (2.5 s PREON → IGNITING → ON-hold → RETRACTING → OFF arc). Output uses the v0.14 workbench renderer (capsule rasterizer + 3-mip bloom), not the simplified gradient that the static Saber Card uses, so the GIF visually matches the live editor preview. Open as PR #67.

Added

  • captureSequence engine helper at packages/engine/src/captureSequence.ts — pure-data multi-frame capture returning Uint8Array[] of LED buffers across N frames. Two modes for Sprint 1: 'idle-loop' (settle to ON, then capture a 1 s window of the steady-state shimmer) and 'ignition-cycle' (full PREON → IGNITING → ON-hold → RETRACTING → OFF, default 2.5 s). Built on the public ignite() / retract() / update() API — no private state poking — so the engine package stays headless. Sibling captureSequenceWithStates returns frames + per-frame BladeState for callers that want to surface state transitions.
  • gif.js Promise wrapper at apps/web/lib/sharePack/gifEncoder.ts — wraps the callback-based gif.js encoder in an async API. Exports encodeGif(canvases, options) (array → Blob), encodeGifStreamed(options, callback) (caller pushes frames to reuse one canvas across the sequence), and setGifEncoderFactory() test seam. Default factory dynamic-imports gif.js so node tests don't try to load the browser-only module.
  • Workbench renderer headless port at apps/web/lib/sharePack/bladeRenderHeadless.ts — 1:1 port of the v0.14 capsule rasterizer + 3-mip bloom chain that lives inline in BladeCanvas.tsx. Exports drawWorkbenchBlade() + getGlowProfile() + ledBufferFrom(). Same plateau / α-feather curve, same mip alphas / blur radii, same per-color glow tuning. Per-color getGlowProfile (red / blue / green / amber / amethyst / cyan / orange / pink / fallback). Leaves BladeCanvas.tsx untouched — workbench risk = zero. Paired-but-parallel until the deferred Phase 4 module-extraction collapses them.
  • renderCardGif(options) orchestrator in apps/web/lib/sharePack/cardSnapshot.ts (additive — renderCardSnapshot for static PNGs is unchanged). Per-frame: clear canvas → repaint chrome (backdrop / header / hilt / metadata / qr / footer) → call drawWorkbenchBlade reading the engine LED buffer directly → gif.addFrame(canvas, copy: true). Reuses one <canvas> across the sequence for bounded memory. Test seams: __setCardFrameRendererForTesting + __setCreateQrSurfaceForTesting.
  • "Save share GIF" button + variant select on CrystalPanel.tsx next to the existing "Save share card" button. Filename pattern: kyberstation-card-<variant>-<presetSlug>-<timestamp>.gif. Disabled state during the 1.5–4 s encode; status line reports final size in KB.
  • gif.worker.js committed at apps/web/public/gif.worker.js (16 KB, copied from node_modules/gif.js/dist/) so the encoder's web worker resolves at /gif.worker.js under Next.js's static asset path.

Defaults

  • DEFAULT_GIF_OUTPUT_WIDTH = 640 (down from layout default 1200) — the workbench renderer produces far more colors per frame than the static gradient (bloom + plateau core + per-LED interpolation), so gif.js's 256-color palette quantization fights harder; the smaller default lands both variants under cap.
  • 18 fps for both variants. Tuning lands idle ≈1.7 MB / ignition ≈4.3 MB at default settings — under the brief's < 2 MB idle / < 5 MB ignition targets. Callers can override width / fps / quality for hosted-use higher-fidelity renders.

Dependencies

  • Adds gif.js@^0.2.0 + @types/gif.js to apps/web/package.json.

Tests

  • engine: 722 → 740 (+18 new captureSequence.test.ts covering frame-count math, deterministic buffers across calls, idle-loop steady-state lit-ness, ignition-cycle state-machine arc with PREON enabled / skipped, and full validation surface).
  • web: 1,041 → 1,069 (+28 across gifEncoder.test.ts (15) and renderCardGif.test.ts (13) — Promise contract, frame count = fps × duration_seconds, abort rejection, console-clean orchestration, QR texture disposal on encoder abort).

Hard constraints honoured

  • apps/web/lib/sharePack/card/* — untouched (PR #36 turf).
  • apps/web/components/editor/BladeCanvas.tsx — untouched (workbench risk zero).
  • apps/web/components/editor/routing/*, BoardPicker, useBoardProfile, useClickToRoute — untouched (modulation turf).
  • packages/engine/src/modulation/* — untouched (locked types).
  • Engine package stays headless — captureSequence is pure data, no DOM.
  • Worker-encoded GIFs — gif.js spawns 2 web workers; main thread doesn't block during encode.

Open / pending

  • Sprint 2 — per-variant ignition / retraction picker GIFs (19 + 13 thumbnails) + build script + MiniGalleryPicker wiring.
  • Sprint 3 — Tier 2 marketing showcases (style grid, colour cycle, lockup loop).
  • Sprint 4 — Tier 3 effect-specific + hilt-only + UI walkthrough GIFs.
  • Phase 4 module extraction (separate, lower-risk PR): collapse bladeRenderHeadless.tsBladeCanvas.tsx inline pipeline into one shared module under lib/blade/*; migrate MiniSaber, card/drawBlade.ts, etc. to call it.
  • The roadmap doc itself (docs/SABER_GIF_ROADMAP.md) lives on the still-open PR #56 — Sprint 1 status flip should land there once it merges, or via a follow-up doc commit on main.

Modulation Routing v1.1 Core — overnight wave (2026-04-27)

Eight PRs landed overnight on top of v0.14.0, completing the v1.1 Core scope from docs/MODULATION_ROUTING_ROADMAP.md. Three parallel-agent phases (Phase 1: 4 worktree agents, Phase 2a: 3 worktree agents, Phase 2b: Wave 5 salvaged after agent stalled). All CI-green, all merged. No tag cut yet — pending hardware validation. Candidate v0.15.0.

Added

  • All 11 modulators surface as plates (was 5 in v1.0). PR #57 unblocks twist / battery / lockup / preon / ignition / retraction with bespoke CSS-keyframe live-viz glyphs. Plate grid lg:grid-cols-4 xl:grid-cols-6 for 11-plate readability.
  • Reciprocal hover highlight — hovering any parameter row in ParameterBank now lights up every modulator plate that drives it (PR #61). New uiStore.hoveredParameterPath field. Multi-driver case lights multiple plates simultaneously. Replaces the 2026-04-20 1:1 placeholder mapping.
  • Per-binding expression editing — magenta fx button on every expression-bound row in BindingList opens ExpressionEditor preloaded with the existing source for in-place iteration (PR #63). Bare-source rows leave the slot empty so column alignment stays consistent.
  • True drag-to-route — HTML5 drag-and-drop layered on click-to-route as the primary mouse/trackpad gesture, Vital / Bitwig style (PR #64). Plates are draggable; slider rows are drop targets with identity-color visual cue. New useClickToRoute.dragBind(modulatorId, targetPath) action. MIME type application/x-kyberstation-modulator exported as a constant. Click-to-route preserved as keyboard / a11y fallback.
  • AST-level template injection in codegen (PR #60) — composeBindings(ast, mappable) walks the style AST and grafts each mapped binding's astPatch into the slot identified by targetPath. v1.1 Core ships the shimmer-Mix slot pattern (Mix<Int<N>, X, Y>) used by every base-style. generateStyleCode rewired: mapBindings → applyModulationSnapshot baseline → buildAST → composeBindings → emitCode + v1.1 comment block distinguishing live / snapshotted / deferred / skipped bindings. Snapshot path retained for unmappable bindings as the always-flashable fallback.
  • 5 new starter recipes (PR #58): heartbeat-pulse (abs(sin(time*0.002))), battery-saver (clamp(1-battery, 0, 0.5)), idle-hue-drift, sound-driven-hue, twist-driven-saturation. Recipe library 6 → 11. V1_0_RECIPES 8; V1_1_EXPRESSION_RECIPES 3.
  • 7 new user-guide pages (PR #59) — recipes.md / combinators.md / modulators.md / expressions.md / canon-patterns.md / troubleshooting.md / sharing.md. ~5,400 new words, voice-matched to the existing quick-start. Honest-scope-tagged (v1.0 / v1.1+ / v1.2+).
  • MODULATOR_DRAG_MIME_TYPE constant exported from useClickToRoute (PR #64).
  • MODULATOR_DRAG_MIME_TYPE constant + dragBind action in useClickToRoute (PR #64).
  • applyModulationSnapshot.ts — new onlyBindingIds filter + CommentBlockExtras shape with mappedBindings / deferredFromMapping for the v1.1 comment block (PR #60).

Changed

  • generateStyleCode flow rewired to a 6-step pipeline (PR #60): mapBindings → applyModulationSnapshot baseline → buildAST → composeBindings (overwrites mappable slots with live drivers) → emitCode → v1.1 comment block. Backward-compatible — configs without modulation payload short-circuit to byte-identical v0.14.0 output.
  • BindingList row grid picked up a 28-px fx-slot column (PR #63) — bare-source rows render an empty placeholder so the columns stay aligned across mixed lists.
  • Modulator plate grid bumped from lg:grid-cols-5 to lg:grid-cols-4 xl:grid-cols-6 to fit 11 plates readably (PR #57).

Tests

  • codegen: 1,842 → 1,859 (+17 new in composeBindings.test.ts, PR #62).
  • web: 1,025 → 1,041+ (+9 dragBind in useClickToRoute.test.ts from PR #64; +7 in bindingListEditExpression.test.tsx from PR #63; reciprocal-hover regression coverage from PR #61).
  • presets: 29 → 47 (+18 in recipes.test.ts from PR #58).

Backfill

  • PR #62 — composeBindings test backfill. PR #60 (Wave 6) shipped the AST composer without test coverage due to a worktree-environment file-revert issue mid-agent. PR #62 adds 17 tests across 9 groups (pure-function invariants, single binding, breathing heuristic, multi-binding, deferred fall-through, purity / idempotency / structural sharing, result shape, generateStyleCode integration, snapshot/live boundary).

Salvage

  • PR #64 — Wave 5 (drag-to-route) post-stall recovery. Background agent stalled post-implementation but pre-commit. Parent session re-ran typecheck + tests against the worktree (clean, 1041 web tests passing), committed the agent's exact code, pushed, and opened the PR. Code is the agent's; commit message + PR shape are the parent session's.

Open / pending

  • Wave 7 — Kyber Glyph v2 modulation round-trip (encoder body)
  • Wave 8 — Button routing sub-tab + aux/gesture-as-modulator plates (L scope, separate session)
  • Wave 10 — Hardware validation + V2.2 modulation flash + cut v0.15.0 tag (hardware-gated)
  • Wave 6 follow-on — composer slot expansion to per-channel RGB + timing scalars (v1.2 candidates per PR #60 body)
  • Manual visual verification of all 8 PRs in the live editor

Blade render rewrite — capsule rasterizer + additive composite (2026-04-27)

Major rework of the blade preview pipeline, landed on feat/blade-layers-debug. Collapses the prior body + separate tip cap + parallel offscreen mirror into a single per-pixel capsule rasterizer that's the source of truth for blade geometry. Pipeline goes from 14 passes → 13, ~140 lines of body/cap matching code removed.

  • Capsule rasterizerrasterizeCapsuleToOffscreen walks the capsule's bounding box per-pixel, samples LED color via axial position with linear interpolation between adjacent LEDs, applies a feathered Gaussian-α radial profile, and lerps toward a luma-driven white-shifted core. Capsule is fixed at the full configured blade length — retraction is rendered via per-LED brightness (engine-driven), not geometry truncation, so the tube doesn't physically shrink during retract. Replaces 4 parallel render paths (offscreen body + offscreen cap + visible body + visible cap) with one.
  • Sharp + soft offscreen split — sharp offscreen feeds the visible body composite; soft offscreen (sharp + diffusion blur) feeds the bloom mips. Decouples "crisp body silhouette" from "diffuse halo source" so cranking diffusion doesn't shorten or soften the visible blade.
  • Pass 12 additive blend — body capsule now ADDS to the bloom underneath via lighter blend instead of occluding it via normal alpha. Eliminates the visible "body sitting on top of halo" seam that the prior normal-blend produced at the body's α-decay zone. Physically correct compositing for emissive surfaces — light adds, doesn't replace.
  • Feathered Gaussian α profile — rasterizer's α curve is now a smooth bell shape (1.0 at center, 0 at rim) with no plateau. Combined with the additive composite, the body and surrounding bloom are continuous expressions of the same emission. No detectable boundary between body silhouette and halo.
  • Tip axial extension — geometric capsule rim sits 0.15 × radius past the LED endpoint so the feathered α can decay to 0 at the rim while visible-bright pixels reach EXACTLY the configured blade length (verified to the pixel for a 40" blade — body terminates at the 40" grid mark, bloom continues past as natural surrounding halo).
  • Body height rebalanced (BLADE_CORE_H 26 → 32) — paired with the feathered α curve to give a clearly-defined bright tube proportional to the BLADE itself (~1" real neopixel diameter), not the hilt graphic. Hilt is treated as a visual placeholder; blade width stays consistent regardless of hilt sizing.
  • Hilt-tuck + clipped body — capsule extends coreH left of the hilt edge so its rounded LEFT cap sits behind the hilt (invisible to the user, but bloom mips still see it and produce halo into the hilt area). Visible body composite is clipped to x ≥ bladeStartPx so the body itself doesn't paint over the hilt's metallic surface. Hilt drawn before bloom so additive bloom mips spill onto the metal naturally — preserves the "lit blade illuminating the metal" look.
  • Per-LED axial linear interpolation — eliminates hard vertical seams between bright and dim adjacent LEDs (prior hard quantization showed ~17 luma steps every 6 px at a Stripes-style transition; max per-pixel delta now ~3 luma). Implements axial polycarbonate diffusion: light from each LED bleeds into its neighbors along the tube length.
  • +25% white-core exposure boostledCoreWhiteAmount multiplied 1.25× (clamped to 1.0). White plateau now reaches pure white (luma 250+) instead of stopping at the per-color asymptote (0.82–0.95). Iconic blown-out tube look for any LED color, not just white blades.
  • Drops tests/blade/endpointSeeds.test.ts — sentinel for the v0.14.0 Phase 1 endpoint-seed widening (glowCapRadius, emGlowR), constants the capsule unified out of existence.

Performance: 120 FPS at 1600×1000 viewport / DPR 2. Per-pixel rasterization adds modest CPU cost but stays well within budget.

Status: shipped on feat/blade-layers-debug, NOT yet merged to main. Branch is in a clean checkpoint state; further tweaks to white-shift/bloom layering deferred.

Left-rail overhaul (2026-04-25)

Replaced the multi-tab + multi-column workbench with a unified Sidebar + MainContent shell driven by a single uiStore.activeSection slot. Page-tabs nav, the DesignPanel pill bar, the macro-knob PerformanceBar, and the swipe-driven tablet tab UI all retired together. Seven PRs (#47-#53) shipped in one day via parallel-agent dispatch.

Final desktop shape. The header keeps its utility chrome (logo, Share, FPS, Sound, Docs, ⌘K, Wizard, Settings) but loses the Gallery/Design/Audio/Output tabs — the sidebar absorbs them. Inspector stays left of the canvas as the always-visible "Quick Controls" surface; RightRail (STATE + ANALYSIS) stays right; new Sidebar (~280px) + MainContent split fills the panel area below the canvas. Tablet (600–1023px) uses the same shell at 240px sidebar width. Mobile is intentionally unchanged in this sprint — a small-screen drawer/bottom-sheet UX pass is owned for a follow-up.

Sidebar groups (collapsible, localStorage-persisted): GALLERY → /gallery · APPEARANCE (Blade Style · Color) · BEHAVIOR (Ignition & Retraction · Combat Effects · Gesture Controls) · ADVANCED (Layer Compositor · Motion Simulation · Hardware · My Crystal) · ROUTING BETA (board-gated) · AUDIO · OUTPUT.

Quick Controls (Inspector left rail). GALLERY tab retired. Single-surface stack: Surprise Me + Undo · 8 canonical color chips (Blue / Red / Green / Yellow / Purple / Orange / White / Cyan) + Custom (jumps to deep Color section) · compact ignition + retraction MGP pickers with inline ms field · the existing 7 ParameterBank sliders. Every fast-access section is a thin view over the same store as the deep sidebar section, so changes propagate both directions.

Three panel merges (#47): - Colors + Gradient Builder → unified ColorPanel (channel selector at top; gradient region only renders for the Base channel) - BladeHardwarePanel + PowerDrawPanel → HardwarePanel (config inputs on top, live power readout below the divider with <StatusSignal> headroom indicator) - ModulatorPlateBar + BindingList → RoutingPanel (plate bar + binding count divider + active bindings list)

SettingsModal reorganized to 3 tabs (#47): Appearance (Aurebesh Mode · Display · Row density) · Behavior (UI Sounds · Effect auto-release · Keyboard Shortcuts · Feedback) · Advanced (Performance Tier · Layout). The "Performance Bar" toggle was deleted alongside the bar itself.

Other shipped pieces. Motion Simulation restored under sidebar Advanced (#49) after PR 1 dropped its mount point. ⌘1–⌘4 digit nav rewired from setActiveTabsetActiveSection; ⌘5 still toggles the All States takeover. KeyboardShortcutsModal now surfaces ⌘1–⌘5, ⌘K, ⌘Z, ⌘⇧Z as first-class Editor rows (#49). The 19-ignition + 13-retraction style tables that were copy-pasted across three sites moved to a shared lib/transitionCatalogs.ts (#50). The previously-inert Custom color chip now jumps to the deep Color sidebar section (#51). Tablet shell migrated to Sidebar + MainContent (#52). PerformanceBar.tsx + MacroKnob.tsx + QuickMacroPreview.tsx finally deleted (#53) — the surviving shiftLedColor helper moved to a tiny lib/shiftLight.ts for ShiftLightRail's exclusive use.

Test count: 1030 / 1030 passing across 58 files (was 1044 across 59 pre-overhaul; net change reflects the deletion of the MacroKnob test suite + the addition of QuickColorChips + QuickTransitionPicker + Inspector regression tests). Typecheck clean across all 10 workspace packages. Verified end-to-end at desktop (1600×1000) and tablet (900×1024) viewports.

Deferred (post-overhaul follow-ups, not blocking a v0.15 tag): mobile shell migration (needs UX call on drawer vs bottom-sheet), inline custom-color popover, and compactThumbnail field on transition catalog entries to author crisp 24×24 MGP triggers instead of scaled-down 100×60 SVGs.

Saber Wizard — hardware step (2026-04-22)

Added a new first step to the Saber Wizard so newcomers tell the app about the saber they actually own (blade length + board) before picking aesthetic. The 3-step archetype/colour/vibe flow shifts to steps 2/3/4 and is otherwise unchanged.

  • Blade length picker — 6 tiles (20"/24"/28"/32"/36"/40"). LED counts mirror BLADE_LENGTH_PRESETS in the engine package; selection writes BladeConfig.ledCount so every per-LED surface in the editor (BladeCanvas + PixelStripPanel + RGBGraphPanel + state-grid takeover) renders the chosen length 1:1.
  • Board picker — 5 tiles (Proffie V3 / V2 / CFX / GH V4 / GH V3) with 3-tier compatibility chips built on the existing <StatusSignal> primitive (paired colored glyph + label, colorblind-safe):
  • VERIFIED (green ✓) — Proffie V3, the only board hardware-validated end-to-end (per the 2026-04-20 Phase A/B/C entry above).
  • UNTESTED (amber ▲) — Proffie V2. Code path identical to V3, hardware testing pending. Community hardware reports welcome.
  • REFERENCE (red ✕) — CFX / GH V4 / GH V3. Different firmware ecosystems entirely; the editor + visualizer work but the generated config.h won't run on these boards.
  • A mini legend strip next to the "Board" heading shows what each color means.
  • Selection writes boardType to the active SaberProfile (or auto-creates a "My Saber" profile if none exists).
  • "Skip for now" advances to the archetype step without writing anything to the blade config or profile — for users who want to dive straight into design and configure hardware later via the Profile + Code panels.
  • Initial focus lands on the currently-selected length tile (matches selection rather than always-first), so keyboard users start where the visual selection is.

10 new contract tests in apps/web/tests/saberWizardOptions.test.ts guard the LED-count ↔ inferBladeInches mapping, the V3-only-verified tier assignment (will fail loudly when V2 gets hardware-validated, prompting a tier bump), and the storeValue strings that CodeOutput.tsx maps back to proffieboard_v{2,3}. 637 web tests pass.

WebUSB flash — hardware validation (2026-04-20)

Phases A + B + C all green on Proffieboard V3.9 (89sabers) + macOS 15 + Brave. Connect → dry-run → real flash → post-write verify → recovery re-flash — full clean pass. Blade ignites blue on the first power press after replug; USB serial enumerates as /dev/tty.usbmodem*; audio DAC active (ProffieOS voice pack announces "SD card not found" / "font not found").

Three real DFU protocol bugs fixed that 576 passing mock tests had missed. Real STM32 DfuSe bootloader correctly returned STALL where the mock was too permissive:

  • DfuSeFlasher.verifyFlash: setAddressPointer leaves the device in dfuDNLOAD_IDLE, but UPLOAD requires dfuIDLE. Added abort() between the two.
  • DfuSeFlasher.flash (manifest step): after UPLOAD verify the device sits in dfuUPLOAD_IDLE, but the manifest's zero-length DNLOAD requires dfuIDLE. Added abort() before the manifest download.
  • DfuSeFlasher.waitForManifestComplete: STM32 resets the USB bus as part of manifest (bitManifestationTolerant=0); the resulting controlTransferIn failure surfaces as a raw DOMException, not our DfuError. The old catch only swallowed DfuError, so successful flashes showed a red error banner. Now any error during the post-manifest poll is treated as success.

Plus two supporting fixes uncovered while building firmware to validate against:

  • firmware-configs/v3-standard.h: legacy InOutTrL<TrWipe<300>, TrWipeIn<500>, Blue> no longer compiles against current ProffieOS master — bare Blue returns RGBA_nod which doesn't convert to OverDriveColor. Replaced with the modern StyleNormalPtr<Blue, WHITE, 300, 500> factory (same visual result).
  • .github/workflows/firmware-build.yml: Linux runners are case-sensitive, so checking out ProffieOS into proffieos/ broke the arduino-cli compile <sketch-dir> contract (ProffieOS ships ProffieOS.ino). Renamed the checkout path to ProffieOS/.

Validated hardware scope: Proffieboard V3.9 on macOS + Chromium. Brave is Chromium-based, and Chrome/Edge/Arc share Chromium's WebUSB implementation so they should behave identically. Windows, Linux, Proffieboard V2, and V3+OLED are untested; community hardware reports welcome via the hardware_report issue template.

Followups:

  • ~~Tighten MockUsbDevice to enforce the three DFU state-machine rules~~ — done (same session). strictState + resetAfterManifest options added; three regression tests added (one per bug), each verified to fail if the corresponding fix is reverted. 579 tests pass.
  • Cross-OS sweep: Windows + Linux hardware smoke-tests before promoting the feature to "validated on all supported configurations".
  • Cross-board sweep: Proffieboard V2.2 and V3+OLED hardware smoke-tests.

Full details in docs/HARDWARE_VALIDATION_TODO.md § Phase C.

- v0.11.1 — Design Review Polish Pass (shipped): alert-color discipline, skeleton + error-state coverage, color-glyph pairing for accessibility, CHANGELOG + README assets, housekeeping - v0.11.2 — Color Naming Math (shipped): three-tier algorithmic naming (landmark + modifier + coordinate-mood) expanding ~120 curated names into 1,500+ HSL coverage - v0.11.3 — Modular Hilt Library (shipped): 33 reusable line-art SVG parts composed into 8 canonical hilt assemblies (Graflex, MPP, Negotiator, Count, Shoto Sage, Vented Crossguard, Staff, Fulcrum), authored across 3 parallel artist-agents on top of a strict-typed composer + HiltRenderer with horizontal / vertical orientation. 8 new SVG hilt options added to the editor's Hilt picker (marked with ✦) - v0.12.0 — Kyber Crystal Three.js renderer (shipped): full 3D crystal component with PBR materials, 5 procedural Forms, bleed + heal + first-discovery animations, scannable QR embedded, card snapshot pipeline - v0.15.0 — Preset Cartography (planned): multi-agent preset expansion across deep-cut lanes (Prequel/OT/Sequel, Legends/KOTOR, Clone Wars, Mando/Ahsoka, cross-franchise) - v0.16.0 — Multi-Blade Workbench (planned): channel-strip UI for editing dual-blade / saberstaff / crossguard sabers (glyph format already supports multi-blade from v1)

(v0.13.0 — Launch Readiness — shipped via PR #31; v0.14.0 slot open for reassignment after deprecating the former "Kyber Forge" ultra-wide layout concept, which is redundant now that OV11's drag-to-resize handles cover the ultra-wide use case.)

Branch protection — server-side active

After the KyberStation owner upgraded to GitHub Pro (2026-04-17 afternoon), pnpm run branch-protection:setup applied the main-protection ruleset (id 15217927) on refs/heads/main:

- non_fast_forward blocks force-push to main - deletion blocks main-branch deletion - pull_request (0 approvals required) blocks direct pushes — all changes must go through a PR - required_status_checks: build-and-test requires CI green before merge

Client-side .githooks/pre-push remains active as defense-in-depth.

Deferred items (documented, awaiting dedicated pickup)

- Hardware validation of WebUSB flash against real Proffieboard V2.2 and V3.9 — see docs/HARDWARE_VALIDATION_TODO.md - Real ESLint enforcement across packages (stub lint scripts currently) - CANONICAL_DEFAULT_CONFIG drift-sentinel test pattern - Shared <HiltMesh> extraction between BladeCanvas3D.tsx and CrystalRevealScene.tsx - Crystal Vault panel (scanned-crystal collection) - Re-attunement UI for visual-version upgrades - Favicon replacement from crystal snapshot pipeline - SHARE_PACK.md §4 size-estimate table refresh (current doc understates max glyph size; real measurements from PR #20 hit ~490 chars at max)

See ~/.claude/plans/declarative-strolling-dragonfly.md for the orchestration plan that scopes the current sprints, and docs/SESSION_2026-04-17.md Part 2 for the full session summary.

Modulation polish + a11y clean (2026-04-23 late — untagged; candidate v0.14.1 or v0.15.0 once hardware-validated)

Two PRs shipped on top of v0.14.0 (PR #41 + PR #42). Items ordered by PR.

PR #41 — modulation follow-up (merge commit bd9bb7b):

- Generated code now reflects modulation. generateStyleCode threads a new applyModulationSnapshot helper that walks config.modulation.bindings, snapshots each to its current value via computeSnapshotValue, bakes the results into the config at each binding's target path (shallow-clone-on-write, no mutation), and prepends a BETA-labeled comment block listing every applied + skipped binding. Opt out via { comments: false } on the preset-array code path to avoid one-comment-per-preset clutter in the full config.h. Expression bindings currently snapshot; v1.1 Core adds AST-level template injection for Scale<SwingSpeed<>, ...> + friends. - Hover wire-highlighting + bound-param stripe. Three priority- stacked visual states on every slider label in ParameterBank: ARMED (click-to-wire) > HOVERED (this plate drives this param) > BOUND (some binding targets this param — persistent left-edge identity-color stripe). Identity colors propagate via BUILT_IN_MODULATORS descriptors. - Inline BoardPicker chip in StatusBar between Profile and Conn. BOARD · PROFFIE V3.9 · FULL; click opens the modal picker. Reactive across capability-sensitive UI. - ExpressionEditor — v1.1 Core math-formula UI. fx button on every SliderControl opens a 380-px popover with auto-focused textarea, live peggy parse status (✓ Valid / ✕ with error location + message), 5 starter-idiom chips (Breathing / Heartbeat / Battery dim / Swing doubled / Loud OR fast), ⌘+Enter shortcut, Escape/outside-click dismiss. Apply creates a binding with source: null, expression: { source, ast }, combinator: 'replace', amount: 1.0. BindingList distinguishes expression bindings from bare-source with an fx label in status-magenta + full-source hover tooltip. - Color-contrast fix across 9 canvas themes + the root default. --text-muted bumped +40 each channel (106 110 120 → 146 150 160 for Deep Space, equivalent deltas for the other 8 themes). Fixes 82 axe-core color-contrast violations concentrated on muted-text surfaces in the modulation UI.

PR #42 — a11y clean + first expression recipe (merge commit c0a92c4):

- Zero axe-core WCAG 2 AA violations at desktop (1600×1000, 30 passes) AND mobile (375×812) viewports on /editor. Closes the P29 launch blocker carried from v0.13.0 readiness. - MobileTabBar: dropped role="tablist" / role="tab" — semantically this is route navigation, not a tab interface; aria-current="page" handles the active state. - AppShell mobile tablist: scoped role="tablist" to an inner wrapper so the collapse toggle + dot indicators become siblings of the tablist, not children (fixes aria-required-children). - AppShell tab aria-controls: replaced per-tab panel IDs with a single stable id="mobile-panel"; dropped when showPanel is false and paired with aria-expanded. - MiniGalleryPicker: role="listbox"role="group" (children use role="button", not role="option"). - Cleared the final 5 contrast issues: DesignPanel BETA chip opacity-70, ColorPanel preset subtitle text-accent/70, PerformanceBar page tabs + SaberProfileManager source badge rgb(var(--text-muted) / 0.65). - Breathing Blade — first expression-based starter recipe. sin(time * 0.001) * 0.5 + 0.5 → shimmer · replace · 100%. AST hand-built inline (can't import parseExpression across the .npmrc hoisted boundary per CLAUDE.md decision #1). Test split: V1_0_RECIPES (5) vs V1_1_EXPRESSION_RECIPES (1). Presets test count 29 → 40. The ProffieOS emitter's existing matchSinBreathingEnvelope heuristic recognizes this exact shape so the flashed blade will breathe live on hardware via Sin<Int<period>>.

Tests delta since v0.14.0: codegen +15, presets +11, web unchanged (new UI work covered by visual QA + axe audit, which itself ran clean on both viewports).

PR #43 — docs catch-up (merge commit b98af51): CLAUDE.md + CHANGELOG updates for PR #41 + PR #42. No code touched.

PR #44 — quick-start illustrations (merge commit bfcdbf6): Three hand-authored animated SVGs replace the [gif-placeholder] markers in docs/user-guide/modulation/your-first-wire.md:

- first-wire-step-1.svg (7.0 KB) — five-plate bar with per-plate live viz (swing needle, angle tilt, sound VU, time sweep, clash flash). - first-wire-step-2.svg (3.4 KB) — SWING plate armed, siblings dimmed, "◎ swing armed" banner below. - first-wire-step-3.svg (7.1 KB) — two-column frame with an animated dashed wire connecting plate → shimmer slider, plus the binding row "SWING → Shimmer · add · 60%".

~17.5 KB combined. Native SVG animation — no JS. Each has <title> + <desc> for screen readers. Matches editor color tokens exactly. An inline HTML comment at the bottom of the markdown documents the replacement pass for future screen-recorded GIFs.

v0.14.0

2026-04-23

Added — Modulation Routing v1.0 Preview (BETA)

First release of the headline Modulation Routing feature. Turns KyberStation from a static blade picker into a "blade instrument" — users wire live signals (swing / sound / angle / time / clash) to any numeric blade parameter with a combinator + amount; bindings drive the blade render in real time at 60 FPS.

Merged via PR #35. Marked BETA in the new ROUTING pill; full v1.1 Core (math formula UI + remaining 6 modulators + drag-to-route + Kyber Glyph v2 sharing + V2.2 flash + button routing) lands ~3 weeks post-launch per docs/MODULATION_ROUTING_ROADMAP.md.

UI additions:

  • New ROUTING pill in the DesignPanel (hidden on non-Proffie boards via the Board Capability System).
  • ModulatorPlateBar — 5 plates (swing / sound / angle / time / clash) with live CSS-driven identity-color viz (--mod-swing through --mod-retraction). Click to arm, Escape to disarm.
  • AddBindingForm — dropdown source/target/combinator picker + amount scrub. Fast form-based binding creation alternative to click-to-route.
  • RecipePicker — 5 one-click starter recipes: Reactive Shimmer (swing → shimmer), Sound-Reactive Music Saber (sound → baseColor.b), Angle-Reactive Tip (angle → emitterFlare), Clash-Flash White (3-binding clash → baseColor.{r,g,b}), Twist-Drives-Hue (twist → colorHueShiftSpeed).
  • BindingList — rows with source → target arrow, combinator dropdown, amount scrub (0–100%), bypass toggle (▶/⏸), remove button. Empty state nudges toward the plate workflow.
  • Click-to-route wiring in every ParameterBank.SliderControl — armed plate tints all slider labels in the modulator's identity color; clicking wires source → targetPath as a new binding with the v1.0 defaults (add combinator, 60% amount).
  • Inline BoardPicker chip in StatusBar between Profile and Conn — BOARD · PROFFIE V3.9 · FULL; click opens modal picker with all 6 boards + color-coded status chips.
  • BoardPicker modal + inline variants (apps/web/components/shared/BoardPicker.tsx) — 6 boards (Proffieboard V3.9 full, V2.2 partial, Golden Harvest V3 mirrors V3.9, CFX / Xenopixel / Verso preview-only), status chips, hardware-validated ✓ badge on V3.9.

Engine additions (packages/engine/src/modulation/):

  • registry.ts — 11 built-in ModulatorDescriptors with identity colors, ranges, units, one-pole smoothing coefficients, clash latch metadata.
  • sampler.ts — per-frame value extraction + one-pole smoothing + clash latching with decay. Returns a frozen SamplerState consumed by applyBindings.
  • applyBindings.ts — pure binding composer. Walks bindings in authoring order, composes with the 5 combinators (replace / add / multiply / min / max), sanitizes NaN / ±∞ / out-of-range values against per-parameter clamp data.
  • parser.ts + grammar.peggy — peggy 4.2 expression parser for the math-formula mini-language (arithmetic + 10 built-in functions min / max / clamp / lerp / sin / cos / abs / floor / ceil / round). Runtime-compiled; module-level parser singleton. Drift-sentinel fixtures: 63 accept / 61 reject.
  • evaluator.ts — tree-walk interpreter over ExpressionNode. Missing modulator IDs fall through to 0; NaN / Infinity propagate (sanitizer upstream of the engine cleans up).
  • BladeEngine.update() now samples modulators + applies bindings before style rendering; persistent _samplerState for smoothing continuity; setParameterClampRanges(map) API for the web layer to push per-parameter range constraints; getSamplerState() accessor for future UI viz.
  • packages/engine/src/index.ts top-level barrel re-exports the full modulation subsystem so consumers (codegen, web, presets) can import from @kyberstation/engine directly.

Codegen additions (packages/codegen/src/proffieOSEmitter/):

  • mapBindings.ts — Option B+ export semantics. Maps each binding to equivalent ProffieOS templates where possible (Scale<SwingSpeed<>>, Sin<Int<>>, SoundLevel<>, etc.); returns a snapshotValue fallback for unmappable bindings (expressions in v1.0, modulator chains, enum-targeted bindings). MAP_BINDINGS_REASONS constant exports the exact user-facing reason strings for the Flash dialog's per-binding review. Mirrors engine types inline with a drift-sentinel test per the CLAUDE.md decision #1 pattern.

Web library additions (apps/web/lib/):

  • parameterGroups.ts — 77 modulatable BladeConfig fields enumerated with path / displayName / group / range / default / unit / isModulatable. 5 groups (color / motion / timing / style / other). Drives drop-target validation, parameter clamping, and the AddBindingForm's target picker.
  • boardProfiles.ts — 6 board profiles: Proffieboard V3.9 (full, hardware-validated) / V2.2 (partial; modulation disabled in v1.0 until V2 hardware validation) / Golden Harvest V3 (mirrors V3.9) / CFX / Xenopixel / Verso (last three: preview-only, modulation hidden). Each profile declares hardware (flash size, max LEDs, buttons, OLED), firmware, feature support (styles / effects / ignitions / retractions / SmoothSwing / prop files), modulation capability flags (supported modulators + functions + max bindings + soft/hard warnings + chains / step-seq / envelope-follower), and template emission ceilings. Helpers: getBoardProfile / canBoardModulate / isParameterModulatableOnBoard / DEFAULT_BOARD_ID.
  • propFileProfiles.ts — 4 prop file profiles (Fett263 most-flexible, SA22C, BC Button Controls, Default Fett) with button event + gesture event vocabularies.

Store additions:

  • bladeStore.config.modulation?: ModulationPayload (optional, backward-compatible) + addBinding / removeBinding / updateBinding / toggleBindingBypass / clearAllBindings actions.
  • uiStore.armedModulatorId: string | null + setArmedModulatorId — click-to-route arm state, separate from selectedLayerId (which drives the layer-config panel).

Hooks:

  • useBoardProfile() — reactive board selection backed by localStorage.kyberstation.board.selected with cross-tab sync via a board-profile-changed CustomEvent dispatched on the window.
  • useClickToRoute(options?) — click-to-route state machine. Arm / disarm / onParameterClick returning { kind: 'created' | 'ignored', reason? }. Escape-key disarm listener. Board-capability gating via isParameterModulatableOnBoard.
  • useBladeEngine() — now pushes the parameterGroups.ts clamp ranges to the engine on init via setParameterClampRanges().

Starter recipe fixtures (packages/presets/src/recipes/modulation/):

  • 5 shipped for v1.0 (bare-source bindings, no expressions): Reactive Shimmer, Sound-Reactive Music Saber, Angle-Reactive Tip, Clash-Flash White (3 bindings), Twist-Drives-Hue. Top-level exports from @kyberstation/presets so the web layer imports directly.

CSS tokens (apps/web/app/globals.css):

  • 11 --mod-* identity colors (one per built-in modulator) as full-color CSS variables, with paired --mod-*-rgb triples for alpha compositing. Swing = electric blue, Angle = teal, Twist = violet, Sound = magenta, Battery = green, Time = gold, Clash = near-white, Lockup = amber, Preon = ice-blue, Ignition = cyan, Retraction = warm-red.

Docs:

Changed

  • apps/web/components/editor/DesignPanel.tsx — added the 4th pill group (Routing) with a BETA chip. The pill is hidden entirely when the current board doesn't support modulation (filtered via canBoardModulate(boardId)); state-rescue logic uses useEffect rather than setState-in-render.
  • apps/web/components/editor/layerstack/LayerStack.tsx — no changes on main but the modulation work consumes the existing ModulatorRow viz kind convention (the SmoothSwing plate prototype from 2026-04-20).
  • packages/engine/package.json — added peggy@^4.2.0 runtime dependency (~25KB gzipped).
  • apps/web/lib/boardProfiles.ts — cleanup pass by Agent P5 replaced 28 lines of inline-mirrored BuiltInModulatorId / BuiltInFnId unions with direct imports from @kyberstation/engine now that the top-level engine barrel re-exports the modulation subsystem. 21 drift-sentinel .toContain string checks in the tests replaced with compile-time assignment assertions.
  • packages/presets/src/index.ts — top-level barrel now re-exports MODULATION_RECIPES + friends.

Fixed

  • Infinite render loop in ROUTING panel. useBladeStore((s) => s.config.modulation?.bindings ?? []) returned a new empty-array reference every render, triggering Zustand's store-rerender loop. Fixed with a module-level EMPTY_BINDINGS: readonly SerializedBinding[] = [] constant as the fallback.
  • setState called during render in DesignPanel's board-gating rescue. Moved the setActiveGroup('appearance') call into a useEffect hook.
  • Card-preview dev page config.name coercionstring | undefinedstring via ?? 'Untitled' fallback (surfaced during modulation CI run; pre-existing issue from the parallel Kyber Glyph v2 session's dev page).

Tests (+386)

| Package | Before v0.14.0 | After v0.14.0 | |---|---|---| | engine | 513 | 714 | | codegen | 1,323 | 1,347 | | web | 749 | 890 | | presets | 9 | 29 |

Full workspace typecheck clean. CI green on every push.

v0.11.2

2026-04-17

Changed

- Color naming system rewritten as three-tier algorithmic model in new apps/web/lib/namingMath.ts. Replaces the 121-entry flat lookup in saberColorNames.ts with: - Tier 1 — Landmarks (~147 curated Star Wars character/location names) at exact HSL points. Every landmark from the original dataset preserved verbatim - Tier 2 — Modifier grammar (10 modifiers: Pale, Deep, Vivid, Muted, Dawn-, Dusk-, Shadowed, Bleached, Ember-, Frost-) applied to nearby-landmark colors. 147 × 10 = 1,470+ modifier-expanded names algorithmically - Tier 3 — Coordinate-mood fallback for colors outside any landmark's orbit. Pattern: {ColourMood} Sector {hex}-{hex} — e.g., Crimson Sector 4E-92, Azurine Outer Rim 6D-F7 - saberColorNames.ts becomes a thin shim that re-exports getSaberColorName from namingMath.ts. No call sites need to change; every caller stays working.

Fixed

- Every possible RGB now returns a distinctive, evocative name. Zero "Unknown Crystal" fallbacks across the full HSL space. - Minute color adjustments no longer produce repeated names — modifier layers discriminate between neighboring hues.

Notes

- All existing tests still pass (backward-compatible signature) - New test suite (namingMath.test.ts) covers landmark preservation, modifier-trigger boundaries, coordinate-mood stability, and 1000-sample coverage


v0.11.3

2026-04-17

Added

- Modular hilt library (apps/web/lib/hilts/) — a composable parts + assemblies architecture. Every hilt is an ordered stack of discrete parts (emitter, shroud, switch, grip, pommel, accent-ring) that mate via three interface-diameter classes (narrow 1.0", standard 1.25", wide 1.5"). 33 original MIT-licensed line-art SVG parts ship across 5 type directories. - 8 canonical assemblies curated from the parts: graflex, mpp, negotiator, count, shoto-sage, ren-vent (5-part crossguard including the quillon), zabrak-staff (double-emitter saberstaff), and fulcrum-pair (dual-shoto compact). - HiltRenderer (apps/web/components/hilt/HiltRenderer.tsx) — pure React inline SVG renderer with opaque metal-gradient body + line-art detail strokes. Supports vertical (emitter up) and horizontal (emitter right) orientations via internal viewBox rotation. - Editor integration — 8 new -tagged options in the Hilt picker route through the SVG renderer overlay, coexisting with the 9 existing canvas-primitive hilts as a zero-risk addition. - Authoring docsdocs/HILT_PART_SPEC.md (canvas, connectors, palette, file structure) and docs/HILT_STAGE_2_BRIEFING.md (the 3-agent parallel fan-out plan) define the contribution path for community-PR'd parts.

Tested

- 18 tests across hiltComposer.test.ts and hiltCatalog.test.ts — composition stacking, connector strict + permissive modes, emitter tracking, catalog conformance (canvas width 48, connector cx=24), per-part spec validation, and round-trip composition of every shipped assembly.

Legal

- All shipped SVG parts are original hand-drawn line art, MIT under the same licence as the rest of the project. Reference commercial packs used only on-device during authoring — never redistributed.


v0.11.1

2026-04-17

Added

- ErrorState shared component (apps/web/components/shared/ErrorState.tsx) with 4 variants (load-failed, parse-failed, save-failed, import-failed) + optional retry callback + compact-mode rendering - StatusSignal shared component (apps/web/components/shared/StatusSignal.tsx) pairing status colors with typographic glyphs (●/◉/✓/▲/⚠/✕) for colorblind accessibility, with era and faction monogram variants - CHANGELOG.md documenting the full release history - docs/images/landing-hero.png — live-engine landing screenshot referenced from README.md above the feature list - --faction-sith-deep and --faction-jedi-deep tokens for gradient-stop use in .sw-sith-text / .sw-jedi-text

Changed

- Alert-color discipline — replaced raw #ff4444 and rgba(255, 60, 60, ...) in apps/web/app/globals.css with theme tokens: - .era-sequel and all era classes now use rgb(var(--era-*)) tokens - @keyframes retract-breathe now uses rgb(var(--badge-creative)) amber — the retract button is a warning, not an error - .console-alert now uses rgb(var(--status-error)) — still red (it IS an alert), but tokenised so colorblind theme overrides flow through - Status indicators across StatusBar.tsx, PresetGallery.tsx, PowerDashboard.tsx, and EngineStats.tsx now pair color with StatusSignal glyphs (FPS performance bucket, system-status indicator, power draw) - Async-boundary panels across the editor now show <Skeleton> during loading and <ErrorState> on failure (previously: blank panels or silent failures) - .sw-sith-text / .sw-jedi-text gradient stops routed through rgb(var(--faction-*)) tokens instead of raw hex - Raw #22c55e / #eab308 / #ef4444 in PowerDashboard.tsx and EngineStats.tsx replaced with rgb(var(--status-ok/warn/error)) so colorblind + theme overrides flow through

Fixed

- Era badges in PresetGallery now honour theme overrides (e.g., colorblind palette) instead of rendering raw hex - Retract-button pulse no longer conflates "action in progress" with "error state"

Infrastructure

- Deleted empty orphan directories: packages/codegen/src/sharing/, apps/web/components/sharing/ - Verified **/.DS_Store in root .gitignore (already present; no tracked files found)

Notes

- Third-party R/G/B-channel visualization renders in BladeCanvas.tsx, VisualizationStack.tsx, RGBGraphPanel.tsx, and visualizationTypes.ts intentionally keep raw red/green/blue hex colors — those represent the literal RGB channels being visualized, not alert semantics - TimelinePanel.tsx event-type category colors (ignite/retract/ clash/blast/etc.) stay as raw hex — these are distinct identity colors paired with text labels, not alert semantics. Tokenising these would be identity coupling, not accessibility - Lint enforcement (originally scoped for this sprint as Phase C4) is deferred. ESLint is not currently in devDependencies; activating it would surface hundreds of preexisting issues and is worth its own sprint with a clear scope for how to handle them (fix vs // eslint-disable-next-line). Tracked as a follow-up


v0.11.0

2026-04-17

Added — WebUSB Flash

- Full STM32 DfuSe protocol library for flashing Proffieboard firmware directly from the browser via WebUSB - FlashPanel.tsx UI with "use at your own risk" disclaimer gate, per-session acknowledgement - Firmware variants pre-built via GitHub Actions CI: firmware-configs/v3-standard.h, v3-oled.h, v2-standard.h - Read-back verification after flash to confirm write success - Dry-run mode for testing protocol logic without actually flashing - 43 mock-USB tests covering the DFU state machine - docs/WEBUSB_FLASH.md technical reference documentation - docs/HARDWARE_VALIDATION_TODO.md pending-validation checklist

Changed

- Landing page (/) now a real landing with blade hero + value strip + CTAs + release strip + footer, replacing the redirect stub

Notes

- v0.11.0 is NOT yet tagged — pending hardware validation against real Proffieboard V3.9 devices before promoting to release


v0.10.0

2026-04-17

Added — Long-tail cleanup

- Spatial positioning for drag, melt, and stab effects (joining lockup and blast from v0.3.0) - Parser warnings channel (non-fatal diagnostics for unknown templates + arg-count mismatches) - Font pairing polish: keyword-based scoring, "Recommended / Compatible" labels in SoundFontPanel

Architecture

- packages/codegen/src/astBinding.ts — six-seam façade for config ↔ AST ↔ code transformations - packages/codegen/src/transitionMap.ts — single source of truth for ignition/retraction ID ↔ AST mappings (fixed pre-existing standard ↔ scroll round-trip swap) - Lexer now consumes :: tokens (fixes SaberBase::LOCKUP_NORMAL misreading as 5 args)


v0.9.1

2026-04-17

Fixed — Validation + polish

- Critical data-loss fix: Import → Apply round-trip no longer silently drops spatial, Preon, and extended-color fields - Mobile companion route now carries preset ID to editor via ?preset= query - Theme-token compliance: replaced accent-[var(--color-accent)] no-ops with real Tailwind classes across 4 components


v0.9.0

2026-04-17

Added — Mobile companion route

  • /m route with 12 curated canonical presets
  • Swipe navigation
  • Deep-link into full editor via preset ID

v0.8.0

2026-04-17

Added — Audio-visual sync

  • useAudioSync hook feeding motion swing → audio pitch/volume
  • SmoothSwing V1/V2 pair crossfading
  • Motion-driven audio envelope ripples through the blade render

v0.7.0

2026-04-17

Added — Timeline easing curves

- 8 named easing curves (linear, easeIn, easeOut, easeInOut, bounce, elastic, dramatic, smooth) - Inline SVG preview of each curve in the TimelinePanel - SSR-safe easingMath.ts pure-function module


v0.6.0

2026-04-17

Added — Prop file visual UI

  • 5 prop file presets (Fett263, SA22C, BC, Shtok, default)
  • Button-action map reference table with gesture icons

v0.5.0

2026-04-17

Added — Sound font pairing + crystal reactive glow

  • Keyword-based sound font scoring against blade config
  • "Recommended / Compatible" pairing labels
  • --crystal-accent CSS var follows baseColor for themed UI accents
  • useCrystalAccent hook publishing the accent

v0.4.0

2026-04-17

Added — Saber Wizard (guided onboarding)

  • 3-step onboarding modal: archetype → colour → vibe
  • 5 archetypes, curated colour swatches, 4 vibe presets
  • AutoFocus on first archetype button for keyboard users

v0.3.1

2026-04-17

Added

  • Blade-accurate colour rendering (Neopixel gamma + diffusion preview)
  • Preon engine preview animation
  • Spatial blast placement polish

v0.3.0

2026-04-17

Added — Preon editor + spatial blast

  • TransitionEffectL<…, EFFECT_PREON> emission + engine preview
  • Spatial blast position + radius round-trip through Bump

v0.2.2

2026-04-17

Added

  • GPL-3.0 attribution for ProffieOS upstream
  • LICENSES/ProffieOS-GPL-3.0.txt with full license text
  • README aggregate-work separation documentation

v0.2.1

2026-04-17

Added — Polish

  • Dual-mode ignition (TrSelect with saber-up / saber-down variants)
  • detectStyle heuristic for imported configs
  • UI theme tokens rationalised across panels

v0.2.0

2026-04-17

Added — WYSIWYG Edit Mode + Spatial Lockup

- Click on blade → moves caret, updates config, re-emits code - AlphaL<LockupTrL<…>, Bump<Int<pos>, Int<size>>> spatial lockup round-trip


[0.1.0] and earlier — Foundational phases

Pre-v0.2.0 work focused on building the core architecture:

  • Monorepo setup (Turborepo + pnpm workspaces)
  • Engine-first architecture (packages/engine)
  • AST-based code generation (packages/codegen)
  • 29 blade styles, 21 effects, 19 ignitions, 13 retractions
  • Multi-column workbench layout with visualization stack
  • 30 canvas themes (9 base + 21 extended)
  • IndexedDB persistence via Dexie

See the full history in git log --oneline for implementation commit details.


Release tagging convention

- Tags use vX.Y.Z format (e.g., v0.10.0, v0.11.0) - Each release has a corresponding feat/vX.Y.Z-* feature branch that squash-merges to main - GitHub releases include a release notes summary and any relevant build artefacts (firmware binaries for v0.11.x, for example)

Related planning documents

- CLAUDE.md — project context + roadmap matrix - ~/.claude/plans/declarative-strolling-dragonfly.md — current multi-sprint orchestration plan - ~/.claude/plans/i-m-curious-what-the-glistening-island.md — design review plan that spawned v0.11.1 - docs/KYBER_CRYSTAL_VISUAL.md, docs/KYBER_CRYSTAL_NAMING.md, docs/KYBER_CRYSTAL_VERSIONING.md, docs/SHARE_PACK.md — Kyber Crystal + Saber Card design specs - docs/COMMUNITY_GALLERY.md — GitHub-PR community gallery spec - docs/WEBUSB_FLASH.md + docs/HARDWARE_VALIDATION_TODO.md — WebUSB reference + hardware validation pending list