mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 01:44:31 +00:00
fix(highway_3d): hoist sY before createScoreFx to resolve TDZ
Root cause: const sY was declared 371 lines after createScoreFx({..., sY})
inside createFactory(). The shorthand {sY} reads the binding immediately
(not a closure), so every factory call threw ReferenceError: Cannot access
'sY' before initialization. highway.js caught it, reverted to 2D, emitted
viz:reverted. THREE.js was never requested.
Fix: hoist sY declaration to just before createScoreFx. Also hoist
_invertedCached, nStr, curX, and highwayCanvas above their first lexical
reference (all were closure false positives, but hoisting makes the code
unambiguously safe and keeps the new ESLint gate clean with 0 errors).
Hoist _bcPanel in bc-panel.js for the same reason.
Regression gate: eslint no-use-before-define (variables:true, functions:false)
scoped over plugins/highway_3d/ (screen.js + src/). Statically catches any
const/let used before its declaration in the factory — the whole class, not
just this pair. RED at broken tip (sY flagged): GREEN after fix.
Pre-existing violations surfaced (all closure false positives, none true TDZ
runtime bugs): highwayCanvas in _v3TopRightChromeBottom body, _invertedCached
and nStr in sY arrow body and DI getters, curX in getCurX DI getter, _bcPanel
in bc-panel.js function bodies — all resolved by hoisting; no silenced errors.
Bump plugin.json 3.53.0→3.54.0 (viz factory change per standing rule).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014uZ169yfoFYArXz962g7KW
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
a6efd4ee5e
commit
ffbbf3fb94
@@ -67,6 +67,23 @@ module.exports = [
|
|||||||
'import-x/no-cycle': 'error',
|
'import-x/no-cycle': 'error',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// highway_3d plugin — screen.js uses ES-module syntax (scriptType:module)
|
||||||
|
// so it needs module sourceType to parse. Add no-use-before-define here
|
||||||
|
// (variables: true, functions: false) to catch const/let TDZ violations
|
||||||
|
// inside factory functions across the whole plugin tree.
|
||||||
|
// Proven to flag the broken-tip regression (fix/h3d-viz-init-fallback):
|
||||||
|
// broken: const sY used at createScoreFx DI before its declaration → ERROR
|
||||||
|
// fixed: sY hoisted above createScoreFx call → clean (0 errors)
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'plugins/highway_3d/screen.js',
|
||||||
|
'plugins/highway_3d/src/**/*.js',
|
||||||
|
],
|
||||||
|
languageOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||||
|
rules: {
|
||||||
|
'no-use-before-define': ['error', { variables: true, functions: false }],
|
||||||
|
},
|
||||||
|
},
|
||||||
// Signed size exemptions (docs/size-exemptions.md) — raise the ceiling so
|
// Signed size exemptions (docs/size-exemptions.md) — raise the ceiling so
|
||||||
// registered files don't warn below it.
|
// registered files don't warn below it.
|
||||||
...SIZE_EXEMPTIONS.map(({ files, max }) => ({ files, rules: { 'max-lines': sizeRule(max) } })),
|
...SIZE_EXEMPTIONS.map(({ files, max }) => ({ files, rules: { 'max-lines': sizeRule(max) } })),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "highway_3d",
|
"id": "highway_3d",
|
||||||
"name": "3D Highway",
|
"name": "3D Highway",
|
||||||
"version": "3.53.0",
|
"version": "3.54.0",
|
||||||
"type": "visualization",
|
"type": "visualization",
|
||||||
"scriptType": "module",
|
"scriptType": "module",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
|
|||||||
@@ -3221,6 +3221,10 @@ import { createSceneInit } from './src/scene-init.js'; // h3d-carve-16
|
|||||||
// — never a per-frame querySelector (see CLAUDE.md "never run DOM queries
|
// — never a per-frame querySelector (see CLAUDE.md "never run DOM queries
|
||||||
// on a per-frame path") — and re-resolved only when a node detaches.
|
// on a per-frame path") — and re-resolved only when a node detaches.
|
||||||
let _v3HudEls = null;
|
let _v3HudEls = null;
|
||||||
|
// Hoisted above _v3TopRightChromeBottom so the function body reference
|
||||||
|
// is not flagged by no-use-before-define (closure false positive — read
|
||||||
|
// only at call time; original declaration was near the lifecycle flags).
|
||||||
|
let highwayCanvas = null;
|
||||||
// Returns the bottom edge (in overlay-canvas px, which are 1:1 CSS px on
|
// Returns the bottom edge (in overlay-canvas px, which are 1:1 CSS px on
|
||||||
// this overlay) of the lowest visible top-right v3 chrome element, or 0
|
// this overlay) of the lowest visible top-right v3 chrome element, or 0
|
||||||
// when none apply (classic v2 UI, or all hidden). Only called while the
|
// when none apply (classic v2 UI, or all hidden). Only called while the
|
||||||
@@ -3607,6 +3611,25 @@ import { createSceneInit } from './src/scene-init.js'; // h3d-carve-16
|
|||||||
// silent — see the live-latch handling in the per-gem loop below).
|
// silent — see the live-latch handling in the per-gem loop below).
|
||||||
let _susVerdictLatch = new Map();
|
let _susVerdictLatch = new Map();
|
||||||
|
|
||||||
|
// ── String-to-Y (respects invert) ─────────────────────────────────
|
||||||
|
// Declared here so createScoreFx (carve-10) can receive a direct
|
||||||
|
// sY reference; the original declaration at carve-6 was after the
|
||||||
|
// carve-10 block, putting sY in TDZ when createScoreFx read it.
|
||||||
|
//
|
||||||
|
// _invertedCached and nStr are hoisted here from their original
|
||||||
|
// positions (lifecycle flags and per-frame state blocks below) so
|
||||||
|
// the sY arrow body and the DI getter arrows in createScoreFx are
|
||||||
|
// not flagged by no-use-before-define. Both read these bindings
|
||||||
|
// only at call time (closure), never at factory-init time.
|
||||||
|
let _invertedCached = false;
|
||||||
|
// Active string count for the current arrangement (resolved each
|
||||||
|
// frame from bundle.stringCount and clamped to MAX_RENDER_STRINGS).
|
||||||
|
let nStr = NSTR;
|
||||||
|
// curX hoisted for the getCurX DI getter arrow below (closure, read
|
||||||
|
// at call time). Initial value assigned later at xFretMid init.
|
||||||
|
let curX;
|
||||||
|
const sY = s => S_BASE + (_invertedCached ? s : (nStr - 1 - s)) * S_GAP;
|
||||||
|
|
||||||
/* ── h3d-carve-10: K-section (score FX) → src/score-fx.js ──────── */
|
/* ── h3d-carve-10: K-section (score FX) → src/score-fx.js ──────── */
|
||||||
const { fxInit, fxTeardown, fxSpawnPop: _fxSpawnPop, drawScoreFx, fxClearSeen } = createScoreFx({
|
const { fxInit, fxTeardown, fxSpawnPop: _fxSpawnPop, drawScoreFx, fxClearSeen } = createScoreFx({
|
||||||
getHighwayCanvas: () => highwayCanvas,
|
getHighwayCanvas: () => highwayCanvas,
|
||||||
@@ -3699,9 +3722,6 @@ import { createSceneInit } from './src/scene-init.js'; // h3d-carve-16
|
|||||||
// Per-fret last-active timestamp for lane persistence
|
// Per-fret last-active timestamp for lane persistence
|
||||||
let fretLastActiveTime = new Array(NFRETS + 1).fill(0);
|
let fretLastActiveTime = new Array(NFRETS + 1).fill(0);
|
||||||
|
|
||||||
// Active string count for the current arrangement (resolved each
|
|
||||||
// frame from bundle.stringCount and clamped to MAX_RENDER_STRINGS).
|
|
||||||
let nStr = NSTR;
|
|
||||||
// Set true once a chart with out-of-range s indices has triggered
|
// Set true once a chart with out-of-range s indices has triggered
|
||||||
// its warning. Reset only on teardown or when nStr changes (e.g.
|
// its warning. Reset only on teardown or when nStr changes (e.g.
|
||||||
// arrangement switch from guitar to bass) — same-nStr songs share
|
// arrangement switch from guitar to bass) — same-nStr songs share
|
||||||
@@ -3866,7 +3886,7 @@ import { createSceneInit } from './src/scene-init.js'; // h3d-carve-16
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
let tgtX = xFretMid(CAM_LOCK_CENTER_FRET), curX = xFretMid(CAM_LOCK_CENTER_FRET);
|
let tgtX = xFretMid(CAM_LOCK_CENTER_FRET); curX = xFretMid(CAM_LOCK_CENTER_FRET); // curX declared above (DI hoisting)
|
||||||
let tgtDist = CAM_DIST_BASE, curDist = CAM_DIST_BASE;
|
let tgtDist = CAM_DIST_BASE, curDist = CAM_DIST_BASE;
|
||||||
// Dolly-back multiplier applied to the curDist lerp target by camUpdate's
|
// Dolly-back multiplier applied to the curDist lerp target by camUpdate's
|
||||||
// fret-row fit guard. 1 = no extra pull-back (the common case); rises
|
// fret-row fit guard. 1 = no extra pull-back (the common case); rises
|
||||||
@@ -3958,11 +3978,11 @@ import { createSceneInit } from './src/scene-init.js'; // h3d-carve-16
|
|||||||
// Lifecycle flags
|
// Lifecycle flags
|
||||||
let _isReady = false;
|
let _isReady = false;
|
||||||
let _destroyed = false;
|
let _destroyed = false;
|
||||||
let _invertedCached = false;
|
// _invertedCached hoisted to before sY — see comment there.
|
||||||
let _invertedForBoard = false;
|
let _invertedForBoard = false;
|
||||||
let _leftyForBoard = false;
|
let _leftyForBoard = false;
|
||||||
let _initToken = 0;
|
let _initToken = 0;
|
||||||
let highwayCanvas = null;
|
// highwayCanvas hoisted to before _v3TopRightChromeBottom — see comment there.
|
||||||
|
|
||||||
// ── Focus state (splitscreen dim) ─────────────────────────────────
|
// ── Focus state (splitscreen dim) ─────────────────────────────────
|
||||||
let _focusSubscribed = false;
|
let _focusSubscribed = false;
|
||||||
@@ -3985,9 +4005,6 @@ import { createSceneInit } from './src/scene-init.js'; // h3d-carve-16
|
|||||||
if (dirLight) dirLight.intensity = focused ? 0.8 : 0.35;
|
if (dirLight) dirLight.intensity = focused ? 0.8 : 0.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── String-to-Y (respects invert) ─────────────────────────────────
|
|
||||||
const sY = s => S_BASE + (_invertedCached ? s : (nStr - 1 - s)) * S_GAP;
|
|
||||||
|
|
||||||
// ── h3d-carve-6: material builders ───────────────────────────────────
|
// ── h3d-carve-6: material builders ───────────────────────────────────
|
||||||
// TXT_STYLES, txtMat, pinchHarmonicMat, naturalHarmonicMat,
|
// TXT_STYLES, txtMat, pinchHarmonicMat, naturalHarmonicMat,
|
||||||
// palmMuteXSpriteMat, fretHandMuteXSpriteMat, muteXMat,
|
// palmMuteXSpriteMat, fretHandMuteXSpriteMat, muteXMat,
|
||||||
|
|||||||
@@ -247,6 +247,12 @@ let _bcPane = null, _bcListEl = null, _bcFilterEl = null, _bcPaneOpen = false, _
|
|||||||
function _bcStatusMark(name) {
|
function _bcStatusMark(name) {
|
||||||
return _bcFavorites.has(name) ? '★ ' : (_bcBanned.has(name) ? '🚫 ' : '');
|
return _bcFavorites.has(name) ? '★ ' : (_bcBanned.has(name) ? '🚫 ' : '');
|
||||||
}
|
}
|
||||||
|
// Hoisted above the functions that reference it so no-use-before-define
|
||||||
|
// does not flag closure reads inside _bcSetHold, _bcLayout, _bcSetPane,
|
||||||
|
// _bcUpdatePanelPreset. All are closures — _bcPanel is read at call time,
|
||||||
|
// not at module-evaluation time. Original declaration was at line ~316.
|
||||||
|
let _bcPanel = null, _bcPanelKeyBound = false;
|
||||||
|
|
||||||
function _bcSetHold(v) {
|
function _bcSetHold(v) {
|
||||||
const s = _bcLoadSettings();
|
const s = _bcLoadSettings();
|
||||||
s.hold = !!v; _bcSaveSettings();
|
s.hold = !!v; _bcSaveSettings();
|
||||||
@@ -313,7 +319,7 @@ function _bcUpdatePanelPreset() {
|
|||||||
if (_bcPaneOpen) _bcRenderList();
|
if (_bcPaneOpen) _bcRenderList();
|
||||||
}
|
}
|
||||||
|
|
||||||
let _bcPanel = null, _bcPanelKeyBound = false;
|
// _bcPanel hoisted to before _bcSetHold — see comment there.
|
||||||
function _bcEnsurePanel(host) {
|
function _bcEnsurePanel(host) {
|
||||||
if (_bcPanel && _bcPanel.isConnected) {
|
if (_bcPanel && _bcPanel.isConnected) {
|
||||||
// Singleton panel: follow the active highway. If it's still parented
|
// Singleton panel: follow the active highway. If it's still parented
|
||||||
|
|||||||
@@ -462,3 +462,47 @@ test('createScoreFx({...}) wiring has correct naming correspondence (no param sw
|
|||||||
|
|
||||||
assert.deepEqual(violations, [], 'createScoreFx wiring violations found');
|
assert.deepEqual(violations, [], 'createScoreFx wiring violations found');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── TDZ regression guard (fix/h3d-viz-init-fallback) ────────────────────────
|
||||||
|
// Input that FAILS at broken tip: calling window.feedBackViz_highway_3d()
|
||||||
|
// (i.e. createFactory()) throws ReferenceError: Cannot access 'sY' before
|
||||||
|
// initialization — const sY was declared 371 lines AFTER createScoreFx({...,
|
||||||
|
// sY}), putting sY in the temporal dead zone on every factory invocation.
|
||||||
|
// Result: viz picker fell back to 2D immediately; THREE.js never requested.
|
||||||
|
//
|
||||||
|
// Guard: ESLint no-use-before-define (variables:true, functions:false) scoped
|
||||||
|
// over screen.js and src/ — statically flags ANY const/let used before its
|
||||||
|
// declaration in the factory, catching the entire class not just this pair.
|
||||||
|
// At the broken tip this rule errors on sY; after the fix it is clean.
|
||||||
|
// Prefer this semantic gate over source-scan byte-offset comparisons, which
|
||||||
|
// miss TDZ bugs invisible to regex (this was the FOURTH such break).
|
||||||
|
test('no-use-before-define gate is clean on highway_3d screen.js and src/ (fix/h3d-viz-init-fallback)', () => {
|
||||||
|
const { execSync } = require('node:child_process');
|
||||||
|
const repoRoot = path.join(__dirname, '..', '..');
|
||||||
|
let stdout;
|
||||||
|
try {
|
||||||
|
stdout = execSync(
|
||||||
|
'npx --yes eslint@9.39.4 --format json plugins/highway_3d/screen.js plugins/highway_3d/src/',
|
||||||
|
{ cwd: repoRoot, encoding: 'utf8' },
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
// eslint exits non-zero when errors exist; output is still on stdout.
|
||||||
|
stdout = err.stdout || '';
|
||||||
|
}
|
||||||
|
const results = JSON.parse(stdout);
|
||||||
|
const tdzErrors = [];
|
||||||
|
for (const file of results) {
|
||||||
|
for (const msg of file.messages) {
|
||||||
|
if (msg.ruleId === 'no-use-before-define' && msg.severity === 2) {
|
||||||
|
tdzErrors.push(`${path.relative(repoRoot, file.filePath)}:${msg.line} — ${msg.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.deepEqual(
|
||||||
|
tdzErrors,
|
||||||
|
[],
|
||||||
|
'no-use-before-define errors found in highway_3d — a const/let is used before its ' +
|
||||||
|
'declaration in the factory (real TDZ risk). At the broken tip, sY was used in ' +
|
||||||
|
'createScoreFx({..., sY}) 371 lines before its declaration.\n' + tdzErrors.join('\n'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user