Skip to content

refactor(app): make app.js's window contract explicit — 66 names (R3a) - #874

Merged
byrongamatos merged 1 commit into
mainfrom
feat/r3-window-contract
Jul 11, 2026
Merged

refactor(app): make app.js's window contract explicit — 66 names (R3a)#874
byrongamatos merged 1 commit into
mainfrom
feat/r3-window-contract

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Step 2 of the core-frontend ES-module migration (R3a), on top of #871 and #872. The load-bearing PR — everything the flip needs, landed on its own so it can bake.

What and why

static/app.js is a classic script, so each of its 385 top-level function foo() declarations is implicitly a property of window. As an ES module it will not be — module scope is not global scope — and every name reached from outside the file would silently vanish.

This is provably a no-op today. All 66 names are top-level function declarations, so while app.js is still classic, Object.assign(window, {…}) merely re-assigns what window already has. That's exactly what makes it safe to ship alone, ahead of the flip that depends on it.

The consumer set is wider than index.html

The obvious source is the shell's inline on*= handlers. It is not the only one:

Source Why it's easy to miss
static/v3/index.html inline handlers the obvious one
handlers app.js builds in template literals goFavPage, updatePlugin, hideScanBanner, … resolve against window at click time, but live in a JS string — scanning the HTML never finds them
static/v3/*.js (34 files) showScreen alone has 17 consumers
capabilities/visualization.js:301 reads window.setViz behind a typeof guard — losing it degrades in silence, no error
feedback-desktop + the plugin repos they live in other repos; no core test covers them

Constitution §II names window.playSong / window.showScreen / window.feedBack as the public extension contract — so this is an obligation, not a convenience.

The four names no static tool can see

app.js:2156-2157 picks the handler name at runtime:

const letterFn = favoritesOnly ? 'filterFavTreeLetter' : 'filterTreeLetter';
const pageFn   = favoritesOnly ? 'goFavTreePage'       : 'goTreePage';

then interpolates it: `<button onclick="${letterFn}('A')">`.

The names exist only inside string literals. ESLint, no-undef, jsscan, and any grep for onclick="fn all miss them. They are the library A–Z rail and its pagination — drop one and those buttons throw at click time and nowhere else. They're hardcoded in the test with a ponytail: comment saying why.

The guard

New tests/js/window_contract.test.js scrapes on*= handlers from both the shell and app.js's template literals, and pins the 4 runtime-composed names by hand.

Verified to bite — a guard that passes both ways guards nothing:

removed from the contract result
showScreen inline handlers that would break under type="module"
goTreePage window.goTreePage is required by the runtime-composed A–Z rail / pagination handlers
setViz window.setViz is read by another file

Verification

  • In-browser: all 66 names resolve on window; the tree view renders 28 A–Z rail buttons carrying their real onclick sources (filterTreeLetter('A'), …) and 8/8 execute with no ReferenceError.
  • pytest 2396 passed · test:js 1032/1032 (4 new) · ESLint 0 errors · tailwind-fresh clean.
  • Codex preflight 0 — it wrote its own independent scan of both the HTML and app.js's template literals and found zero unexposed handlers.

Not done here

The 21 desktop-only internals (_librarySongId, renderTreeInto, _trapFocusInModal, …) are exposed verbatim — they're an accidental API surface worth narrowing, but that's a behaviour change and doesn't belong in a move-only PR. Worth a follow-up issue.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Preserved existing app interactions and controls when the application is loaded using modern module behavior.
    • Improved compatibility for inline actions, navigation controls, pagination, and other dynamically generated interface actions.
    • Added safeguards to ensure all required interactive actions remain available and responsive across the application.

app.js is a classic script, so each of its 385 top-level `function foo()` decls
is implicitly a property of `window`. As an ES module it will not be — module
scope is not global scope — and every name reached from outside this file would
silently vanish. This adds the explicit `window.*` assignments BEFORE the flip.

Provably a NO-OP: all 66 are top-level function declarations, so while app.js is
still a classic script `Object.assign(window, {...})` only re-assigns what
`window` already has. That is what makes it safe to land on its own, ahead of
the flip that needs it.

The consumers are wider than the inline handlers in index.html:
  - inline on*= handlers in static/v3/index.html
  - on*= handlers app.js BUILDS inside template literals (goFavPage,
    updatePlugin, hideScanBanner, ...) — they resolve against window at CLICK
    time, but live in a JS string, so scanning the HTML alone never finds them
  - static/v3/*.js (showScreen alone has 17 consumers), capabilities
  - feedback-desktop and the external plugin repos — easy to miss, they live in
    other repos and no core test covers them
  - capabilities/visualization.js reads window.setViz behind a `typeof` guard,
    so losing it DEGRADES IN SILENCE rather than throwing

Constitution II names window.playSong / window.showScreen / window.feedBack as
the public extension contract, so this is an obligation, not a convenience.

FOUR names are invisible to every static tool. app.js:2156-2157 picks the
handler NAME at runtime —
    const letterFn = favoritesOnly ? 'filterFavTreeLetter' : 'filterTreeLetter';
— and interpolates it into `onclick="${letterFn}('A')"`. The names exist only
inside string literals, so ESLint, no-undef, and any grep for `onclick="fn` all
miss them. They are the library A-Z rail and its pagination: drop one and those
buttons throw at click time and nowhere else.

New tests/js/window_contract.test.js scrapes the HTML's handlers AND app.js's
template-literal handlers, and pins the 4 runtime-composed names by hand.
Verified to BITE: dropping showScreen, goTreePage, or setViz each fails it with
the right message.

On-device: 28 A-Z rail buttons render with their real onclick sources
(filterTreeLetter('A'), ...) and 8/8 execute with no ReferenceError; all 66
names resolve on window in the browser.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an explicit window export contract for app functions and tests that inline, runtime-composed, and cross-file handler names resolve against that contract.

Changes

Window Contract Exposure

Layer / File(s) Summary
Global window export contract
static/app.js
Adds an Object.assign(window, { ... }) block exposing selected app functions globally.
Window contract validation
tests/js/window_contract.test.js
Adds static checks for exported names referenced by HTML handlers, template literals, runtime-composed handlers, and cross-file readers.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: making app.js's window contract explicit with 66 exported names.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/r3-window-contract

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/js/window_contract.test.js (1)

46-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Broaden the handler regex in tests/js/window_contract.test.js:46. The current event list misses handlers already present in the scan target, e.g. ondblclick in static/v3/index.html, so the guard can skip inline handlers on uncovered events.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/js/window_contract.test.js` at line 46, Broaden the HANDLER regular
expression to recognize all inline event handler names present in the scan
target, including ondblclick, while preserving the existing handler-name capture
and matching behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/js/window_contract.test.js`:
- Line 46: Broaden the HANDLER regular expression to recognize all inline event
handler names present in the scan target, including ondblclick, while preserving
the existing handler-name capture and matching behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b7a344ef-d848-4afe-bfca-19334a38ab8b

📥 Commits

Reviewing files that changed from the base of the PR and between 4b4c156 and 69002c1.

📒 Files selected for processing (2)
  • static/app.js
  • tests/js/window_contract.test.js

@byrongamatos
byrongamatos merged commit ff7e855 into main Jul 11, 2026
5 checks passed
byrongamatos added a commit that referenced this pull request Jul 11, 2026
One attribute. #871/#872/#874/#875 exist to make this line safe.

app.js's 385 top-level `function` declarations stop being implicit `window`
properties: 87 stay reachable via the explicit contract (#874's Object.assign
block + the 47 pre-existing `window.X = X` assignments), and 298 become
module-private. Verified NO unexposed name is read from outside app.js.

Strict mode (modules are always strict) checked ahead of the flip: app.js parses
clean as `sourceType: module` (no octal, dup params, `with`), and has no implicit
globals, no `eval`/`new Function`, no top-level `this`. `registerShortcut` is
called bare at 15 top-level sites but is assigned at `window.registerShortcut`
(app.js:10387) before its first call (10648), and a bare identifier in a module
still resolves through the global object — verified `typeof
window.registerShortcut === 'function'` in the browser.

HARD GATE — app.js IS the plugin loader:
  - /api/plugins script_type passthrough: editor/stems/studio = "module"
  - /api/plugins/stems/src/main.js -> 200; conditional GET -> 304 (live-edit ETag)
  - deep graph: stems/src/transport.js, editor/src/state.js -> 200
  - window.loadPlugins present; 5 plugin screens mount; the 3 migrated plugins
    injected as <script type="module">
  - 37 capability participants, 14 compatibility shims, bus + capabilities v1

Every one of the shell's 336 inline handlers resolves on window under module
scope, and the A-Z rail / pagination execute 6/6 with no ReferenceError. A/B
against origin/main: the ONLY unresolvable handler is `editorToggleStemMixer`,
which is equally broken on main (a dead handler in the editor plugin — not
defined anywhere in its source; pre-existing, flagged separately).

Codex preflight raised a [P1] claiming restartCurrentSong / requestExitSong /
editRegionInEditor / returnToEditorFromHighway would ReferenceError — FALSE
POSITIVE. It scanned only #874's new Object.assign block and missed app.js's 47
scattered `window.X = X` assignments; all four are at app.js:7086/7204/8492/8511
and all four resolve as `function` in the browser with app.js loaded as a module.

pytest 2396, node 1032/1032, ESLint 0 errors, tailwind-fresh clean.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant