refactor(#588): decompose the createApp composition root (phase 4) - #604
Conversation
…-strip.ts (wave 1) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…st keyboard-owner (wave 2) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…thunks (wave 3) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s (wave 4) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…n (wave 5) createApp() now builds `app` as one `let app: App;` late-bound literal assembled at the end of the function (no `as App` cast anywhere) instead of a `Partial<App>` cast mutated in place across ~75 `app.foo = ...` statements. Missing/extra members are now compile errors (TS2739/TS2353) rather than silently passing through the old cast -- this caught a genuine gap: `editingLibrary` was never initialized by createApp (file-menu.ts reads/ writes it directly), masked before by the cast since `undefined` reads falsy just like the intended default; now explicitly `false` in the literal. `state` and the workspace repository are hoisted to real locals (`state`, `workspaceRepo`) since a handful of factory calls read them synchronously, before the literal exists; every other construction-time value collapses into literal entries in the same order the old assignments ran. `sqlEditor`/ `specEditor` get noop placeholders in the literal and are overwritten in a Stage 5 late-wiring block (mirroring the plan's pseudocode) alongside their `onDocChange` registrations and the popstate listener, so real editor adapters are constructed against a fully-wired `app` instead of a partially-built one mid-construction. The four pre-existing early-fire guards (`if (app.actions)` etc., app.ts:284-286/2166-2168) are preserved verbatim. Added the mandatory construction-safety test: an injected Editor/SpecEditor whose `onDocChange` fires synchronously at registration proves createApp() returns without throwing and writes through to the real, fully-built app. Gate green: check:arch, check:types, and the full suite (212 files / 6971 tests, 100% statements/functions/lines, app.ts 90.25% branches). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…hape change `syncSqlRoute` and `rewriteWorkspaceRoute` moved off the flat `App` contract onto `app.nav` in wave 4 of the composition-root decomposition; production call sites were repointed but two raw-ESM e2e fixtures (import-example-dashboard.html, oauth-document-recovery/index.html) still called the old flat members directly, throwing during module evaluation and leaving window.__ready/__oauthRecoveryReady unset — a silent page.waitForFunction timeout rather than a clear error. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e comments Readiness-review findings applied before opening the PR: a phase-4 CHANGELOG entry under [Unreleased], Architecture.md/Source-Map.md updated for the new src/application/workspace-session.ts and src/application/surface-navigation.ts modules plus the two extracted src/ui/workbench/* controllers, six now-unused imports removed from app.ts (fixedAnchor, SavedQueryV2, QueryOrName, Result, ScriptResult, ScriptEntry — their only users moved to the extracted modules), and two dashboard.test.ts comments corrected from the removed flat `app.serializeWrite` to `app.workspaceSession.serializeWrite`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ChatGPT review pass 1Reviewed head: Findings
Areas checked with no finding
GitHub Actions for this exact SHA are green across Vitest/coverage, build, release-bundle smoke, Docker smoke, and Playwright. I could not run a separate local test pass because this execution environment's git client could not resolve |
…cumented var-strip signature gap
app.types.ts's header comment still described the pre-wave-5 `const app = {}
as App` cast, contradicting the typed-literal construction that replaced it.
The Workbench var-strip rebuild signature (moved verbatim from app.ts, present
on origin/main before this PR) folds in enum option COUNT but not option
IDENTITY, so a same-cardinality option-set change doesn't trigger a rebuild
and the dropdown goes stale. Pre-existing, not introduced by this refactor;
documented in place and filed as #605 rather than fixed here, matching how
#603 (the popover stale-close defect) was handled — a pure structural
extraction is not the place for a behavior change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ChatGPT review pass 2Previously reviewed head: Reviewed head: ResultNo new actionable findings. The new head is exactly one commit ahead of pass 1. That commit changes comments only in Pass-1 finding reassessment
Regression reviewI rechecked the complete updated PR using the pass-1 full review as the baseline. Because the old SHA is the merge base of the new head and the only new repository changes are comments, the previously inspected production paths are unchanged: staged construction/TDZ behavior, application-layer dependency direction, workspace queue and broadcast ordering, beforeunload generation tokens, surface-generation guards, resolve-before-navigate, tree-reveal-before-open, responsibility removal, and the separately tracked #603 popover defect. GitHub Actions for |
ChatGPT review pass 3Previously reviewed head: Reviewed head: ResultNo new actionable findings. The canonical PR head has not moved since pass 2. Comparing the two exact SHAs reports Earlier finding reassessment
Regression reviewBecause there is no delta from the pass-2 SHA, no regression can have been introduced after that review. The previously inspected staged-construction/TDZ behavior, application dependency direction, workspace write/broadcast ordering, beforeunload generation tokens, surface-generation guards, resolve-before-navigate ordering, tree-reveal-before-open ordering, responsibility removal, and characterized #603 defect are unchanged. GitHub Actions for |
What & why
Part of #588, phase 4 of the #593 refactor umbrella (ADR-0004's vanilla-shell investment track).
src/ui/app.ts'screateAppwas a god composition root: ~3,246 lines, a 112-memberAppbag assembled via ~130 scatteredapp.X = ...assignments and typed only by anas Appcast. This decomposes it along the four extraction seams the issue specifies, plus typed staged construction, landing as five sequential gate-green commits (one per wave), a coordinator-caught e2e-fixture fix, and a reconcile commit — all pure refactor, no behavior change (one narrow, deliberate exception noted below).Delivery contract claimed:
app.tsshrinks toward ≈1,800 lines — landed at 2,226 lines (from 3,246). Above the plan's own ≈1,950±100 forecast band; the plan flagged this criterion as directional, not exact, and the actual number is reported honestly rather than the forecast.Appinterface shrinks — 112 → 105 required members.openSavePopover,handleSqlPopState,focusDashboardMember,syncSqlRoute,rewriteWorkspaceRoute,sourceTabId,documentVisible,getLastCommittedToken,serializeWrite,flushWorkspaceWrites,refreshWorkspaceFromStoreare gone from the flat bag (repointed toapp.nav.*/app.workspaceSession.*at every production consumer — verified by a whole-tree grep during readiness review, zero stragglers beyond the two e2e fixtures fixed in0d5e511);Appgainsnav/workspaceSession.origin/mainand confirmed behaviorally byte-equivalent, except one intentional bookkeeping improvement (variable-strip'svarStripSig/etc. keyed to strip-element identity instead of the oldapp.dom-reset coupling — documented and tested) and two deliberately preserved pre-existing defects, both moved verbatim and present onorigin/mainbefore this PR:anchoredPopover.close()can clobber a newer popover sharing the samedomref slot (filed as anchoredPopover.close() can clobber a newer popover sharing the same dom refKey #603, pinned by a characterization test inpopover.test.ts), and the Workbench var-strip rebuild signature folds in enum option count but not option identities, so a same-cardinality option-set change doesn't trigger a rebuild (filed as Workbench var-strip rebuild signature ignores enum option identities (only counts them) #605, ChatGPT pass 1 caught this — verified viagit show origin/main:src/ui/app.tsbefore accepting; documented in a code comment at the signature site, no test added since fixing/characterizing it further would be scope creep for a pure structural extraction).keyboardOwnerChannelexists in exactly one place (src/ui/keyboard-owner.ts) — the three prior copies (file-menu.ts/library-assign-menu.ts/dashboard.ts) were confirmed byte-identical before unifying.workbench-shell.tsstill builds and inserts the pre-existingrunBtn/saveBtn/varStripDOM elements directly (workbench-shell.ts:132/136/160) — the extraction moved each element's render/update logic into its own controller module, not the element's construction/insertion. So a genuinely new stateful button under this pattern still touches 4 files: its controller module,workbench-shell.ts(element build + insertion),app.ts(flat delegate wiring), andapp.types.ts(the delegate's type). That is a real reduction from the old 4–5 (noWorkbenchShellDepsmember, no forwarding-literal entry, no separate updater symbol living insidecreateApp's ~2,200-line body) but not the 3-file or 2-file path an earlier description draft claimed — corrected here rather than left overstated.npm run check:types,npm run check:arch,npm run check:schemas,npm run check:examples,npm test, andnpm run buildare green at every wave and at the final head (see Tests below).Design decisions (deviations from the issue's literal text, all verified against the code, not just argued)
applyCommittedWorkspacestays inapp.ts, not moved intoworkspace-session.tsas the issue's "zero DOM" framing implied — it renders tabs, cancels dashboard-tree clicks, and rewrites the route on lost selection, which is real UI orchestration.workspace-session.tsowns queueing/tokens/broadcast/refresh-scheduling/listeners/beforeunload/provisioning only, and callsapplyCommittedWorkspaceas an injected hook.src/ui/workbench/variable-strip.ts, not the issue-suggestedvariable-bar.ts— that module'sVariableBarAppport is deliberately adapter-facing with neutral names (Rename shared VariableBar activation port to caller-neutral names #478) and a private combo-field type; forcing Workbench-specific state into it would break that contract.WorkspaceChangedMessage→workspace-session.ts;SurfaceCommandPort/DashboardFocusOutcome/WorkspaceRouteStatus→main-surface.ts(with re-export shims so existing importers compile unchanged) — required becausesrc/application/*cannot importsrc/ui/, including type-only, and these types previously lived insrc/ui/.Review process (High risk — large cross-cutting refactor of the composition root every test constructs)
src/application/. A second fable-model pass verified every finding against the actual code and produced a revised plan closing all 9 (exact construction pseudocode with a TDZ safety argument, frozen port interfaces, 7 additional invariants, replacement sabotage designs, an honest AC5 worked example, and 3 corrected module designs).app.ts), each producing its own gate-green commit with new tests at this repo's coverage floors and sabotage-verified invariant tests.git diff/git logagainst the self-report, full local gate re-run from a clean state.import-example-dashboard.html,oauth-document-recovery/index.html) called the old flatapp.rewriteWorkspaceRoute/app.syncSqlRoutedirectly — production call sites were repointed by the implementing waves but these two raw-ESM fixtures were missed. Fixed in0d5e511; full e2e suite (chromium+webkit) is green.ca92c0b.Tests
variable-strip.test.ts,save-controller.test.ts,keyboard-owner.test.ts,workspace-session.test.ts,surface-navigation.test.ts.app.test.ts's directdocumentVisibledefault-reader test, whose member moved fully insideworkspace-session.ts's private construction) has documented replacement composition coverage.Follow-ups filed
inbox) — the pre-existinganchoredPopover.close()stale-clobber defect (moved verbatim, not fixed here; see above).inbox) — the pre-existing var-strip rebuild-signature gap (enum option count vs. identity; moved verbatim, not fixed here; see above).Checklist
npm testpasses (the per-file coverage gate is non-negotiable)npm run buildsucceeds (single-filedist/sql.html)src/core/, network insrc/net/(injected fetch), DOM insrc/ui/CHANGELOG.md([Unreleased]) updated.wiki/Architecture.md,.wiki/Source-Map.md)