Commit Graph
514 Commits
Author SHA1 Message Date
byrongamatosandClaude Sonnet 4.6 b02c760aec fix(h3d-carve-14): restore dropped tw arg in tremoloOffsetWorldX call; distinguish mStr/mGlow fakes
Creed F1 (MED): slideRibbonUpdatePositions called tremoloOffsetWorldX(n, Tk) — the
move dropped the tw (ribbon width) argument. Helper signature is (n, chartTime, trailW);
with trailW=undefined every ribbon vertex position was NaN for notes with n.tr truthy
(tremolo slide sustains). Restored verbatim: tremoloOffsetWorldX(n, Tk, tw).

Creed F2 (LOW): _buildDrawNote gave getMStr and getMGlow identical fakeMat objects, so
swapping the wiring left the suite green. Now each string index gets its own named object
(mStr[i]/mGlow[i]); added behavioral assertion that gem core.material === mStrMats[s].

Tests: 26/26 note-renderer pass (+2 new kills: tests 25 and 26). Suite 1379/2 (same
2 pre-existing failures as 59bccef baseline).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
2026-09-05 21:38:18 +02:00
byrongamatosandClaude Sonnet 4.6 59bccefe00 fix(h3d-carve-14): Toby r1 — 4 findings resolved
F1 (dead DI wiring): remove 7 fn-refs absent from V-section pre-carve:
  xFret, pbBeg, pbEnd, arpeggioChordIdForNoteWithInferCache,
  arpHsBoundsForNote, chordWireHighDensity, muteXMat
  Wiring count 137→130 in both factory signature and screen.js call.

F2 (dead imports): trim geometry.js import to the 3 live symbols:
  dZ, slideTrailEnd, renderOrderForLayerAtZ (drop 8 unused).

F3 (counts coherence): header 136/30→130/24, test name and
  assertion 137→130, all coherent after F1.

F4 (behavioral kill): add two execution-path tests for drawNote via
  new Function sandbox with full DI stubs.
  - Negative: dt=-999 → early exit at line 452 → pNote.get()=0
  - Positive: dt=0 → gem body executes → pNote.get()≥2 (outline+core)
  Kill proof documented in test comments.

Suite: 1377 pass / 2 fail (same 2 pre-existing as c58c40f baseline).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
2026-09-05 21:17:27 +02:00
byrongamatosandClaude Sonnet 4.6 c58c40f1ed feat(h3d-carve-14): extract V-section (note renderer) into src/note-renderer.js
Carve cut 14 of the h3d-carve epic. Moves the full V-section (note renderer):
  drawNote, drawArpBrackets, drawNotedetectLabels, chordHarmonyLabels
  and 14 private helpers (~1,433 lines)
from screen.js into plugins/highway_3d/src/note-renderer.js
as a factory-DI ES module: createNoteRenderer({137 DI params}).

Beyond-subst (3 sites):
  _ndVerdictSawAlpha = true  → setNdVerdictSawAlpha(true)
  _ndVerdictMaxAlpha = v     → setNdVerdictMaxAlpha(v)
  _streakHits = 0/++         → setStreakHits(0/getStreakHits()+1)

Tests:
  - New: tests/js/highway_3d_note_renderer.test.js (22 assertions)
    wiring guard (137 params), export contract, chordHarmonyLabels
    behavioral kills, beyond-subst sentinel, tombstone checks
  - Updated 8 existing test files to search note-renderer.js
    alongside screen.js for moved patterns (h3d-carve-14 retarget)

Bumps plugin.json 3.49.0 → 3.50.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
2026-09-05 20:32:15 +02:00
byrongamatosandClaude Sonnet 4.6 66aa829362 h3d-carve-13: extract S-section lookahead helpers into src/camera.js
Partial cut (Option A) per contract ~/Feedback-harness/hive/plans/h3d-cut13-contract.md.

What moves:
- lookaheadEndTime (factory-private) — 1 beyond-subst: _measureStarts → getMeasureStarts()
- lookaheadBootstrapTime (exported)
- lookaheadComputeFretBounds (exported) — uses lowerBoundT from geometry.js (new import)
- lookaheadTargetWorldX (exported)

DI delta: +9 params to createCamera()
  Constants (+4): NFRETS=24, CAM_LOOKAHEAD_MEASURES=9, CAM_LOOKAHEAD_SEC=3.0, CAM_FRET_EDGE_BLEND=0.1
  Getter  (+1): getMeasureStarts: () => _measureStarts
  Fn refs (+4): validString, getChartAnchorAt, xFretMid, xFret
Total createCamera() DI: 57 (was 48)

Deferred to cut 15 (U-section entanglement):
  lookaheadSmoothCamStep, _applyNoteCamTargets — write S-section state vars
  Region B (per-frame cam target block, ~90 lines) — reads 8 update() locals
  Region C (song-change + bootstrap block, ~174 lines) — U-section init code

Call sites unchanged (destructured names unchanged):
  lookaheadComputeFretBounds: lines 7474, 7941
  lookaheadBootstrapTime: line 7939
  lookaheadTargetWorldX: lines 7943, 10062

Tests: 17 new (highway_3d_camera_lookahead.test.js), 2 retargeted to cameraSrc.
Suite: 1353/1355 pass (2 pre-existing failures in unrelated test files).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
2026-09-05 19:19:57 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 18:32:25 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 17:51:54 +02:00
byrongamatosandClaude Sonnet 4.6 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 4a45ed8 baseline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
2026-09-05 17:19:01 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 16:55:49 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 16:37:20 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 16:07:37 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 15:58:33 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 15:51:42 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 15:37:46 +02:00
byrongamatosandClaude Sonnet 4.6 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 0a91f4f (base) → 264/264 (+2 tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
2026-09-05 15:14:53 +02:00
byrongamatosandClaude Sonnet 4.6 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 a993d2b
Post-cut: 262/262 (+9 tests — 8 overlay class-killers + 1 panel-controls stub)

Named mutations verified RED:
  • createOverlay({ diagRenderCache: new Map() }) → test 6 RED (ref-severance)
  • void _DIAG_CACHE_MAX in screen.js         → test 4 RED (bare private)

plugin.json: 3.42.0 → 3.43.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
2026-09-05 14:53:03 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 12:40:34 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 10:24:40 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 09:55:36 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 09:49:23 +02:00
byrongamatosandClaude Sonnet 4.6 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 (c4eebe1 base) → 236/236 tip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
2026-09-05 09:43:30 +02:00
byrongamatosandClaude Sonnet 4.6 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 (f69c544) → tip 235/235 (+13: 13 new class-killers)
plugin.json: 3.40.0 → 3.41.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
2026-09-05 09:25:44 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 09:03:10 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 08:55:06 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 08:43:00 +02:00
byrongamatosandClaude Sonnet 4.6 6050b6262b fix(h3d): thread maxStrings param through resolveStringCount (Toby r1)
Toby r1 on c7f7c88: MAX_RENDER_STRINGS=6 hardcoded in utils.js is a
stale compile-time copy — if S_COL grows to 7 entries, resolveStringCount
silently clamps a 7-string chart to 6 and no test fails.

Fix: delegator-param pattern (mirrors fretX / geoFretX):
  - Remove const MAX_RENDER_STRINGS from utils.js
  - resolveStringCount(bundle, maxStrings) — param replaces the copy
  - _openStringPitchLabelsForTuning(bundle, songInfo, n, maxStrings) — same
  - screen.js import aliases (_resolveStringCountBase, _openStringPitchLabelsForTuningBase)
  - 1-line delegators in IIFE supply MAX_RENDER_STRINGS (= S_COL.length); zero call sites change
  - NSTR=6 kept (it is a fixed semantic fact about standard guitar, not a palette ceiling)

New test: 'maxStrings param is authoritative, not a hardcoded 6'
  — resolveStringCount({stringCount:7}, 7)=7; re-hardcode mutation → RED.
Wiring tests: delegator lines asserting _resolveStringCountBase/
_openStringPitchLabelsForTuningBase each receive MAX_RENDER_STRINGS.

Mutation-verified: re-hardcode 6 → 1 RED; original → 210/210 GREEN.
Suite: node --test tests/js/highway_3d*.test.js plugins/highway_3d/tests/*.test.js
207→210/210 pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
2026-09-05 08:02:00 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 07:52:41 +02:00
byrongamatosandClaude Sonnet 4.6 f75e91088f test(h3d): catch const-shadow mutation in T=mod assertion (Toby r1)
Toby r1 finding on 8ea123d: the count-based check /T\s*=\s*mod\s*;/g
matches even when the .then bodies declare 'const T = mod' — a local
shadow that leaves the module-level live-binding T permanently null.
Fix: add doesNotMatch(/(?:const|let|var)\s+T\s*=\s*mod/) to reject
any declaration form.

Mutation-verified: 7/8 RED under const-shadow, 188/188 GREEN on original.
Suite: 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
2026-09-05 07:42:18 +02:00
byrongamatosandClaude Sonnet 4.6 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 (5e401af): 180/180 pass.
Post-cut run: 188/188 pass.
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
2026-09-05 07:35:27 +02:00
byrongamatosandClaude Sonnet 4.6 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 (a1f7ad5): 172/172 pass.
Post-cut run: 180/180 pass.
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
2026-09-05 07:19:58 +02:00
byrongamatosandClaude Sonnet 4.6 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 (84d3376): 169/169
Tip:          172/172  (+3 discriminating tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
2026-09-05 07:05:24 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 06:50:52 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-05 06:35:26 +02:00
byrongamatosandClaude Fable 5 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
2026-09-04 14:35:47 +02:00
byrongamatosandClaude Sonnet 4.6 cafb1ee790 fix(scan): mass-prune guard v2 — partial degraded listing refused on auto scan
The v1 zero-listing guard (f6e9727) was bypassed by a partial degraded
mount: if even one song was visible, current_files was non-empty and
delete_missing ran freely, pruning every invisible DB row (Creed r1 HIGH).

Guard contract (god ruling):
- Auto scans (startup, periodic, /api/rescan): refuse when
  would_remove >= max(_PRUNE_MAX_ABS=1, _PRUNE_MAX_FRAC=0.5 * existing).
  Zero-listing also refused (would_remove == existing).
  Scan sets stage='error', leaves DB intact.
- /api/rescan/full: allow_mass_prune=True → guard logs a warning and
  proceeds; delete_missing runs (user-authorised explicit intent).

Changes:
- lib/scan.py: _PRUNE_MAX_ABS/FRAC constants; combined guard (zero +
  partial) before delete_missing; allow_mass_prune param on
  background_scan(); _scan_mass_prune_next global threaded through
  kick_scan() and _scan_runner()
- server.py: trigger_full_rescan calls kick_scan(allow_mass_prune=True)
- tests/test_scan_prune_guard.py: Case B (Creed partial, RED f6e9727→
  GREEN here) + Case C (full-rescan bypass)

Gates: pytest 2816/2816, JS 1155/1155

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2bM5jSbMskpdxm2CmuQVj
2026-09-04 13:45:04 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-04 13:18:46 +02:00
byrongamatosandClaude Fable 5 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
2026-09-03 18:49:02 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-03 18:34:02 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-03 18:27:12 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-03 18:13:53 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-03 18:02:57 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-03 17:52:30 +02:00
byrongamatosandClaude Fable 5 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
2026-09-03 15:42:27 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-03 14:37:34 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-03 14:11:22 +02:00
byrongamatosandClaude Sonnet 4.6 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
2026-09-03 13:57:19 +02:00
gionnibgudandGitHub 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>
2026-07-23 11:27:21 +02:00
gionnibgudandGitHub 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>
2026-07-23 11:26:35 +02:00
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>
2026-07-23 11:26:32 +02:00
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>
2026-07-23 01:01:51 +02:00
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
2026-07-23 00:09:46 +02:00