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:
byrongamatos
2026-09-06 12:58:19 +02:00
co-authored by Claude Sonnet 4.6
parent a6efd4ee5e
commit ffbbf3fb94
5 changed files with 95 additions and 11 deletions
+44
View File
@@ -462,3 +462,47 @@ test('createScoreFx({...}) wiring has correct naming correspondence (no param sw
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'),
);
});