mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-10-04 03:57:54 +00:00
930b492fa5d09a7723f168a1f476d1944d4e081a
157
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
270cb39f41 |
Enable Linux nightly AppImage auto-update in the System settings UI (#999)
ship-ci / ci (push) Waiting to run
* fix(settings): enable Linux nightly AppImage auto-update in System settings
Fixes the Settings → System "App updates" panel so it actually works on
Linux, and adds Nightly as a selectable channel — previously missing
entirely, so Linux self-update couldn't be reached from this UI at all.
- The channel dropdown no longer gets permanently disabled the moment
the desktop bridge reports 'unsupported', which is the normal state
whenever the channel isn't Nightly on Linux. It stays enabled so the
user can switch to Nightly, the only way out of that state.
- Shows live download progress ("Downloading update… N%") and an
explicit button state machine (Check → grayed out while busy →
Restart now once staged), instead of a frozen "Checking…" during the
~1.5GB background download.
- Renders every status update from the triggering action's own return
value (checkNow()/setChannel()'s result) rather than a separate
follow-up getStatus() call, which can race against other state
changes and show a stale result even after a real success.
- setupAppUpdates() no longer re-syncs the channel to the backend on
every Settings-panel re-render — only once per page load — so a
redundant sync can no longer stomp an in-flight download's state.
- Routes update-flow events into the existing diagnostics.js
console-capture + contribute() snapshot API, so the user's existing
"Export Diagnostics" button now captures the full update decision
trace end to end — no new UI or log file. This diagnostic tracing is
what actually root-caused the bugs above, from real on-device
captures rather than guesswork.
Companion PR in feedBack-desktop (the underlying update engine).
Verified end-to-end on a Steam Deck: channel switch → check → live
download progress → restart button → relaunch onto the new build,
confirmed via a real Export Diagnostics capture showing a clean,
fully-accounted-for trace.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(settings): extract + unit-test the app-update status view; dedupe diag log
- Extract the status→UI state machine from renderFrom into a pure, exported
_appUpdateStatusView() (DOM-free) and cover it with tests/js — settings.js's
large module graph made importing it for a full harness impractical, so the
pure function is the testable seam. Behavior-preserving; renderFrom applies
the returned shape to the DOM exactly as before.
- Dedupe the [update-diag] renderFrom console line so the ~1.5s download poll
no longer floods the diagnostics ring buffer with byte-identical entries;
every real state/percent change still logs, and the structured contribute()
snapshot stays unconditional.
Left the 'audio_engine' diagnostics key as-is: the server export filters
client contributions to loaded plugin ids (diagnostics_bundle.py path-traversal
guard), so a dedicated key would be silently dropped from the bundle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Byron Gamatos <xasiklas@gmail.com>
---------
Signed-off-by: Byron Gamatos <xasiklas@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Byron Gamatos <xasiklas@gmail.com>
|
||
|
|
23c509322b |
Add gamepad/controller support (#1001)
* feat(input): add gamepad/controller support
Adds full gamepad/controller navigation and playback control, driven
by requests from players who use fee[dB]ack on a TV/console setup and
from wheelchair users for whom a controller is far more convenient
than a keyboard + mouse. Confirmed working end-to-end on a Steam Deck
across several rounds of on-device testing.
- static/v3/gamepad.js: polls navigator.getGamepads() and dispatches
synthetic keydown events (Arrow/Enter/Space/Escape) on the focused
element (falling back to document), reusing the app's existing
keyboard pipeline (static/js/shortcuts.js's scope-aware dispatcher,
player shortcuts, text-field/modal guards) instead of a parallel
action-mapping table. Only acts on gamepads reporting the W3C
"standard" mapping — which is what Steam Input presents for the
Deck's built-in controls, both in Gaming Mode and in Desktop Mode
via a non-Steam shortcut — so button order is guaranteed correct
and a non-standard/raw device safely no-ops instead of misfiring.
Handles Steam Input's virtual-pad duplicates (a real controller
plus 1-2 mirrored XInput slots) without spamming connect toasts or
losing input when the live pad isn't at index 0. Xbox-style face
button mapping: bottom face = Space (play/pause, and activates the
focused control), right face = Escape (back), top face reveals the
player screen's tool rail (focuses it into visibility via the
existing CSS :focus-within rule). D-pad/stick repeat while held,
mirroring OS keyboard auto-repeat.
- static/v3/gamepad-nav.js: fills the one real gap in that reuse
strategy — no screen but the song library grid had any arrow-key
navigation, and Chromium doesn't run native Enter/Space button
activation for untrusted synthetic events even when dispatched at
the focused element. Gated entirely on `!e.isTrusted`, so it only
ever reacts to gamepad-originated events and never touches real
keyboard/mouse users: emulates Tab-order (the sidebar + active
screen's real, already-focusable buttons/links) for Arrow keys,
explicitly .click()s the focused element for Enter/Space, and gives
Escape a consistent "go back" behavior — an existing in-screen back
button if one's visible (reusing each screen's own drill-down logic
for free), else the main menu. Every branch defers via
`e.defaultPrevented` to any screen that already handles the key
itself (the song grid, the player, settings), so nothing here
overrides existing behavior.
- static/v3/songs.js: adds real 2D d-pad/arrow-key navigation to the
song library's virtualized grid (only a slice of the library is
ever in the DOM), including fetching/scrolling off-screen rows into
view and correcting for the sticky filter toolbar's occlusion.
- static/v3/index.html: wires up the two new scripts.
* chore: regenerate stale tailwind.min.css
Rebuilt in a fresh clone (not the local working copy). Several plugin
directories (audio_engine, plugin_manager, community_charts, etc.) are
gitignored locally but present on disk from checking out plugin repos
for local dev/testing — Tailwind's content scan picks them up
regardless, so a rebuild against the contaminated local working copy
bakes in extra utility classes that don't belong in the real,
git-tracked build. A clean checkout reproduces CI's expected output
exactly.
* fix(gamepad): check all matching back buttons, not just the first
document.querySelector on the combined [data-ap-back], [data-albums-back],
#v3-pl-back selector only ever inspects the first match in DOM order —
since screens stay in the DOM (hidden, not removed) when you navigate
away, a hidden back button from an unrelated screen could sort before
the one that's actually visible, incorrectly falling through to
showScreen('v3-home') instead of clicking it. Uses querySelectorAll +
find(visible) instead.
* fix(gamepad): address CodeRabbit findings on connect/disconnect and grid nav
- gamepad.js: anyLiveConnectedPad -> anyLiveStandardPad, filtering by
mapping === 'standard' like firstLiveStandardPad already does, and
applied at the top of the gamepadconnected handler too. A still-
connected non-standard raw mirror could otherwise mask the real
pad's disconnect (toast never fires, polling never stops).
- songs.js _gpMove: an unset cursor now always seeds at index 0
before the first press, instead of applying that press's delta
immediately (ArrowDown/Right previously skipped straight past row
0; Left/Up only looked right by accident of clamping). Matches the
existing convention in shortcuts.js's legacy _handleLibArrowNav.
- songs.js _gpBlockedTarget: form-control/button blocking now
requires the element to be visible (offsetParent !== null), not
just present. Screens stay in the DOM hidden (not removed) when you
navigate away, so a real button focused on some other now-hidden
screen could leave document.activeElement pointing at it and block
all grid navigation indefinitely. (An el.closest('#v3-songs') scope
was tried first and reverted — it fixed that case but broke
blocking for the topbar search input, which lives outside
#v3-songs's DOM subtree even while v3-songs is active; visibility
is the distinction that actually matters, not DOM nesting.)
Skipped two CodeRabbit suggestions, verified against current code:
gating songs.js's grid keydown listener to synthetic-only events
would regress the real keyboard accessibility this PR intentionally
added (v3-songs' grid had none before); renaming the _gp* helpers to
drop their underscore prefix would break from this codebase's own
established module-private naming convention.
Verified in-browser: first arrow press lands on index 0, stale hidden
focus no longer blocks grid nav, the topbar search input still
correctly blocks it, and normal nav resumes after blur.
* test(gamepad): unit-cover the controller + nav state machines
- gamepad.test.js (10): standard-mapping filter, Steam Input duplicate-slot
dedup, disconnect masking, button edge-detection, d-pad/stick repeat timing,
analog deadzone — driven via a fake navigator + manual rAF queue.
- gamepad_nav.test.js (10): !isTrusted/defaultPrevented gating, arrow focus
traversal + clamping, hidden-element skipping, Enter/Space click activation
(not into text fields/body), Escape visible-back-button vs home fallback.
songs.js grid nav is left to on-device coverage (async + windowed-DOM heavy).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Byron Gamatos <xasiklas@gmail.com>
---------
Signed-off-by: Byron Gamatos <xasiklas@gmail.com>
Co-authored-by: Byron Gamatos <xasiklas@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
be49465540 |
fix(highway_3d): initialize camera before silent intros (#1002)
Co-authored-by: Byron Gamatos <xasiklas@gmail.com> |
||
|
|
05be9ebdbe |
Add new chart-transform plugin capability (#1000)
* Chart-transform plugin capability * PR comments * Cleanup * Fix markdown * CodeRabbit feedback Signed-off-by: Joe <jphinspace@gmail.com> --------- Signed-off-by: Joe <jphinspace@gmail.com> Co-authored-by: Byron Gamatos <xasiklas@gmail.com> |
||
|
|
1745b13ba7 |
feat(playlists): flag songs that are not in your current tuning (#1009)
* feat(playlists): flag songs that are not in your current tuning
Making the library's tuning filter instrument-aware does not repair playlists
already built under the old guitar-first behaviour. Those keep their
wrong-tuning songs, so a player still hits a surprise retune mid-practice and
reasonably concludes nothing was fixed.
Adds a per-playlist check: each row is marked against the player's current
tuning, with a summary ("3 of 24 songs are not in your tuning"), a filter to
show only those, and an explicit removal that lists every affected song by
title and states they stay in the library. Flagging is the feature -- nothing
is ever removed without being asked for, and removal reuses the existing
per-song DELETE rather than adding a bulk destructive endpoint.
Reuses the tuner capability's coverage report and `window.feedBack
.workingTuning`, the same pair the library cards already score against,
rather than introducing another source of truth.
Two deliberate departures:
- A coverage report reads "not covered" both for a real mismatch and for a
bail-out it could not evaluate. Only a report carrying an actual reason
counts as a mismatch; the rest render as unknown. This differs from the
library grid, which paints every not-covered song amber -- acceptable on a
grid, not on a hand-curated playlist where a false warning costs trust.
- With no tuning perspective available it makes no claim at all, rather than
defaulting to guitar and reproducing the original bug in a new place.
Playlist rows carry `tuning_offsets` and `bass_only`; a tuning *name* cannot
be scored, since two "Custom Tuning" rows are different tunings.
Fully correct once the instrument-aware tuning filter lands. That dependency
is confined to `rowTuningForCheck()` in static/v3/playlists.js, marked SEAM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFDokqh2H6mEjk1Kgbi6JW
Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
* build(tailwind): regenerate for the playlist tuning-check classes
CI's tailwind-fresh gate rebuilds static/tailwind.min.css and hard-fails if
the committed file differs. The new chip/summary/filter markup introduces
classes the previous build never saw.
Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
* fix(playlists): stay within the shipped Tailwind class set
Reverts the regenerated static/tailwind.min.css and reworks the tuning-check
markup to use only classes already in the committed sheet.
Regenerating that file is not reproducible off CI: nothing pins tailwindcss,
autoprefixer or caniuse-lite, so a local `npx -y tailwindcss@3.4.19` resolves
different browser data and rewrites unrelated bytes -- a clean checkout of
main rebuilds with the -webkit-backdrop-filter prefixes dropped. Committing
that output fails the tailwind-fresh gate no matter how many times it is
regenerated.
Six utilities were new: bg-fb-good/10, border-fb-accent/50,
hover:bg-fb-accent/10, list-disc, list-inside, max-h-48, plus gap-x-3/gap-y-2.
Substituted bg-fb-good/30, the amber border already used by the mismatch
state, hover:bg-fb-card, a literal bullet in a div, max-h-32 and gap-3. Visual
intent is unchanged.
The removal-confirm test pinned the <li> markup; it now accepts either
wrapper, since what it guards is that every song is named and escaped ahead
of any DELETE, not which element wraps it.
Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
* Use instrument tuning in playlist checks
---------
Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
cc75cb876a |
fix(library): tuning filter answers for your instrument, not always guitar (#1003)
* fix(library): tuning filter answers for your instrument, not always guitar The library indexed exactly one tuning per song, chosen guitar-first (lead > rhythm > combo, bass only as a last resort), and nothing consulted the player's instrument. A bassist filtering by tuning was shown the guitar chart's tuning, so playlists built by tuning contained songs needing a retune. Reported by a tester building bass practice sets; Covet "Shibuya" is the clean case, with a custom guitar tuning over a standard bass chart. Indexes each arrangement role's own tuning and makes the facet, filter, sort and labels answer for one perspective. `guitar-lead` reads the original unprefixed columns and adds no payload keys, so the default response is unchanged. The same defect existed inside guitar -- lead and rhythm charts can disagree -- so perspective is three-valued (guitar-lead, guitar-rhythm, bass) driven by one PERSPECTIVES table rather than parallel column families. Songs with no chart for the perspective fall back to the song-level tuning rather than vanishing (18 of 59 packs in the test library have no bass chart), but the fallback is marked inferred in the facet counts and on the row instead of being silently coalesced. "Only real charts" reuses the existing `arrangements_has` filter rather than adding one. Bass-specific handling, from measured content: - Bass tuning arrays are padded to six entries; charts never reference string index 4 or 5. Truncated to four before naming and grouping. - Grouping uses a canonical open-pitch key, so [-2,0,0,0] and [-2,0,0,0,0,0] are one facet row instead of two. - Offsets above +1 semitone are refused a name. Bassists tune down, near never up; one pack ships [5,5,5,5,4,4] (A-D-G-C, unplayable, and its own notes sit in the song's real key under standard tuning). Naming that would send a player to retune to a tuning that does not exist. Rhythm deliberately does not truncate -- padding is a bass finding, and cutting a seven-string array would invent a tuning the chart lacks. Adds an opt-in `tuning_match=playable` mode alongside exact match: a chart is offered when your lowest open pitch is at or below its lowest open pitch, so a five-string bass covers four-string standard and drop-D with no retune. Open strings only -- note range is not indexed and the scan stays manifest-only -- so it fails conservative: unknown low pitch is excluded, and the upper bound is unchecked and documented rather than guessed. Existing installs would otherwise never populate: the tree-signature fast path reports "unchanged" forever on a settled library. Rows with NULL marker columns re-extract, and the fast path is disabled until that backfill converges (writes use '' rather than NULL, so it self-clears). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SFDokqh2H6mEjk1Kgbi6JW Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com> * test(v3): accept the tuning-perspective indirection in the badge guard The album-art badge now reads shownTuningName(), so the source-pattern guard no longer matched the inline `tuning_name || tuning` form and CI went red. Accept the helper, and pin the helper's own fallback in a companion test so the guard still fails if a guitar player's tuning label is ever dropped. Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com> --------- Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2413991c5a |
feat(highway_3d): fret wires flash on a confirmed hit (#969)
ship-ci / ci (push) Waiting to run
* feat(highway_3d): fret wires flash on a confirmed hit The fret wires were static scenery: gold inside the anchor lane, grey outside, and nothing tied them to what the player was actually doing. Give them a job. Widen the lane/neck contrast so the wires around the active lane read as a focus cue, and flash the wires bracketing a note when a scorer confirms it. A fretted note lights the wire behind it and the wire it is pressed against; a chord lights only the outermost wires of its shape, so it reads as one bracketed block rather than a picket fence; an open string has no fret of its own and its gem is drawn as a slab spanning the lane, so it lights the lane's edge wires instead. Gated on the provider verdict, never the proximity heuristic -- the latter only means "near the strike line", so it would flash on every passing note whether or not it was played. With no scorer attached the neck behaves exactly as before. Emissive (and emissiveIntensity) carry the flash, not albedo: these are MeshStandard materials in a scene with no envMap, so raising albedo alone barely brightens them. Every value is a named constant -- see FRET_WIRE_* -- because the look is a taste call that wants tuning by eye, not a derivation. Signed-off-by: Kris Anderson <topkoa@gmail.com> * feat(highway_3d): cap the fret-wire flash at one outer pair Fast passages overlap their decay tails: consecutive notes on nearby frets left three, four, five wires glowing at once — the picket fence the chord rule was written to avoid, arriving through time instead of through a shape. The apply pass now decays every wire's glow state as before, but flashes only the outermost pair of the lit span (or the single wire when only one is above threshold). Interior wires keep decaying invisibly — the base tier loop re-seeds their materials each frame — so the bracket tightens naturally as the outer tails expire, and a hit inside the current span widens nothing. Net effect: at most two wires are ever lit, and everything currently glowing reads as one bracket, exactly like a chord. Signed-off-by: topkoa <topkoa@gmail.com> * feat(highway_3d): chord flash frames the lane, not the shape The lit lane strip spans the anchor's width (minimum ~4 frets), which can run a fret past the chord's outermost fret. The chord flash bracketed the shape (wire behind its lowest fret, wire at its highest), so on those anchors the bracket sat one wire INSIDE the lit lane — reading as misaligned rather than as a frame around what's lit. Chord hits now light the anchor lane's edge wires: the exact wires the lane strip itself spans, and the same pair open strings already use, so every hit shape inside a lane produces the same bracket. The shape's own outer pair survives only as the fallback for charts with no anchors. Fretted and open intensities merge into one entry (they light the same two wires now), and an all-open chord on an anchor-less chart still degrades to no flash rather than a bad index. Signed-off-by: topkoa <topkoa@gmail.com> * feat(highway_3d): gem rims flash string-coloured, wire-fashion On a confirmed hit the gem's outline now flashes in the STRING'S OWN colour with the same intensity treatment as the fret wires — the FRET_WIRE_HIT_INTENSITY emissive ramp, faded by the provider's alpha — instead of the fixed spring-green mHitBright rim. Just the rims: the lateral face fill keeps its existing green, and the sustain trail is untouched. Mechanics mirror the wires' pattern. mRimFlash[s] is one material per string (created with the other per-string materials, palette-retint aware, fog-exempt, disposed in teardown); drawNote() assigns it as the outline on a good verdict and records the verdict alpha into a per-frame per-string max (_rimFlashIn); the flash pass applies the intensity ramp once per string. Shared-per-string is the same compromise mGlow already makes — two same-string gems flashing in different phases share the brighter alpha. No decay tail of our own, deliberately: the material is only assigned while the provider confirms the note, and the provider's alpha already fades. When it goes silent the outline reverts, so idle intensity never shows. Signed-off-by: topkoa <topkoa@gmail.com> * feat(highway_3d): wire flash is a lightning strike, not a lingering glow The flash was instant-on with a 0.32 s exponential tail, and a held sustain kept re-feeding it — wires stayed lit for the whole note. The requested feel is a shock: light hits the frets, they jolt, it's over. The flash is now a one-shot pulse triggered on the input's rising edge: a near-instant crack up (RISE 25 ms), a fast fall (FALL 160 ms) shaped (1-u)^2 so it drops hard then eases out, with a 26 Hz flicker biting into the fall (the electric shudder — the crack itself stays clean), then hard zero. A held 'active' verdict keeps the input high continuously, which by construction triggers nothing new: one strike per hit, and the wires go dark while the note rings on. A re-strike after the provider goes silent re-triggers cleanly. Seeking backward or a long stall clears all pulse state, and a pulse whose strike time lands ahead of the playhead after a seek is discarded. The outer-pair bracket rule is unchanged — it now selects across pulses instead of decay tails. Knobs: FRET_WIRE_HIT_RISE / _FALL / _FLICKER_HZ / _FLICKER_DEPTH (replacing FRET_WIRE_HIT_DECAY). Signed-off-by: topkoa <topkoa@gmail.com> * fix(highway_3d): one wire strike per judged hit, not per wire edge The strike trigger was a rising edge on each WIRE's input, which merged distinct hits: two consecutive correct notes on the same fret kept that wire's input continuously high, so the second note produced no strike at all. The wires must respond to what the player did — one strike per judged hit-zone event. The trigger is now per event identity, using the same seen-map pattern as _sparkSeen: the first frame a note gets a good verdict its key (string|fret|time — or the chord key for a strum, which strikes once as a unit) lands in _fwStruck and requests a strike on its wires; the event never fires again however long its verdict stays live. Because every producer is gated, any nonzero input in the apply pass IS a fresh strike, so it restarts a pulse already in flight — a rapid re-hit on the same wire re-cracks instead of being swallowed. Seeks clear the map (replayed notes strike again); it is size-bounded like _sparkSeen. Envelope, flicker, and the outer-pair rule unchanged. Signed-off-by: topkoa <topkoa@gmail.com> * Revert the lightning-strike experiment — back to the decaying glow Reverts |
||
|
|
2f2a095e4c |
fix(venue): fly in once per set, not before every song (#978)
ship-ci / ci (push) Waiting to run
Tester, mid-gig: "the second song in the gig started when the first one ended. But it showed the flyover intro again." The flyover is arriving at the venue, and you arrive once. #968 stopped it replaying on an arrangement SWITCH (same filename), but a gig's song 2 is a genuinely different file, so it took the full-teardown path and played the arrival flyover again — the camera flew in from the back of the room before every track of the set. The play queue now answers isContinuation(): false for the first song of a set (or a standalone play — an arrival), true for song 2..N. onSongLoaded carries the room over to the new song's loop on a continuation, and only a real arrival plays the intro. Verified on the built AppImage: isContinuation goes false (song 1) -> true (song 2) across an advance, and song 2 no longer flies in. Also confirmed NOT a bug, same session: "didn't show the author for the second song." The credits card shows on a queue advance whenever the song carries authors — reproduced with a song that has them as the advanced-to track. The tester's song 2 simply had no `authors:` metadata (most auto-converted feedpaks don't). No code change. Tests: isContinuation across start/advance/clear, and that onSongLoaded gates the flyover on the continuation check. Both fail on pre-fix source. JS 1214/1214. |
||
|
|
e14ef64224 |
fix(playback): the song queue must survive a playSong wrapper that drops options (#977)
ship-ci / ci (push) Waiting to run
Tester: "Passports does not advance in the song queue." The play queue tells playSong "don't clear the queue I'm driving" by passing options.fromQueue. But window.playSong is wrapped by a CHAIN of plugins — nam_tone, midi_amp, fretboard, invert_highway, tabview — and each wrapper forwards only (filename, arrangement), silently dropping the options object. So fromQueue never reached playSong: it cleared the queue the instant its first song started, and a gig/album/playlist never advanced. Reproduced on the real build via a queue.start + a hooked clear(): the queue went inactive with 0 remaining immediately after start, and the clear stack ran through nam_tone -> midi_amp -> invert_highway -> fretboard -> session.js. Fixing six plugin wrappers is whack-a-mole and the next plugin re-breaks it. Fix it at the source instead: the queue raises an out-of-band flag (_consumeInternalPlay, one-shot) beside the wrapper chain, not through it, and playSong's clear-guard honours it. options.fromQueue stays as the in-band path. The flag is consumed on read so a later MANUAL play still abandons the queue. Verified on the real build: the gig queue stays active after start and advances on song:ended (Iron Maiden -> Blind Guardian), and a manual play still clears. Tests drive the real clear-guard against the queue for: a dropped-options wrapper (the bug), the one-shot manual-play-still-clears invariant, and the in-band fromQueue path on its own. All 3 fail on the pre-fix source. JS 1211/1211. |
||
|
|
365cec1d29 |
fix(career): gig song selection — full-genre pool, working re-roll, and the venue pack loads (#976)
* fix(career): a gig's song pool is the whole genre, and re-roll varies it Two tester reports, one root: the gig song pool was built from only two sets — songs played ON THIS PASSPORT'S INSTRUMENT, and songs never played AT ALL (`filename NOT IN song_stats`). A song played on a DIFFERENT instrument's arrangement is in neither: it has a stats row (so the "unplayed" filler skipped it), and its played bucket is that other instrument's, not this passport's. It could never be gigged. - "Metalcore says 137 songs only shows 1 in the gig list" — a library of metalcore all played on another instrument. Reproduced: a guitar passport with 137 bass-played metalcore songs got a 404, zero songs. The "1" the tester saw was whatever handful happened to be on-instrument or truly unplayed. - "Passport re-roll does not change songs" — a set drawn from that filler was the library's first N in table ORDER, every call. Re-roll re-proposes, so it returned the identical set. Reproduced: 3 proposals, byte-identical. _unplayed_genre_songs -> _fill_genre_songs: the pool is now every library song of the genre the set hasn't already picked (a stats row on some other instrument has no bearing on whether a song can be in THIS gig), and it is shuffled so re-roll actually re-rolls. Both reproduced against the real propose logic before the fix and pinned as regression tests (both fail on the pre-fix routes.py). Full career suite green. * fix(career): load the gig's venue pack when the gig starts Tester: "Venue doesn't load when starting song from passport. Loads standard particles." crowd.setManifest(venue) — the call that actually loads a venue's crowd/stage pack — is reached ONLY through pushCrowdManifest, and pushCrowdManifest is called ONLY from refresh(), the career tab's own reload. A gig navigates AWAY from the career tab to the player, so refresh() never runs during it. startGig set the venue override and nulled _appliedManifestVenue but never re-pushed, so the venue visualization turned on (3D highway) while its pack never loaded — the song played over the bare highway backdrop, or over whatever venue a previous refresh() had left applied. startGig now pushes the crowd manifest for the gig venue right after setting the override, using the career state the booking screen already fetched. This is a call-graph fact, not a guess (pushCrowdManifest has exactly one other caller and startGig is not it), but it is fixed by static analysis — I could not reproduce the user-visible symptom locally because this instance happened to have a manifest already applied from a prior refresh. On-device confirmation on a real passport gig is still owed. Guard test: startGig must push the manifest after setting the override (fails on the pre-fix source). Career suite green. |
||
|
|
1702afa379 |
feat(career): extract the whole setlist before the gig starts (no more waiting between songs) (#971)
ship-ci / ci (push) Waiting to run
* feat(career): extract the whole setlist before the gig starts
A feedpak is a zip, and the first play of one pays for its extraction into
sloppak_cache. Inside a set that cost landed BETWEEN songs: the player finished
a number and then sat there waiting for the next one to unpack, mid-gig.
A setlist is a known list up front, so unpack it all while the poster is still on
screen. New POST /gigs/prepare walks the set through resolve_source_dir; the
poster's Play button shows "Preparing set…" while it runs.
Best-effort by design, at every level:
- a corrupt pak in the set does not sink the prepare (it is reported in
`failed`; the play itself surfaces the error exactly as it does outside a
gig — slow beats blocked)
- a host without the library resolvers degrades to a no-op rather than 500
- a failed request just falls through to the old lazy extraction
Ordering matters and is pinned: the set is unpacked BEFORE the stage is borrowed
(venue/viz overwritten) and before the queue starts, so a proposal cancelled
while unpacking leaves nothing half-applied to unwind.
Tests unpack REAL zips rather than mocking the extractor: every song of the set
lands on disk before the first note, a re-prepare does not duplicate the unpack,
one bad pak still leaves the good one prepared, and no-library / empty-setlist
degrade cleanly. 18/18.
NB the other half of the gig report — the per-song results popup interrupting
the set (and worse, claimAutoExit'ing so the queue would not advance until it was
dismissed) — is fixed in the note_detect plugin repo, which is not part of this
checkout.
* fix(career): bound the prepare request; validate the setlist (PR #971 review)
Both CodeRabbit findings were right.
1. A HUNG PREPARE COULD BLOCK THE GIG FOREVER.
`await fetch(...)` only rejects on a network ERROR. A server that accepts the
connection and then never answers hangs indefinitely — and the gig would never
start. That makes this optimisation the exact thing the PR promises it can
never be: the reason you cannot play.
The request is now bounded by an AbortController (PREPARE_TIMEOUT_MS, generous
because unpacking a setlist is real work — but a CEILING, not a wait). Past it
we start the gig and let the first play extract lazily, as it always did. The
Play button is restored in a `finally`, so a timeout cannot strand the poster
on "Preparing set…" with Play disabled — which would have been the same bug
wearing a different hat.
2. THE `songs` BODY WAS UNVALIDATED.
A str is iterable: "abc" would have prepared three one-character "songs". And
the endpoint unpacks zips, so an arbitrary caller could ask for unbounded work.
Now list-only, string entries, blanks dropped, capped at MAX_GIG_SONGS.
Tests: the fetch is abortable and the button is re-enabled on EVERY path
including the abort; non-list bodies, non-string/blank entries, and an
oversized setlist. 50 career tests, JS 5/5, eslint clean.
* fix(career): path-traversal guard on prepare; a cap test that actually tests the cap
CodeRabbit again, and the first one is a real hole I put there.
1. PATH TRAVERSAL. sloppak.resolve_source_dir() does a bare `dlc_root / filename`
with NO containment guard — so `../../x` walks straight out of the library, and
my new endpoint handed it attacker-supplied filenames. Every filename now goes
through _resolve_dlc_path first, the same check every other filename-bound
handler applies. Pinned: `..`, backslash traversal, an absolute POSIX path and
a Windows drive path are all refused, and nothing outside the library is
unpacked.
2. THE CAP TEST WAS VACUOUS. It asserted `prepared == 0` against a fixture with no
library — where the endpoint exits before extraction — so it passed whether or
not MAX_GIG_SONGS existed. It now runs against a real library and asserts the
endpoint CONSIDERED at most MAX_GIG_SONGS of the 82 it was handed. Verified to
fail when the cap is removed.
Same class of mistake as the notedetect gigBlock: a test that passes for the
wrong reason. Worth saying out loud since it is twice in one day.
3. E702 — semicolon-joined statements in the new tests, split.
51 career tests; full suite green.
|
||
|
|
917d81c2d2 |
fix(highway): a SUPERSEDED renderer init is not a FAILED one (#970)
Starting a gig dropped the player onto the fallback 2D highway with no venue.
startGig() calls setViz('venue'), which installs the 3D renderer — whose init is
async — and then immediately starts its play queue. playSong() re-initialises
that same renderer a tick later. A renderer mints a fresh readyPromise per
init() and rejects the previous one with "superseded"; highway.js only checked
that the RENDERER OBJECT was unchanged, which it is. So it treated a healthy,
re-initialising renderer as a failed one, tore it down, and reverted to 2D:
renderer async init failure: Error: superseded
viz picker: reverted to default renderer (async-init-failure)
The guard now also checks the PROMISE identity: a rejection from an init cycle
the renderer has already moved on from is ignored. The renderer-identity guard
stays (a rejection for a renderer since REPLACED is also not ours), and a
genuine failure of the CURRENT cycle still reverts — both init() call sites go
through _setRenderer, which re-wires the handler every time, so the new cycle is
always watched.
Reproduced and fixed against the real build:
before: vizSelection=default viz-picker=default venue=inactive viz:reverted
after: vizSelection=venue viz-picker=venue venue=ACTIVE (no revert)
Also widens the paused-frame throttle's opt-out. The throttle fires whenever the
CHART CLOCK is stalled — not only on a pause, but through a count-in and the
credits/author overlay too. Its opt-out only asked "is a crowd video rolling",
but the venue scene animates on a clock of its own with no pack at all (backdrop
breathe, parallax, haze drift, warmth pulse — Math.sin(t) in the draw loop), so
that motion was still being throttled. It now claims frames for both sources; a
plain 3D highway with no venue reads motion mode 'off' and keeps the #654 GPU
saving.
HONEST CAVEAT on that second part: I could not get the throttle to fire in a
reproduction. A control run on the shipped code showed 100 draws/sec while
paused, not the ~10/sec a firing throttle would give — so the change is
defensible on its own terms (a stalled clock is genuinely not a static picture)
but it does NOT have a demonstrated symptom behind it. The viz fix above does.
Tests: the superseded guard, and that the throttle opt-out covers both motion
sources. All fail against the pre-fix source. eslint 0 errors; JS 1207/1207.
|
||
|
|
4e0e3c5417 |
fix(venue/highway): flyover replay on arrangement switch, venue on Virtuoso, and the paused throttle starving the venue (#968)
* fix(venue): don't replay the flyover on an arrangement switch; keep the venue off other screens Two bugs from a live career session. 1. CHANGING ARRANGEMENT REPLAYED THE ARRIVAL FLYOVER. changeArrangement() reloads the song through the normal load path, so highway.js re-emits `song:loaded` — same filename, new arrangement. The venue could not tell that from a fresh arrival, so it reset the machine and flew the camera in from the back of the room again, mid-set, every time the player switched lead -> rhythm. The player is already on stage. onSongLoaded now compares the filename. A repeat of the song already on stage keeps the video pipeline running and only re-syncs the mood: the performance restarts, so the loop follows the reset machine with a quiet crossfade, never the intro. A genuinely different song still gets the full teardown + flyover. 2. THE VENUE SHOWED UP ON THE VIRTUOSO HIGHWAY. The venue was gated purely on `isVenueViz()` — the selected visualization, which is a GLOBAL preference and says nothing about what is on screen. Virtuoso borrows the same highway_3d renderer for its practice charts, so with Venue selected it inherited the backdrop: the crowd and the stage behind a chromatic exercise. Selecting Venue is a preference for the PLAYER; it is not a licence to paint the venue over whatever else happens to be using the renderer. The venue is now gated on viz AND screen (`shouldBeActive`), and follows `screen:changed` — it tears down on leaving the player and rebuilds on return. Nothing else changes: stop() already unbinds the videos from the renderer, so deactivating is enough to clear the backdrop. Tests: both decisions exposed as pure predicates and pinned — arrangement switch vs new song (including the first load, and a malformed payload that must not suppress the flyover forever), and the venue's screen scope. The existing syncViz test encoded the OLD contract (activate regardless of screen), so it now states the new one and additionally asserts the venue does NOT activate on virtuoso. Includes a guard test: with Venue selected AND on the player, the venue IS active — without it, every "not active" assertion could pass vacuously. All 8 new/updated assertions fail against the pre-fix source. eslint clean; JS 1199/1199; pytest 2597 passed. * fix(highway): the paused-frame throttle was throttling the whole venue Pausing the song dropped the venue, the crowd and the stage to ~10 fps — "everything around the highway drops fps by a lot". draw() caps paused frames to one per _PAUSED_FRAME_INTERVAL_MS (100ms), on an assumption stated plainly in highway-constants.js: a heavy WebGL renderer "does a full render every frame even while paused. That is pure waste." That was true when a paused chart was a still picture. The venue broke the assumption. Its video backdrop keeps playing and its crowd reacts on a clock of their own, and BOTH are drawn into the same canvas as the notes — so a throttle aimed at static notes throttled the entire room. The scene only got a texture upload 10 times a second while the transport sat paused. Renderers can now declare that their picture is not static while the chart clock is stopped: an optional needsContinuousFrames(). The throttle is skipped only when it returns exactly true, and the probe fails closed — a renderer that doesn't implement it, or one that throws, keeps the throttle unchanged. So the GPU saving that motivated #654 survives everywhere it was actually valid. highway_3d implements it and claims continuous frames ONLY while a crowd video is genuinely rolling (bound, unpaused, not ended, readyState >= 2). With no venue pack — the common case — the paused scene really is static, so it keeps the throttle and the GPU still idles. Tests extend tests/js/highway_pause_throttle.test.js, which guards this code path source-level (the draw loop owns the rAF + WebGL lifecycle and is deliberately not reproduced in a vm — see the file header). The new guards pin that the capability GATES the early return rather than merely being called near it, that the probe fails closed on absent/non-function/throwing/truthy-but-not- true, and that the 3D renderer keys off the real video elements and can still return false. All 3 fail against the pre-fix source. eslint 0 errors; JS 1202/1202; pytest 2597 passed. |
||
|
|
2991612531 |
feat(career): bundle the AXA club venue pack (career stage 2) (#963)
ship-ci / ci (push) Waiting to run
The Velvet Room (50 stars) now ships in every build like the bar and arena: 4 reactive crowd loops, 2 stingers, and a balcony flyover intro rendered from the AXA Music Stage scene (110 spectators, state-scaled stage washes over the venue's own neon). Audio files are dive-bar placeholders until club-scale recordings land. The installed/delete test now asserts bundled-fallback semantics: with every venue bundled, deleting a downloaded pack reveals the bundled copy instead of uninstalling. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |