mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-10-02 19:21:50 +00:00
e2306297709bcccdc2e4bba31819520c205276f8
510
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e230629770 |
h3d-carve-12: extract T-section (arpeggio inference) into src/arp.js
Move all ~770-line T-section functions (chordWireHighDensity, chordTemplateLabel,
chordTemplateMarkedArpeggio, chordHandShapeArpeggioHint, mergeHandShapeSynthChords,
mergeChordShape, inferArpeggioFromNotePattern, chordShapeCoveredByStandaloneNotes,
hsStart, hsEnd, handShapeChartSpanSec, fillArpeggioGhostInferFlags,
arpeggioChordIdForNoteWithInferCache, arpHsBoundsForNote, fillLaneRailHandShapeFlags,
fillArpeggioRailShapeBoundsCaches, arpeggioLaneOuterRailLaneSlice,
arpeggioLaneOuterRailAtChartTime, arpeggioLaneDividerFrameAccentMul,
arpeggioLaneDividerXYScaleMatchFrameRim) into a createArp({}) factory.
DI surface: 19 params — 18 plain const shorthand + 1 live getter (getNStr).
lowerBoundT imported directly from ./geometry.js (not DI'd).
NEXT_ON_STRING_T_EPS was found during ALL_CAPS grep after initial survey and
added as param 19.
Structural fix: _resetStringDependentCaches() remains in screen.js; exports
resetChordShapeCache() so screen.js can reset _chordShapeCache without reaching
into arp.js internals. DI rewire: arpeggioLaneDividerXYScaleMatchFrameRim uses
getNStr() instead of bare nStr (the live let var).
Test coverage (tests/js/highway_3d_arp.test.js — 19 tests):
- Module shape, 21-export return object, lowerBoundT direct import
- DI param surface (NEXT_ON_STRING_T_EPS, getNStr getter)
- Screen.js wiring: import, T-section body gone, destructure callsite
- Wiring-correspondence guard (PINNED_RENAMES = {}, naming-class invariant)
- Amendment 2: resetChordShapeCache identity kill (gut reset → r3 === r1 → RED)
- Amendment 3: WeakMap re-keying guard (same-ref hit, new-ref recompute)
- Behavioral kills: mergeChordShape, mergeHandShapeSynthChords, chordWireHighDensity,
chordTemplateLabel, arpeggioLaneDividerXYScaleMatchFrameRim DI check
Also updates highway_3d_arp_deferral.test.js to look in src/arp.js for
chordShapeCoveredByStandaloneNotes (moved out of screen.js by this cut).
plugin.json: 3.47.0 → 3.48.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
|
||
|
|
930b492fa5 |
h3d-carve-11: extract R-section (string glow) into src/string-glow.js
VERBATIM-MOVE of updateStringHighlights from screen.js into a new
createStringGlow() factory. 7 DI-rewires at function entry (aliased
locals), plus 1 plain const shorthand. 8 DI params total, 0 setter
pairs, single export { updateStringHighlights }.
screen.js changes:
- Function definition (old 6610–6649) replaced with createStringGlow({…})
factory destructure
- mStr confirmed absent from updateStringHighlights (plan row 11 stale;
declared surprise in contract, accepted by god)
- VENUE_GEM_EMISSIVE_MUL passed as plain const shorthand (not a getter)
Tests (highway_3d_string_glow.test.js, 11 new, all green):
- Module shape + DI rewire source-scans
- Wiring-correspondence guard (createStringGlow, empty PINNED_RENAMES)
- 3 behavioral tests: emissive/opacity writes, venue multiplier,
null-mesh slot safety — behavioral kill: gut loop → assert RED
Suite: 1317/1319 pass. Pre-existing failures #46 (legacy analyser)
and #639 (nut-labels) unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
|
||
|
|
4b2e4172c5 |
h3d-carve-10: extract K-section (score FX) into src/score-fx.js
VERBATIM-MOVE of the score-FX block from screen.js into a new
createScoreFx() factory. 7 DI-rewires only (getNdFrameNowMs,
getCam, getProbe, getNStr, getCurX, getHighwayCanvas × closure,
sY shorthand). No logic changes.
Four regions in screen.js replaced with factory callsites:
- State block (old lines 3619–3720): createScoreFx({…}) destructure
- drawScoreFx body (old 12564–12672): tombstone comment
- Init block (old 5839–5862): fxInit() callsite
- Teardown lines (old 12796/12814–12820): fxTeardown() callsite;
_fxOnSkin removal moved into fxTeardown(), feedBack block
restructured to contain only non-K-section listeners.
Tests (highway_3d_score_fx.test.js, 37 new, all green):
- Gut-audit: all 4 exports + _fxHandle + _fxSpawnPop paths
- Class-killers: fxTeardown listener removal, _fxGen increment
- Verbatim-declaration: no re-entry guard in fxInit (per dispatch)
- Wiring-correspondence guard: createScoreFx({…}) naming (cut-9
pattern, empty PINNED_RENAMES)
Pre-existing failures: #46 (legacy analyser), #628 (nut-labels) —
both pre-date this cut, confirmed against
|
||
|
|
f401127c20 |
test(h3d-carve-9): naming-correspondence guard for createCamera wiring (Creed r1)
Adds a structural source-scan test that verifies every entry in the
createCamera({...}) argument object satisfies its naming class:
- Shorthand entries (plain consts, sY): key===value by definition, skipped.
- Getter arrows (getXxx: () => [_]var): var stem (stripped of optional _)
must equal key minus 'get' prefix (getCurX → curX, getProbe → probe).
- Setter arrows (setXxx: (v) => { [_]var = v; }): same rule for set.
- Fn-ref renames (freeCamFor: _freeCamFor, etc.): must appear in the
exhaustive PINNED_RENAMES map; any unregistered key:value entry fails.
Kills the entire param-swap class (not just the CAM_H_BASE/CAM_DIST_BASE
instance Creed probed). Cut-13 additions inherit the guard automatically;
new fn-ref renames only need a one-line PINNED_RENAMES entry.
Mutation proofs (all RED before revert):
(a) CAM_H_BASE: CAM_DIST_BASE, CAM_DIST_BASE: CAM_H_BASE → not ok 14
(b) getCurX: () => curDist (wrong var stem) → not ok 14
(c) bogusKey: _someOtherFn (unregistered rename) → not ok 14
Gut-audit: severing the violation check (violations.push suppressed) → the
assert.deepEqual(violations, []) always passes — so the push lines are the
live part; removing any one of the three class branches lets its mutation
class through silently.
Suite: 1272/1274 pass (same 2 pre-existing failures, +1 new test).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
|
||
|
|
10c7ec8a9d |
refactor(h3d-carve-9): extract W-section (camera lerp) → src/camera.js
effectiveVfov + camUpdate (~192 lines) extracted from the screen.js IIFE
into a createCamera() ES-module factory in plugins/highway_3d/src/camera.js.
screen.js imports and destructures the return { effectiveVfov, camUpdate }.
DI surface (48 params): 22 plain constants, 11 getters, 5 getter+setter pairs,
5 fn-refs (DI-renamed: _freeCamFor→freeCamFor, _aspectPaneKey→aspectPaneKey,
_resolveTuneFor→resolveTuneFor, _aspectRegisterPane→aspectRegisterPane).
Peer ES imports at module level: computeBPM (geometry.js), _ssActive (utils.js).
Per-call write-backs: setCurX, setCurDist, setCurLookY, setTgtLookY,
setFretRowFitBoost (all write-backs confirmed by setter class-killer tests).
Test retargeting (54 tests across 4 files):
- highway_3d_wide_fov.test.js: 6 tests → cameraSrc; 4 regexes updated for
DI-renamed fn refs (resolveTuneFor, aspectRegisterPane, aspectPaneKey/getPaneUid)
- highway_3d_camera_framing.test.js: 5 tests → cameraSrc; getTgtDist() regex fix
- highway_3d_camera_bootstrap.test.js: extractFn retargeted to cameraSrc;
getTgtX() ordering-check fix; 2 new setter class-killers added (setCurX,
setFretRowFitBoost)
- highway_3d_lefty.test.js: shoulder-offset test → cameraSrc + getLeftyCached()
- highway_3d_panel_controls.test.js: createCamera stub added
Bite proofs:
- Gut effectiveVfov in camera.js → wide_fov not ok 4 (RED) ✓
- Gut camUpdate body (H_NEAR lerp) → framing not ok 3 (RED) ✓
- Gut camUpdate body (curX+=) → bootstrap not ok 11 (RED) ✓
- Sever setCurX → bootstrap not ok 12 (RED) ✓
- Sever setFretRowFitBoost → bootstrap not ok 13 (RED) ✓
Suite: 1271/1273 pass; 2 pre-existing failures unchanged from cut-8 baseline
(#46 analyser fallback, #591 nut-labels — both in flight before cut 9).
plugin.json: 3.44.0 → 3.45.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
|
||
|
|
4a45ed8782 |
test(fx): source-scan BG_DEFAULTS.nutColor fixture (Creed r2)
test 11 now extracts the real nutColor from screen.js via regex and
pins it against a literal ('#f5f3f0'). Three-layer guard:
1. guard: BG_DEFAULTS literal is present in screen.js
2. guard: nutColor key is extractable
3. literal-pin: extracted value == '#f5f3f0' → fail loudly if
production drifts (verified: mutating screen.js to '#deadf5'
makes the test fail on the pin assertion; revert confirmed clean)
Fixture and fallback assertion both use PROD_NUT_COLOR (the extracted
value), so they track production automatically.
panel_controls stub updated: added _applyBloom to the createFx stub
(Toby r1 added _applyBloom to the return set; stub was one short).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
|
||
|
|
1f3bde73d1 |
test(h3d-carve-8): add discriminating tests for _applyCinematic and _h3dHexOrDefault
Creed gap fix: two functions had no test that went RED when gutted.
_applyCinematic: tests both paths (cinematic=true and false) against
fake ambLight/dirLight objects with intensity fields. Asserts all 4
expected intensity values. Mutation `return` at entry → RED (4 fail).
_h3dHexOrDefault: tests valid hex parse ('#a1b2c3' → 0xa1b2c3), no-#
fallback (regex requires #), gibberish fallback, and explicit defHex
override. Mutation (return default always) → RED (valid-hex assertion fails).
Suite: 1269/1271 (same 2 pre-existing failures unchanged).
Note: from cut 9 onward, self-audit step added before reporting — every
moved function names which test kills it; no uncovered function ships.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
|
||
|
|
d060c49238 |
test(h3d-carve-8): fix Toby r1 findings — _applyBloom extract + test 7 vacuity
F1 (MED — declared gap confirmed and widened):
Extract _bloomEnsure's .then() body into named _applyBloom([EC, RP, UB, OP])
at factory scope. _applyBloom added to return set and screen.js destructure.
Test 9 calls _applyBloom directly with mock module objects (no import() needed).
All 4 write-back mutations now RED:
sever setBloomPass(bp) → RED
sever setBloomW(w) → RED
sever setBloomH(h) → RED
sever setComposer(comp) → RED
F2 (MED — vacuous OR clause in test 7):
Old:
The arm is always true (no living sparks) → NO-OP passes.
Fix: replaced with independent AND assertions on BOTH needsUpdate flags.
NO-OP mutation (return; at top of _sparkUpdate) now goes RED.
DI beyond-subst count +5: _applyBloom reads T/ren/scene/cam/highwayCanvas
from live-accessors (instead of inheriting _bloomEnsure's locals).
fx.js return set: 6 → 7 symbols (added _applyBloom).
Suite: 1267/1269 (2 pre-existing failures unchanged).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
|
||
|
|
9b98d78cbe |
refactor(h3d-carve-8): extract Q-helpers (lighting/FX) → src/fx.js
Six functions extracted from Q-section of screen.js into a new
src/fx.js ES module behind a createFx({...}) factory:
_h3dHexOrDefault, _applyCinematic, _timingHex,
_sparkBurst, _sparkUpdate, _bloomEnsure
buildBoard (330 lines) deferred to plan §3 row 16 (P-section cut):
its write-back surface spans 9 factory-scope vars owned by P/U/Y —
premature extraction would require ~50 DI params. See plans/highway3d-carve.md.
DI surface (26 beyond-subst rewires):
BG_DEFAULTS, K — plain IIFE-scope constants
getT — live-accessor (Three.js, lazy)
getAmbLight/getDirLight/getCinematic/getTimingFx — live-accessors
getSparkPts/.../getSparkN — live-accessors (per-call, not init-cached)
setSparkPts/setSparkPos/setSparkVel/setSparkCol/setSparkLife — setters
getComposer/setComposer — getter+setter (_bloomEnsure lazy reassigns)
getBloomLoad/setBloomLoad/getBloomPass/setBloomPass — getter+setter pairs
getBloomW/setBloomW/getBloomH/setBloomH — getter+setter pairs
getRen/getScene/getCam/getHighwayCanvas — live-accessors (null pre-init)
canvasSize — factory function ref (stable)
Class-killers (all mutations verified RED before commit):
- _timingHex EARLY branch removed → RED
- _sparkBurst stale init-time sparkPos cache → RED (live-accessor required)
- setBloomLoad silenced → RED (synchronous write-back path)
- setComposer getComposer init-cached → RED (live getComposer() required)
GAP declared: setComposer(comp) inside .then() (async, no import() mock)
Tests: 1266/1268 (2 pre-existing failures unrelated to this cut)
plugin.json: 3.43.0 → 3.44.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
|
||
|
|
4b87db0c17 |
test(h3d-carve-7): add discriminating render-path tests for drawToneHud and drawSectionHud
Creed gap: both functions only tested with empty/null state — a return 0 at
the body entry point kept the suite green (Creed's exact injection at
overlay.js:631 for drawToneHud; equivalent non-empty branch gut for drawSectionHud).
Fix: two new recording-ctx tests (tests 9–10) that drive real rendering:
• drawToneHud: toneBase='Clean', next change 'Lead' at t=10, currentTime=5
→ assert boxH>0, fillText contains 'Clean' and 'Lead', ctx.fill() called
• drawSectionHud: sections=[Intro@0, Verse@10], currentTime=5
→ assert boxH>0, fillText contains 'Intro' and 'Verse', ctx.fill() called
Named mutations verified RED:
Mut 1: return 0 at drawToneHud body start (overlay.js:631) → test 10 RED
Mut 2: return 0 after drawSectionHud empty-guard → test 11 RED
Shipped code: zero changes. Test files only.
Suite: 262/262 on
|
||
|
|
0a91f4f1b1 |
refactor(h3d-carve-7): extract O-section (lyrics + HUD overlay) → src/overlay.js
Move longestConsecutiveRun, drawChordDiagram, _drawDiagramCached,
drawSectionHud, drawToneHud, drawLyrics out of screen.js IIFE into
plugins/highway_3d/src/overlay.js as export function createOverlay({ diagRenderCache }).
Surprises vs approved contract:
• longestConsecutiveRun (lines 4338–4352) co-moved — called only by drawChordDiagram
• DIAG_SIZE_MIN, DIAG_SIZE_MAX, DIAG_CELL_MAX DELETED from screen.js (lines 573–575);
only users were inside the O-section; moved not copied to avoid drift
• _DIAG_CACHE_MAX DELETED from screen.js factory scope (line 3256); moved not copied
Beyond-subst rewires (2):
1. Factory wrapper createOverlay({ diagRenderCache })
2. _diagRenderCache → diagRenderCache (DI param) in 3 sites in _drawDiagramCached
Base run stated: 253/253 on
|
||
|
|
a993d2b291 |
test(h3d-carve-6): strengthen stale-private guard to catch full class (Toby r1)
Replace hardcoded ['_pmXSpriteMat','_fhXSpriteMat'] list in test 4 with a full factory-depth-1 scan of materials.js (4-space indent const/let declarations NOT in the return set). This catches TXT_STYLES and any future factory-private additions automatically. Mutation-verified: void TXT_STYLES injected into screen.js → test 4 RED; reverted → 16/16 GREEN. Full h3d suite: 253/253, 0 fail. Command: node --test tests/js/highway_3d*.test.js plugins/highway_3d/tests/*.test.js Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW |
||
|
|
e6b2f86068 |
refactor(h3d-carve-6): move N-section material builders to src/materials.js
Extract TXT_STYLES + 13 material builder functions (txtMat, pinchHarmonicMat,
naturalHarmonicMat, palmMuteXSpriteMat, fretHandMuteXSpriteMat, muteXMat,
triMat, bendChevronMat, darkenHex, slideArrowMat, _meshMatForGhostFretDigit,
_spriteMat2MeshMat) and pool() from screen.js N-section into:
plugins/highway_3d/src/materials.js (createMaterialBuilders factory)
screen.js drops ~600 lines (15302→14705).
Surprises vs plan §4:
• DI is 4 params { getT, getTxtCache, techMatCache, techMeshMatClones } not 1
• _syncOpenStringPitchLabels cluster excluded (20+ factory-scope deps)
• _techMatCache stays in screen.js factory scope for teardown .values()/.clear()
Beyond-subst (4):
1. Factory wrapper createMaterialBuilders({...})
2. T → const T = getT() inside each function body (live accessor)
3. txtCache[k] → const cache = getTxtCache(); cache[k]
4. _techMatCache/_techMeshMatClones → DI param names techMatCache/techMeshMatClones
Tests: 16 new class-killer tests in highway_3d_materials.test.js;
pool warm tests retargeted to src/materials.js; panel_controls stub added.
Full suite: 1246/1248 pass (2 pre-existing: network + nut-labels).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
|
||
|
|
733d772154 |
test(h3d-carve-5): pin literal tables — _PC_C, _PC_PILL, _PC_LABELS, _PC_USES (test 15)
Creed finding: DOM/style literals in the moved factory were never asserted; a single-value change (e.g. idle '#181830'→'#181831') shipped silently. Add test 15: table-driven source scan pinning all 6 _PC_C color entries, 4 _PC_PILL CSS fragments, all 8 _PC_LABELS display strings, and all 9 _PC_USES intensity/reactive boolean pairs. Mutation-verified RED: idle '#181830'→'#181831' → not ok 15, fail 1 (Creed's mutant) particles intensity true→false → not ok 15, fail 1 Suite: 236/236 → 237/237. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW |
||
|
|
90283f9d3a |
test(h3d-carve-5): fix test 14 defined-set to full identifier scan
Toby r2 finding: the declaration-only regex only captured the first variable from multi-var let statements. bg-control.js lines 81-88 have a single let with 8 state symbols (_pcEl, _pcSel, _pcReactive, _pcIntensity, _pcIntensityWrap, _pcReason, _pcRetry, _pcRetryTimer); 7 of 8 were invisible to the guard, so a bare _pcEl ref in screen.js escaped all tests. Fix: replace the declaration-pattern matchAll with a full identifier scan (/\b(_pc\w+)\b/g). Every _pc* word in bg-control.js lands in `defined`; the privateSymbols filter (excluding destructured public API) is unchanged. Mutation-verified RED: _pcEl.remove() injected in screen.js → not ok 14, fail 1 (Toby's mutant) _pcSync() injected in screen.js → not ok 14, fail 1 (Mut B, still works) Suite: 236/236. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW |
||
|
|
97799fff37 |
test(h3d-carve-5): fix test 13 kill + add stale-private guard (test 14)
Toby r1 finding: test 13's body-ref filter let the stated mutation escape
('add _pcNewFn to return only → RED' was actually GREEN), and the cut-4
stale-private-reference class (_pcSync bare ref in screen.js) was also
unguarded.
Fix test 13: drop the && screen.js body-ref condition from the leaked
filter — pure 'returned ⊆ destructure' check. Mutation-verified RED:
add _pcNewFn to return but not destructure → leaked = ['_pcNewFn'] → fail 1.
Add test 14: extract every _pc* symbol defined in bg-control.js (function/
let/const declarations), filter to private (not in the destructure), assert
none appears bare in screen.js IIFE body after stripping imports, block
comments, line comments, and the destructure statement. Mutation-verified RED:
inject _pcSync() in screen.js body → stale = ['_pcSync'] → fail 1.
Suite: 235/235 (
|
||
|
|
c4eebe1c9f |
refactor(h3d-carve-5): move H-section player-chrome bg-control to src/bg-control.js
Extracted _pc* subsystem (420 lines) from screen.js IIFE into
src/bg-control.js using a factory DI pattern (createBgControl({...})).
Exports: createBgControl({ BG_STYLE_IDS, _bgReadGlobal, _bgSubscribe,
_bgUnsubscribe, getVenueSceneOverride })
→ { _pcAcquire, _pcRelease }
screen.js: const { _pcAcquire, _pcRelease } = createBgControl({...})
Beyond-subst (2):
1. Factory wrapper (IIFE-scope closure → DI params) — factory export pattern
required because DI values are IIFE-scope, not ES module imports.
2. _venueSceneOverride → getVenueSceneOverride() (live accessor, 1 call site
in _pcSync — mutable let at screen.js:1523 must be read per-call).
DI values all defined before the createBgControl call (screen.js):
- BG_STYLE_IDS: line 1435 | _bgReadGlobal: 1825
- _bgSubscribe/_bgUnsubscribe: 1917-18 | _venueSceneOverride: 1523
First _pcAcquire caller: init() in createFactory() (~line 14850 post-cut).
Construction order correct: createBgControl call before createFactory.
Surprise declared to god before commit (outbox/h3d-cut5-surprise.json):
No bc-panel.js dependency — §8's anticipation was wrong. The
_bcCreateController call at what was ~line 8082 is in _bcSyncMode
(P-section / factory scope), not the H-section. bg-control.js has ZERO
dependency on bc-panel.js.
Tests:
- tests/js/highway_3d_bg_control.test.js (new, 13 class-killers):
stranded-caller (test 13, factory-adapted from bc-panel test 12),
construction-order (tests 11-12), DI completeness (test 2),
live-accessor enforcement (test 3), lifecycle (tests 4-7).
- plugins/highway_3d/tests/background_control.test.js: retargeted from
screen.js slice → bg-control.js factory eval; all 20 existing behaviour
tests preserved (load() uses vm.createContext + augmented return getters).
- tests/js/highway_3d_panel_controls.test.js: createBgControl stub added.
Suite: node --test tests/js/highway_3d*.test.js plugins/highway_3d/tests/*.test.js
base 222/222 (
|
||
|
|
f69c544eea |
fix(h3d-carve-4): export _bcLoadSettings + _bcFfIdx; add caller-coverage test (Creed HIGH)
screen.js H/P-section render path at lines 15396-15402 calls _bcLoadSettings()
and _bcFfIdx() (3×) — both moved to bc-panel.js in cut 4 but omitted from the
export list. Browser: first render() after bcCtrl creation → ReferenceError;
seek/loop fast-forward index also dead. Suite was green because no test
executed the butterchurn render path.
Fix:
- export _bcLoadSettings and _bcFfIdx from bc-panel.js
- add both to the tagged import in screen.js
Class-killer (test 12 — generic, not instance-specific):
Extracts all exports from bc-panel.js, all imports in screen.js's
bc-panel.js import clause, then asserts no exported symbol appears as a
bare reference in the screen.js IIFE body without being imported.
Generic: adding a new export + new caller without updating the import → RED.
Own grep (audit):
grep (non-comment lines, all private _bc* names from bc-panel.js):
_bcLoadSettings 1 hit (line 15396)
_bcFfIdx 3 hits (lines 15400-15402)
all others: 0 hits
Only the two symbols Creed found.
Mutation-verify:
remove _bcLoadSettings from screen.js import →
node --test tests/js/highway_3d_bc_panel.test.js
tests 12 pass 11 fail 1 (test 12 RED) ✓
restore →
node --test tests/js/highway_3d*.test.js plugins/highway_3d/tests/*.test.js
tests 222 pass 222 fail 0 ✓ GREEN
Plan §8 amended: exports 2→4 with dated note.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
|
||
|
|
81b366746e |
test(h3d-carve-4): fix vacuous module-scope check in bc_panel.test.js (Toby r1)
Test 2 used s.indexOf which matched the JSDoc comment at offset 133, not
the real assignment at line 183 — so it passed even with the assignment
indented inside a function.
Fix: s.search(/^window\.h3dBcApplySettings\s*=/m) anchored to line-start.
An indented assignment (inside a function) does not match /^window\./m and
returns -1. Also removed the erroneous firstFnIdx comparison: the assignment
is legitimately after _bcIsDesktop in the file and still at module scope.
Mutation-verify:
indent assignment → node --test tests/js/highway_3d_bc_panel.test.js
not ok 2 (fail 1/11) ✓ RED
restore → node --test tests/js/highway_3d*.test.js plugins/highway_3d/tests/*.test.js
tests 221 pass 221 fail 0 ✓ GREEN
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
|
||
|
|
4e060cbf7b |
refactor(h3d-carve-4): extract Butterchurn panel to src/bc-panel.js
Move all _bc* constants, mutable state, and functions (~670 lines) from
screen.js B-section (lines 27-695) to src/bc-panel.js.
Public API: _bcCreateController, _bcIsDesktop (2 named exports).
window.h3dBcApplySettings assigned at bc-panel.js module scope (1
beyond-subst; body verbatim — scope moves from IIFE to ES module top-level).
THREE_URL / THREE_CDN dead-code tombstoned (unused since three-loader.js).
Vendor files untouched (R2); asset URL constants verbatim (R3).
Delta note: original survey estimated ~1,333 lines for B-section. Actual is
~670 because (a) prior cuts 1b/2/3 moved material that was interleaved in the
B-section range, and (b) the original survey section boundaries were wrong —
the B-section ends where the H-section factory begins, not at line 695 of the
pre-cut file.
Completeness grep (post-cut):
grep -nP '^\s*(function|const|let|var)\s+_bc' screen.js
→ 2 hits: _bcActive and _bcSyncMode at line 8081-8082 (factory-scope
H/P-section helpers that orchestrate the imported controller; not B-section
symbols — confirmed F-section in the survey table).
B-section _bc* definitions in IIFE: 0.
Suite: node --test tests/js/highway_3d*.test.js plugins/highway_3d/tests/*.test.js
tests 221 pass 221 fail 0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
|
||
|
|
6050b6262b |
fix(h3d): thread maxStrings param through resolveStringCount (Toby r1)
Toby r1 on
|
||
|
|
c7f7c88c62 |
refactor(h3d): extract color/tuning/splitscreen utils to src/utils.js (h3d-carve-3)
Moves 12 pure-function / compile-time-constant exports from the screen.js
IIFE into a new src/utils.js ES module:
Color utils: _h3dHexToInt, _clampByteI, _darkenInt, _lightenInt
String-count: resolveStringCount
Tuning/pitch: _NOTE_NAMES_SHARP, _BASE_OPEN_MIDI_BASS4/5, _BASE_OPEN_MIDI_GUITAR6/7/8,
_baseOpenStringMidis, _midiToPitchLabel, _openStringPitchLabelsForTuning
Splitscreen: _ssActive, _ssIsCanvasFocused
Free-identifier audit: all clean. resolveStringCount uses MAX_RENDER_STRINGS
and NSTR — copied as compile-time constants (both = 6) matching IIFE values.
_ssActive/_ssIsCanvasFocused read window.feedBackSplitscreen live per call.
Survey discrepancy declared: the 12 functions span two non-contiguous regions
in current screen.js (lines 741–882 and 1490–1503) rather than the plan's
original 1703–2193 range; function list from the plan is exact.
New test file highway_3d_utils.test.js: 19 class-killer tests.
Mutation-verified (3-char shorthand removal → RED, original → GREEN).
Panel-controls sandbox: 16 stubs added for the new imports.
Suite: node --test tests/js/highway_3d*.test.js plugins/highway_3d/tests/*.test.js
188→207/207 pass.
Plugin: 3.38.0 → 3.39.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
|
||
|
|
f75e91088f |
test(h3d): catch const-shadow mutation in T=mod assertion (Toby r1)
Toby r1 finding on
|
||
|
|
8ea123deaf |
refactor(h3d-carve-2): extract Three.js loader to src/three-loader.js
Move the D-section (~21 lines) from screen.js factory scope to its own module.
Exports:
- loadThree() — memoised import() with local-vendor→CDN fallback; same body verbatim
- T (live let-binding) — updated to the Three.js namespace on first resolution
screen.js gains: import { loadThree, T } from './src/three-loader.js'
screen.js loses: let T = null; let threeLoadPromise = null; function loadThree()
T and loadThree remain accessible to the IIFE via module-scope closure. The live
let-binding means the IIFE reads the populated T after loadThree() resolves
without any call-site changes.
Panel-controls vm test: strip regex extended to consume all consecutive import
lines; T: null + loadThree stubs added to sandbox context.
Class-killer tests (8 new — highway_3d_three_loader.test.js):
- loadThree exported (mutation: rename/remove → import fails)
- T exported as mutable let (mutation: const → T=mod throws TypeError)
- T=mod in both .then handlers (mutation: remove both → T stays null)
- memoisation guard !threeLoadPromise (mutation: remove → race + duplicate loads)
- CDN fallback .catch chain (mutation: remove → deploy failures unrecoverable)
- threeLoadPromise reset on failure (mutation: remove → no retry possible)
- screen.js imports loadThree+T (wiring confirmed)
- IIFE no longer declares local T or threeLoadPromise (shadow defeated)
All class-killer mutations confirmed distinguishable before commit.
Base run (
|
||
|
|
5e401afe87 |
refactor(h3d-carve-1b): extract render-order, note-key, camera-bootstrap, fretMid to src/geometry.js
Move 9 symbols verbatim from screen.js factory scope to geometry.js:
- RENDER_ORDER_LAYER_STACK, RENDER_ORDER_LAYER_INDEX, RENDER_ORDER_AT_Z_ZERO,
RENDER_ORDER_FAR_CLAMP — compile-time constants, now exported from geometry.js
- renderOrderForLayerAtZ — pure fn; reads K + RENDER_ORDER_* from geometry scope
- _noteKey, lowerBoundT — hot-path helpers; no external deps
- hwyFirstRelevantFrettedTime — camera bootstrap scan; no external deps
- fretMid → geoFretMid(f, uniform) — same fretX delegator pattern as Cut 1;
screen.js keeps: const fretMid = f => geoFretMid(f, _h3dFretUniform); (1 beyond-subst)
screen.js import line updated to import all 9 new exports; tombstones replace
each original definition.
Source-scan retargets:
- highway_3d_render_order.test.js: layers()/zZeroRenderOrder() now read
GEOMETRY_JS; the 5 renderOrderForLayerAtZ-internals asserts in
chordFrameRenderOrder test retargeted to geo() (call-site assert stays on src()).
- highway_3d_camera_bootstrap.test.js: extractFn now reads geoSrc (GEOMETRY_JS);
sourceBetween wiring tests remain on SCREEN_JS.
- highway_3d_panel_controls.test.js: sandbox stubs extended with 9 new names.
Class-killer tests added to highway_3d_geometry.test.js (8 new tests, 180 total):
- RENDER_ORDER_LAYER_STACK length + first/last entries
- RENDER_ORDER_LAYER_INDEX spot-checks (CHORD_FILL=0, NOTE_CORE=10)
- renderOrderForLayerAtZ far-clamp (worldZ=-5 gives 50, not 33 without max)
- renderOrderForLayerAtZ unknown-layer throws
- _noteKey |0 truncation (1.5,3)=150003 not float-derived 150008
- lowerBoundT strict lower-bound (3 in [{t:1},{t:3},{t:5}] gives 1, not 2)
- hwyFirstRelevantFrettedTime smoke (empty → null)
- geoFretMid sentinel (f=0 gives -2K≈-0.015, not 0) + ratio invariant
Mutation analysis confirmed all 8 tests fail under their named mutation before
committing (per Toby r1 lesson).
Base run (
|
||
|
|
a1f7ad5e68 |
test(h3d-carve-1): add class-killer coverage for camBaseDistU, camLowFretPullbackU, _makeGaussTex
Toby r1 finding: three exports in src/geometry.js had no class-killer tests.
Named mutations that were undetected before this commit:
- camBaseDistU: Math.max(span,4)→span leaves camBaseDistU(0)=65 vs 77
- camLowFretPullbackU: drop Max(0,..) leaves (10)=-20 vs 0
- _makeGaussTex: default sigma=0 leaves centre-pixel alpha=0 vs 255
Each test is written to call without the value that makes the mutation
transparent (sigma passed without default; span=0 for the floor case;
fret=10 for the clamp case). Mutation verification run before commit:
all three mutations confirmed caught, all three pass on real code.
Suite (cmd): node --test tests/js/highway_3d*.test.js plugins/highway_3d/tests/*.test.js
Base (
|
||
|
|
84d33769b8 |
refactor(h3d-carve-1): extract pure geometry helpers to src/geometry.js
Moves 7 exports from screen.js to a new ES module src/geometry.js: geoFretX (was fretX, now 2-arg), dZ, slideTrailEnd, camBaseDistU, camLowFretPullbackU, computeBPM, _makeGaussTex. Internal helpers _fretXLog / _fretXUniStep / _fretXUni are module-private inside geometry.js. Module-level constants (SCALE, K, FRET_SCALE, NFRETS, FRET_SPACING_*, TS) are duplicated at geometry.js module scope — values are compile-time and never vary at runtime. screen.js keeps a 1-arg delegator (1 beyond-subst): const fretX = f => geoFretX(f, _h3dFretUniform); No call site changes; fretMid / fretColumnWorldW / slideOffsetWorldX / _recomputeFretSpacingDerived stay in screen.js and use the delegator. Tests: - highway_3d_geometry.test.js: class-killer via import() with 9 known-answer assertions (geoFretX uniform/log invariants, dZ linearity, slideTrailEnd, computeBPM BPM estimate). - highway_3d_fret_spacing: test updated to match delegator pattern + adds GEOMETRY_JS read to verify geoFretX export. - highway_3d_sustain_bloom: retargeted _makeGaussTex check to GEOMETRY_JS (definition moved); call-site check stays on SCREEN_JS. - highway_3d_panel_controls: vm loader strips ES import line and injects geometry stubs; all factory-statics tests unaffected. Suite: 169/169 pass node --test tests/js/highway_3d*.test.js plugins/highway_3d/tests/*.test.js Version bump: 3.35.0 -> 3.36.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW |
||
|
|
d5f622f31b |
refactor(h3d-carve-0): add scriptType:module to plugin.json
Zero logic changes. screen.js content byte-identical — an IIFE is valid
inside a <script type="module">. The loader consumes scriptType at
plugin-loader.js:666 (if plugin.script_type === "module" -> script.type =
"module"), fed from plugins/__init__.py:1429 (manifest.get("scriptType")).
Isolates the R7 module-flip risk as its own verifiable cut before any
function extraction begins.
Version bump: 3.34.1 → 3.35.0
Suite: 160/160 pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
|
||
|
|
7bce521a5b |
Merge fix/scan-prune-guard: refuse mass prune on automatic scans
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0175AhnWV84XBNuLFSS1CqRT |
||
|
|
cafb1ee790 |
fix(scan): mass-prune guard v2 — partial degraded listing refused on auto scan
The v1 zero-listing guard (
|
||
|
|
f6e9727b04 |
fix(scan): refuse to prune when listing returns 0 songs and DB is non-empty
When background_scan() discovers zero songs in the DLC directory but the
songs table is non-empty, skip delete_missing and return with stage='error'.
An empty listing on a non-empty library almost certainly means the DLC mount
was temporarily inaccessible (FUSE remount, NTFS dirty-flag RO fallback, brief
unmount mid-scan) rather than every song being genuinely deleted.
delete_missing({}) on a non-empty DB deleted ALL rows — the #P1-libpurge
incident that wiped 50,943 songs was caused by exactly this path.
The guard fires only when current_files is empty AND the DB has at least one
row, so a genuinely empty new library is unaffected.
Test: test_scan_prune_guard.py::test_empty_listing_refuses_prune_when_db_nonempty
Failing input: dlc dir with no feedpak/sloppak/wem + 1 DB row.
Before: delete_missing({}) fires, row gone.
After: stage='error', row survives.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2bM5jSbMskpdxm2CmuQVj
|
||
|
|
b739e44e3d |
Merge feat/career-gig-tuning: gig tuning preference + tuner interstitials
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0175AhnWV84XBNuLFSS1CqRT |
||
|
|
2ae46fe754 |
test(career): F2b — drive real closeBook() in in-flight-discard test (Creed closer)
The previous test for closeBook() invalidating in-flight requests bumped _ppBookGen directly via setBookGen instead of calling the production closeBook(). Deleting ++_ppBookGen from closeBook() left the test green — decoration. Fix: expose closeBook via the __careerPassportTest seam and call it directly in the test. Mutation proof (run before commit): - delete ++_ppBookGen from closeBook() → 1 fail (test RED) ✓ - restore → 14/14 pass (GREEN) ✓ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2bM5jSbMskpdxm2CmuQVj |
||
|
|
1033de87ce |
fix(career): gen check after error-branch json + closeBook invalidates in-flight (Creed F1b/F2b)
F1b (MEDIUM): the error path (non-ok response) lacked a gen check after res.json() completed. A stale 404's json() could finish after a newer booking had incremented _ppBookGen, and the error handler would still revert the newer pref to 'any'. Fix: `if (gen !== _ppBookGen) return;` immediately after the error-branch `await res.json().catch(...)`. Test: json() side-effect bumps _ppBookGen (simulating a new booking racing in), verifies pref stays 'standard'. F2b (LOW): closeBook() dismissed the poster but left _ppBookGen unchanged, so a still-pending successful bookGig response could land after dismissal and reopen the overlay / repopulate _ppGigProposal. Fix: `++_ppBookGen` in closeBook(). Test: pending request fires, gen bumped (simulating closeBook), response resolves, asserts _ppGigProposal stays null. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2bM5jSbMskpdxm2CmuQVj |
||
|
|
c264a66e5d |
fix(career): bookGig generation guard + 404-only pref revert (Creed F1/F2)
F1 (MEDIUM): bookGig had no request-generation guard. Rapid pref changes could let a stale response overwrite _ppGigProposal → user sees the wrong song set. Fix: _ppBookGen counter incremented on each request; response discarded unless gen === _ppBookGen at both the res.ok check and after json(). Stale-response driver test fails without the guard. F2 (LOW): every non-ok response reverted _ppGigTuningPref to 'any' and persisted it. A transient 500 would silently blow away the user's pref. Fix: pref reverted only on 404 (no-match case). Other errors notify but keep the pref. 500 driver test asserts pref stays 'drop' — fails without the fix. 404 driver still asserts revert to 'any'. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2bM5jSbMskpdxm2CmuQVj |
||
|
|
c46b6484bf |
fix(career): specific-tuning interstitial guard + 404-revert re-render (Toby F1/F2/F3)
F1 (HIGH): onGigSongLoading used `pref !== 'specific'` but production pref
is always 'specific:<name>' not bare 'specific' — guard was always true,
interstitial fired even when every gig song shared one tuning.
Fix: !pref.startsWith('specific:')
F2 (MEDIUM): JS test for specific-exemption used bare 'specific' (impossible
in production), giving false confidence. Updated to 'specific:E Standard',
which is the real production shape and correctly exercises the fixed guard.
F3 (MEDIUM): On 404-revert (_ppGigTuningPref → 'any'), poster was not
re-rendered so the stale pill from the previous successful booking stayed
highlighted while internal pref was already 'any'.
Fix: re-render overlay with gigPosterHTML(_ppGigProposal) before returning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2bM5jSbMskpdxm2CmuQVj
|
||
|
|
2a455702b8 |
feat(career): tuning preference filter + interstitial for gigs
Users can now pick a tuning preference before booking a gig: - Any (default), Standard only, Drop only, or a specific tuning - Backend filters the song pool (stubs + filler) by that preference - Empty-filter case returns a 404 with a descriptive message; frontend reverts the pref to 'any' and shows a notification - Interstitial pause before first song and on tuning changes (all prefs except 'specific') via window.feedBack.holdAutoplay(); opens the tuner panel in auto mode while the user retunes - 'Specific' gigs skip interstitials (every song already shares one tuning) - Graceful degradation: no holdAutoplay → interstitial silently skipped New backend: - _tuning_ok_fn helper for standard/drop/specific classification - _fill_genre_songs accepts optional tuning_ok filter - propose_gig batch-fetches tuning_name for played stubs, applies filter - GET /gigs/tunings endpoint for the specific-tuning picker Tests: - tests/test_career_gig_tuning.py — 17 Python tests (classification, filter) - tests/js/career_gig_tuning.test.js — 9 JS tests (interstitial logic) - tests/plugins/career/conftest.py — songs table schema gets tuning_name Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2bM5jSbMskpdxm2CmuQVj |
||
|
|
a57b62378c |
Merge feat/vocal-calibration-wizard: Vocals path + first-run vocal calibration handoff
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0175AhnWV84XBNuLFSS1CqRT |
||
|
|
d2847ab153 |
fix(vocal-cal): cancel fallback timer on Skip via _activeCleanup (Creed finding)
Root cause: clicking Calibrate in the vocalCalibration-absent path queued
an 1800ms setTimeout but never stored the timer id. If the user clicked
Skip before the timer fired, advance('vocals', false) resolved the wizard;
the stale timer then fired advance('vocals', true), mutating the completed
array after resolution and (with a multi-instrument queue) double-
incrementing idx so the next instrument was dropped from both lists.
Fix: capture the setTimeout return value as _timerId and assign:
_activeCleanup = () => clearTimeout(_timerId)
advance() already drains _activeCleanup on every exit path (Skip,
Calibrate, and any future button), so no further call sites needed.
The _advancing flag + calBtn.disabled remain as the double-click guard;
this fix covers the orthogonal Skip-before-timer race.
Test 3c (new): Calibrate then Skip before timer — asserts clearTimeout
called, vocals in skipped only (not completed), stale timer no-op.
Fails without the fix. Harness updated to expose clearTimeout to vm ctx.
Gates: JS 1141/1141, pytest 60/60.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2bM5jSbMskpdxm2CmuQVj
|
||
|
|
47e85d61c7 |
fix(vocal-cal): address Toby review F1/F2/F3
F1 (HIGH): add double-click guard to fallback path in renderAudioPanel.
- Declare `_advancing` flag before click handler; fallback else branch
returns early if already advancing, then sets flag + calBtn.disabled.
- Prevents two setTimeout advances from queuing when the button is
clicked twice before the 1800ms timer fires, which would silently
drop subsequent instruments from both completed and skipped.
- Test 4 (new): double-click with two instruments queued, asserts only
one timer is scheduled -- fails without the guard.
F2 (MEDIUM): add 'vocals' to _inputSetupRelaunch fallback list.
- Was ['guitar','bass','keys','drums']; now includes 'vocals' so the
Settings re-calibration wizard runs the vocals panel even when
/api/progression fails to respond.
- Test 6 (new): reads fallback literal from source, asserts 'vocals'
present -- fails without the fix.
F3 (LOW): key button label and notice off vocalCalibration presence for
vocals, not hasDetector (noteDetect).
- New `hasVocalCal` and `canCalibrate` vars; vocals shows 'Calibrate'
iff vocalCalibration facade present, 'Continue' otherwise.
- notLoadedNotice selects the correct per-instrument message.
- Test 7 (new): facade present + noteDetect absent => label 'Calibrate'.
Also: fix curly-quote string delimiters introduced by editor autocorrect
in prior commit -- replaced with straight ASCII quotes; restored original
curly apostrophes in prose content (isn’t, it’s).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2bM5jSbMskpdxm2CmuQVj
|
||
|
|
95f4bb80e1 |
feat(vocal-cal): Vocals path + input_setup vocal-calibration handoff
1. Add data/progression/paths/vocals.json (5 levels, instrument:vocals
challenges) so the Vocals tile appears on wizard step 4.
2. Add vocals to INSTRUMENTS in plugins/input_setup/screen.js (mode:audio)
so the wizard renders a Vocals panel when that path is selected.
3. In renderAudioPanel, branch vocals away from noteDetect.launchCalibration
to window.feedBack.vocalCalibration.launch({requester,onDone,onCancel}).
Guard: facade absent (vocal-highway plugin disabled) → shows a notice and
auto-advances via setTimeout; never hangs or throws.
4. Update test_bundled_content_loads_clean to expect the 'vocals' path id.
5. Add tests/js/vocal_calibration_handoff.test.js — 4 tests covering:
- vocals.json shape (id/icon/5 levels, namespaced challenge ids)
- INSTRUMENTS includes vocals
- fallback when facade absent (no hang/throw, auto-advance)
- facade.launch called with correct args, onDone resolves wizard
Facade contract: window.feedBack.vocalCalibration frozen {version:1,
launch({requester,onDone,onCancel})} — built by Dwight (vocal-highway).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2bM5jSbMskpdxm2CmuQVj
|
||
|
|
eef58c88c3 |
feat(sloppak): core reader for source rigs (feedpak 1.18.0) (#1040)
ship-ci / ci (push) Has been cancelled
* Carry rig bindings through the sloppak tone payload
`sloppak_tone_changes` emitted `{t, name}` only, so a chart's declared
sound never reached the client: `base_rig` was never read and each
change's `rig` was dropped at the wire boundary. Both survive load
intact (`Arrangement.tones` is an opaque passthrough) — the strip
happened here, at the last step before send.
That left the rig model (feedpak-spec 1.18.0 §6.9/§7.9) unreachable
from core: a pack could declare which rig voices a part, and nothing
downstream could ever see it. First step of the core reader for source
rigs; the rig library itself and the manifest precedence cascade follow.
Return `(base, base_rig, changes)` and keep `rig` on each change. Both
ids are validated as non-blank strings and stripped — anything else is
dropped rather than forwarded, so presence of the key means the change
binds a rig. Resolution against `rigs.json` deliberately does NOT happen
here: this builder preserves the declared binding, while realization
selection and the `intent.gm` fallback belong to whatever voices the
part.
On the wire `base_rig` is omitted entirely when empty, so packs that
bind no rig produce the byte-identical `tone_changes` message they
always did.
Signed-off-by: gionnibgud <gionnibgud@gmail.com>
* Load the pack's rig library from the manifest
feedpak 1.18.0 lets a chart declare what a MIDI part should sound like
by binding a rig id, but core had nothing to bind to: `rigs`, `base_rig`
and `drum_tones` appeared nowhere in lib/, server.py or static/. The
preceding commit carries the reference onto the wire; this adds the
library it references.
Read the manifest `rigs:` key into a new `LoadedSloppak.rigs`, alongside
the other side-files rather than on Song — every side-file (drum_tab,
song_timeline, keys, notation) hangs off the load result, and rigs is
pack-level, not per-arrangement. Same permissive posture as its
neighbours: missing, unreadable, malformed or traversing disables rigs
with a warning and never fails the pack, which §7.9 requires outright.
Rig objects pass through VERBATIM. §7.9 obliges a Reader to preserve
unknown role/engine/kind values and `ext` namespaces, so validating
block structure here would be wrong as well as premature — realization
selection and the `intent.gm` floor belong to whatever voices the part.
The only entries dropped are ones unreachable by construction: a rig is
addressable solely by `id`, so a non-dict entry or one without a usable
string id can never be referenced. Ids are stripped to match the
reference side, and a duplicate id resolves first-wins with a warning,
since ambiguity there would surface as the wrong sound rather than an
error.
Signed-off-by: gionnibgud <gionnibgud@gmail.com>
* Resolve which tones block binds a part
feedpak 1.18.0 lets a sound binding arrive from three places, and core
honoured none of them: the manifest arrangement entry, the arrangement
JSON, and the top-level drum_tones. Reading them needs a precedence
rule, because two of the three can be present at once.
Arrangement entries: the entry's `tones` replaces the arrangement JSON's
WHOLESALE (spec 5.2), unlike name/tuning/capo/centOffset beside it,
which override field by field. A merge would produce a sound nobody
authored -- one source's base under the other's changes -- which is
worse than either block alone. This is also what makes a notation-only
keys entry bindable at all, since it has no arrangement JSON to carry
tones in the first place.
Drums: the top-level drum_tones binds the song-level primary part, and
a `type: drums` entry's own tones takes precedence, with a Reader
forbidden from applying both to the same part (5.1). That is the same
shape as the drum_tab alias rule, so it lives inside
_resolve_drum_parts next to it rather than beside it -- one precedence
resolver, not two that drift. drum_tones is the PRIMARY's fallback
only: a second drummer with no binding gets None, never the primary's
kit.
An empty `tones: {}` reads as absent rather than as an override to
silence, matching how arrangement_from_wire already normalizes the
in-JSON empty dict, so a stray empty object cannot quietly unbind a
part.
Spec-conformance gate passes with drum_tones added to the keys core
reads (22 of the spec's 32, all declared).
Signed-off-by: gionnibgud <gionnibgud@gmail.com>
* Document the rig bindings on the tone_changes wire message
CHANGELOG entry for the core rig reader, plus the WS protocol table in
CLAUDE.md, which described `tone_changes` as carrying only base + name.
While in that row: its time key was documented as `time`, but every
producer emits `t` — both the sloppak builder and the legacy XML path.
The 3D highway already carries a comment warning readers about exactly
this discrepancy. Corrected here rather than left sitting next to the
newly-added keys, where a reader would reasonably assume both were
equally reliable.
Signed-off-by: gionnibgud <gionnibgud@gmail.com>
---------
Signed-off-by: gionnibgud <gionnibgud@gmail.com>
|
||
|
|
1a7e2bf084 |
Extract the pack-path containment guard into one helper (#1039)
Every manifest key that names a file carried its own copy of the same traversal guard: resolve, prove containment under source_dir, warn and skip on ValueError, warn and skip on OSError. Seven copies — original_audio, drum_tab, arrangement, notation, song_timeline, lyrics, keys — which is seven chances for the next side-file to get a security check subtly wrong by copying the wrong neighbour. Route them all through `_resolve_pack_path(source_dir, rel, label)`. Deliberately preserved, because each was load-bearing: - Both exception branches, with their different messages. ValueError means the path resolved outside the pack (a crafted or broken manifest); OSError means it could not be resolved at all (symlink loop, permissions). They send an operator to different places. - Per-call-site control flow. The helper returns `Path | None` and says nothing about what to do next, so the two sites that return, the one that continues, and the four that fall through to an `is not None` test each keep the shape they had. - The existence-check asymmetry. Some sites test `.exists()` (or `.is_file()`) after resolving and some do not, which is intentional — a missing optional side-file is silent, a missing arrangement skips an entry — so existence stays out of the helper entirely. Pure refactor: no behaviour change and no new validation. Log output is byte-identical (the hardcoded labels become a `%s` argument rendering to the same text). Full suite is unchanged at 2774 passed / 4 skipped before and after, and the five loader-level traversal tests that used to cover five separate copies of the guard now all exercise the same function. Signed-off-by: gionnibgud <gionnibgud@gmail.com> |
||
|
|
32c00cdd78 |
fix(count-in): follow the song's meter and its pickup measure (#1029)
The count-in always clicked exactly four beats, so a 3/4 song was counted in 4/4, and a song opening with a pickup (anacrusis) had the pickup enter where the downbeat belonged — putting the player a beat ahead all song. Bar length now comes from the song_timeline beats already on the highway (measure >= 0 marks downbeats), so no new plumbing: the time_signatures map is streamed to plugins rather than stored in the frontend. A first bar shorter than that meter shortens the count by its length — a 1-beat pickup in 4/4 counts "1 2 3" and the music enters on 4. Bar length is the mode of the downbeat gaps, not the first gap, so a pickup's own short gap can't be read as the meter; the beats trailing the last downbeat count as a candidate too, or a song of pickup + one bar offers only the pickup's gap. Pickup shortening is scoped to the song's first bar — a short bar elsewhere is a meter change, and is counted by its own length instead. Songs without beats (pre-chart, minigames, synthetic highways) still get four. Applies to both count-in paths: loop wrap / section practice, and the start-of-song 'Countdown before song' setting. Signed-off-by: gionnibgud <gionnibgud@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8297afc449 |
feat(tools): per-platform VST3 slicing for rig content packs (#1025)
ship-ci / ci (push) Waiting to run
Rebased onto merged main (was stacked on #1023/#1024, whose venue work is
now in main) so it no longer carries a stale content_packs.py that would
revert 1023's build_pack fixes.
- build_vst_pack: slice a fat .vst3 tree to one platform (keep its binary
dir + shared bundle files, drop the two foreign platform dirs and src/
build trees). Pins create_system=3 like build_pack — without it the same
tree hashes differently on a Windows runner (native .vst3 are built there),
breaking the precomputable-hash guarantee exactly where it matters.
- Publish wiring: 'python tools/content_packs.py <vst-root> --vst --version N
--publish' builds+uploads vst-<plat>-vN releases for mac/win/linux and emits
a platform-keyed {url,sha256,bytes} manifest — the shape rig_builder's
data/vst_packs.json consumes. publish() refactored onto a shared
_publish_release helper (venue behaviour unchanged).
- Tests: slice keeps target+shared/drops foreign, per-platform binary,
reproducibility, unknown-platform reject, and a simulated-win32 guard that
fails if the create_system pin is dropped. selfcheck covers the VST path.
Original build_vst_pack by Matthew Harris Glover; reworked for the create_system
fix, publish wiring, and rebase.
Signed-off-by: byrongamatos <xasiklas@gmail.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
59bcf338a3 |
feat(career): host higher venues as opt-in content packs (#1023)
* feat(career): host higher venues as opt-in content packs Move the club and arena venue packs (~678 MB of crowd MP4s) out of the bundle and download them on demand, keeping the bar starter bundled so career still works offline. Leans on career's existing pack pipeline (_download_pack: stream -> sha256 -> extract -> validate -> swap), which already degrades gracefully when a pack is absent. - venues.json: club/arena gain `pack` URLs pointing at per-pack, versioned, immutable releases (venue-<id>-v<N>, matching the existing venue-arena-v1). Arena's sha256/bytes are the real published asset (verified end-to-end); club is a placeholder until its release is published. - tools/content_packs.py: reusable, reproducible pack build/publish/manifest tool. Byte-identical output for identical media (fixed order/mtime/perms, STORED) so a pack's hash can be known before upload. --local (file://) for offline tests, --publish for the per-pack release. Has a --selfcheck. - .github/workflows/content-packs.yml: workflow_dispatch automation that builds/publishes packs and opens the venues.json manifest-bump PR, so publishing is never a manual checklist. - test: round-trips a tool-built pack through career's real _download_pack. Part of the nightly-slimming effort (feedBack-desktop#122). The desktop bundle change (stop shipping club/arena) is a companion PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(career): don't offer a venue pack until its release is published A committed venues.json entry carries a 0-byte placeholder (and all-zero sha) until its release exists. Previously has_pack was true as soon as a `pack` object was present, so the UI showed a "Download" button that could only fail (the placeholder URL 404s). Gate on a real, publish-stamped size via _pack_published(): the card shows "coming soon" and the download endpoint 404s until the pack is actually published. Caught by a real bundle+runtime smoke. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(content-packs): address CodeRabbit review on #1023 - workflow: stop interpolating dispatch inputs into Bash (template injection flagged by zizmor). Pass venues/version via env, validate formats, use an argument array. - content_packs: reject top-level files the career downloader would refuse (PACK_FILENAME_RE) before publishing — a stray .DS_Store would otherwise ship and fail _validate_pack_dir for every client. + test. - content_packs: pin ZipInfo.create_system=3 so packs hash identically across Windows/Unix runners (was the documented reproducibility caveat). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(changelog): note opt-in career venue packs (#122) Signed-off-by: Matthew Harris Glover <matthew@harrisglover.com> * docs(content_packs): correct --publish usage in module docstring --publish is a flag (no tag arg) and publish() deliberately omits --clobber; the docstring said otherwise. Signed-off-by: byrongamatos <xasiklas@gmail.com> --------- Signed-off-by: Matthew Harris Glover <matthew@harrisglover.com> Signed-off-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>vst-linux-v1 vst-mac-v1 vst-win-v1 |
||
|
|
03e1c1d57e |
feat(server): session-sync relay WebSocket /ws/sync/{session_id} (#1030) (#1032)
ship-ci / ci (push) Waiting to run
* feat(server): add session-sync relay WebSocket /ws/sync/{session_id}
Cross-device followers (splitscreen's upcoming LAN pop-out mode,
feedBack-plugin-splitscreen#21) need a machine-crossing replacement for
BroadcastChannel — the one link in the follower architecture that cannot
leave the host browser. Chart data already streams per-client over
/ws/highway, so all that's missing is a dumb live-state channel.
Add a fan-out room endpoint: a JSON text frame from one client is relayed
verbatim to every other client on the same session id. No schema, no
history, no persistence — rooms are created on first join and GC'd when
the last socket leaves. The statelessness is deliberate: an idle room is
indistinguishable from a nonexistent one, and a host that crashes and
rejoins the same id resumes publishing to reconnecting subscribers with
no server-side coordination.
Caps for a LAN-exposable port: 16 KB frames (1009), 16 sockets/room and
32 rooms (1013), 120 msg/s sustained / 240 burst per socket (1008),
text-only (1003), session id validated against [A-Za-z0-9_-]{4,64}. An
over-limit socket is closed individually; a peer that dies mid-fan-out
is dropped without wedging delivery to the rest.
Closes #1030
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>
* fix(ws_sync): bound stalled peer sends; cap ws frames at the transport
Review feedback (CodeRabbit on #1032):
- A peer that stops draining its socket left send_text() pending forever;
since publishers await the fan-out gather, one stalled peer stalled
every publisher's receive loop behind it. Fan-out sends are now bounded
by SEND_TIMEOUT_SECONDS (5 s) so a stall becomes an eviction through
the existing failed-send drop path.
- uvicorn buffers inbound WS frames up to its 16 MB default before the
handler's 16 KB check ever runs, so the DoS bound wasn't enforced at
the transport. main.py now passes ws_max_size=64 KB (no client sends
large frames: the highway WS receives only small control messages, and
the relay keeps its tighter application cap as the primary limit).
Regression tests for both; the desktop's own uvicorn spawn gets the
matching --ws-max-size flag with the feedBack-desktop follow-up work.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Kris Anderson <topkoa@gmail.com>
---------
Signed-off-by: Kris Anderson <topkoa@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
0e3522ccc3 |
feat(player): drum-part picker for multiple drum charts (re-land of #1021) (#1028)
ship-ci / ci (push) Waiting to run
* feat(player): drum-part picker for multiple drum charts (feedpak 1.17.0) The last mile of the multiple-drum-parts feature: let a player CHOOSE which drum chart plays. #1020 taught the loader + highway WS to carry several drum parts (song_info.drum_parts + ?drum_part=<id> + a part_id echo on drum_tab); this adds the host-chrome selector that drives it. A "Drum part" <select> sits beside the arrangement switcher in the advanced settings popover, shown only when a song has 2+ drum charts (drum_parts is always present — empty for non-drum songs — so single-drum / no-drum songs hide the row and nothing changes for them). Selecting a part re-streams that part's tab over the highway WS, exactly like an arrangement switch. - static/highway.js: - reconnect() gains a third `drumPart` arg → sets `?drum_part=<id>` on the WS URL (mirrors the existing `arrangement` param one line up). Empty/undefined → the primary part, i.e. byte-identical to today for any pack untouched. - song_info handler populates #drum-part-select from msg.drum_parts and shows/hides #v3-drum-part-row on `length > 1` (parallel to the #arr-select block right above it). - drum_tab handler carries msg.part_id onto hwState.drumTab (plugins can read bundle.drumTab.part_id) and reflects it as the picker's selected value, so the dropdown stays honest even when the server resolves an unknown/absent selection to the primary. - static/app.js: - changeArrangement() gains an optional `drumPart`; at reconnect it forwards the explicit part, else preserves the current picker selection — so an ARRANGEMENT switch keeps the chosen drum part (parts are song-level). - new changeDrumPart(id) delegates to changeArrangement with the current arrangement held + the new part applied (a part switch is the same re-stream, so it reuses all the transition ceremony). Exported on window. - static/v3/index.html: the #drum-part-select row (hidden by default). No plugin change: the drum renderers just draw whatever drum_tab streams. RUNTIME-VERIFIED (Playwright, the core player, a 2-drum pack + a no-drum pack): 10/10 — the picker populates with both parts and shows for the multi-drum song; song_info.drum_parts reaches getSongInfo(); the primary is pre-selected; selecting the 2nd part drives highway.reconnect with the id and the WS URL carries `?drum_part=drums-2`; the picker then reflects the server's part_id echo; a no-drum song hides the row; no page errors. ESLint 0 errors (the two max-lines warnings are pre-existing on these files). No pytest touched (JS-only). Stacked on #1020. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xGPjDBF8NTwTK7VQvizix Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com> * Update reconnect source contract test --------- Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e0270e5c30 |
fix(song): make bass detection instrument-type-aware, not name-only (#1019)
ship-ci / ci (push) Waiting to run
Editor now authors an arrangement's instrument as first-class data (a manifest 'type' field). Core dropped it: the sloppak loader never read 'type', and 'is this a bass?' was defined three different ways across call sites (name-only in note_pitch_midi and the highway scale-degree path; path_bass+name in bass selection; name-only in arrangement_string_count). So an authored type=bass chart not named 'bass' got 6-string lane counts and guitar open-string MIDI. - Add optional Arrangement.type; sloppak load_song lifts the manifest type onto it - Add arrangement_is_bass(arr) = type=='bass' OR path_bass OR 'bass' in name (None/whitespace safe), and route string count, note_pitch_midi, the highway scale-degree base, and bass-player selection through it - Back-compat: no bass signal -> unchanged 6-string / guitar behavior Companion to editor #335 (first-class instrument type). Scale degrees are display-only and never feed a grader. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
605dbdfd25 |
feat(sloppak): load multiple drum parts (feedpak 1.17.0 drums-as-arrangements) (#1020)
* feat(sloppak): load multiple drum parts (feedpak 1.17.0 drums-as-arrangements)
A song can now ship SEVERAL drum charts (a second drummer, an aux-percussion
layer). The Arrangement Editor already writes them per the feedpak 1.17.0 FEP
(feedpak-spec#63): the primary stays the song-level `drum_tab:` key (what this
app has always played), and each part rides the manifest as a `type: drums`
arrangement entry carrying a per-arrangement `drum_tab` file pointer and NO
note `file` — an entry this loader's file/notation gate already skips, which
is exactly why old builds are unaffected by such packs.
lib/sloppak.py:
- The arrangements loop collects drum-part pointer entries instead of merely
skipping them — but still NEVER turns one into a fretted Arrangement. That
skip is the grading invariant (an empty drum chart must not reach the
fretted pipeline / note-detection grading) and is now pinned by test.
- New `LoadedSloppak.drum_parts`: [{id, name, drum_tab}], primary FIRST. The
entry aliasing the song-level file contributes its id/name but is never
loaded twice (the primary's payload IS `loaded.drum_tab`, same object).
Legacy single-drum packs read as a one-part list; a pointer-only pack (a
writer omitted the alias) promotes its first part so has_drum_tab, the
default stream, and the drum-only placeholder keep working.
- The song-level drum_tab loading block is extracted verbatim into
`_load_drum_tab_file()` and shared by both paths, so every part gets the
same permissive posture: missing file → that part silently absent;
traversal / parse / validation failure → that part skipped with a warning,
never an aborted load. (The 9 pinned drumtab-load tests pass unchanged.)
lib/routers/ws_highway.py:
- `song_info` gains `drum_parts` (names only; always a list, empty without
drums) so a part picker can bind unconditionally.
- `?drum_part=<id>` on the WS URL selects which part's tab streams as the
`drum_tab`/`drum_hits` messages; the default and any unknown id fall back
to the primary — byte-identical legacy behavior. The `drum_tab` message
carries `part_id` only when a parts list exists, keeping the legacy frame
unchanged.
Tests: tests/test_sloppak_drum_parts.py (9) — the grading invariant +
parallel-ids pin, primary-first resolution with alias identity, legacy
one-part list, pointer-only promotion, per-part failure isolation (bad JSON,
path traversal, duplicate rels), and the drum-only placeholder with pointer
entries. Full suite: the only failures are 9 machine-environmental tests
(installed desktop plugins under LOCALAPPDATA, CRLF/path-shape assertions)
that fail identically on an untouched origin/main checkout on this box.
tools/check_spec_conformance.py passes against the spec's current HEAD
(`drum_tab` and `type` are declared keys); the semantics of the
per-arrangement placement land in feedpak-spec#63 — this PR should merge
after it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xGPjDBF8NTwTK7VQvizix
Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
* Fix drum-part review findings
* Normalize drum part pointer identities
* fix(sloppak): enforce drums grading invariant + green the suite
- Gate the drum-pointer skip on type FIRST: a type:drums/drum entry never becomes
a fretted Arrangement even if it carries a note file/notation (with drum_tab it
is collected as a drum part, without it dropped+warned). Closes the spec
§5.2/§7.5 MUST-NOT hole (a malformed drums+file entry was being fretted-graded).
- Make test_drum_pointer_with_wrong_type_logs_warning robust (attach handler to the
feedBack logger + set WARNING, restore in finally) and fix the root-cause level
leak in test_tuning_provider_isolation.py (finally restored the handler but not
the level, leaking ERROR onto the feedBack tree and turning the suite red under
full ordering).
- Restore the chart-transform CHANGELOG bullet (#952) the drum entry had truncated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
|