Skip to content

Refactor global architecture - #108

Merged
deluminor merged 205 commits into
mainfrom
refactor/global-architecture
Oct 25, 2026
Merged

deluminor merged 205 commits into
mainfrom
refactor/global-architecture

Conversation

@deluminor

@deluminor deluminor commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Refactor global architecture

Long-running, behaviour-preserving refactor that brings the whole codebase (including MonoCode-derived modules) to the project standard: small modules, a real workspace state layer, enforced layer boundaries, and upstream sync that survives file moves/splits. Spec, plan and task tracker: docs/specs/global-refactor/ (ADR docs/adr/0001-own-structure-port-upstream-changes.md).

Status: work in progress — not ready to merge. Stages 0–4 are done, Stage 5 is partially done (paused 2026-10-19), and Stages 6–14 have not started. This PR is open to show the full diff against main and run CI.

Goals

  • G1 Upstream sync survives restructuring: changes to moved or split paths are ported into the new modules or listed as Needs human, never lost.
  • G2 Module size ceiling enforced by CI: 250 lines by default. Root/composition files and genuinely complex cohesive modules may go to 500 (hard cap 1000), each listed in file-size.exceptions.json with a reason.
  • G3 Workspace state lives in a Zustand store with selectors; shell components read the store instead of long prop chains (<Sidebar> had 105 props).
  • G4 Layer dependency direction and lint rules are checked in check:web.
  • G5 No behaviour change: every stage is a refactor, existing tests stay green, and moved logic gains tests at its new interface.

Scope (done)

Stage 1 — Upstream sync tooling

  • patch-sync.mjs applies the upstream diff per path with git apply --3way instead of one atomic apply. Paths that MOLFAR moved are rewritten through the move map. Split or missing paths go to .upstream-pending/ with exit code 4, so one missing path no longer aborts the whole sync.
  • .github/upstream-moves.json (renames generated from git, splits written by hand) is validated by check:moves.
  • Covered by tests on temporary git repos. A dry run against upstream/main produced an index identical to the old script.

Stage 2 — Guard rails

  • Biome lint as a ratchet (noImportCycles, noBarrelFile, noReExportAll, noConsole, …), plus check:size and check:boundaries, all baselined and wired into check:web and CI.
  • Adds zustand and the @/ path alias.

Stage 3 — Naming

  • 709 files renamed to kebab-case, with 1644 references rewritten.
  • Biome useFilenamingConvention enforces the convention. All renames are recorded in the move map.
  • Cross-layer imports moved to @/.

Stage 4 — Layer boundaries

  • New src/domain/ for shared contracts: harness, session, models, remote protocol, git.
  • Generic UI moved to shared/; window chrome and updates became their own features.
  • Boundaries baseline went from 251 violations to 0.
  • Stage 4.R merged the MOLFAR rebrand from main.

Stage 5 — Workspace store + App.tsx (in progress)

  • Workspace store with slices and flows extracted from the Workspace component.
  • Stage 5.M merged main (0ef02fa: task board, familiars, companion, Devin) in 91e83a4.
    • All 127 conflicts were resolved by porting main's behaviour into the new structure.
    • 172 new camelCase files were renamed and added to the move map.
  • After the merge:
    • Familiars: view state moved to a store slice; flows and hooks extracted.
    • Task Board engine and companion bridge: moved into workspace hooks.
    • BTW side questions: moved into flows/btw.
  • App.tsx: 13 531 lines after the merge, 12 574 now.

Remaining

  • Stage 5: 18 items remain, starting with tab/pane close. Checkpoint: App.tsx ≤ 500 lines, composition only, full check, owner smoke test. Resume point: tasks.md § "Resume point (2026-10-19)".
  • Stages 6–14: app/shell (Sidebar, TitleBar, ProjectRail), per-feature splits with ad-hoc stores moved to Zustand, integrations/harness, shared/platform, CSS, Rust (fs.rs, harness.rs, session_store.rs), host/, close-out, merge.
  • Merge plan: reach the Stage 5 checkpoint, then merge this branch into main. Do Stages 6–7 afterwards on main as small per-feature PRs, so the branches stop drifting apart.

Notes for reviewers

  • The diff is huge (~2.2k files) and mostly renames and moves. Reviewing commit by commit, or stage by stage with tasks.md, is far more useful than reading the full diff.
  • Merge debt. The main merge brought new findings: lint 2400 → 3029, boundaries 0 → 11 (3 in production code), 45 new files over 250 lines.
    • Baselines were re-generated to record this debt, not fix it. The lists are in scratch/merge2-debt/.
    • Stages 6–7 must lower these numbers and never raise them.
  • Avoid touching App.tsx on main until this branch merges. Every change there has to be ported by hand into the extracted flows.
  • State reads changed. Familiar view state is now read synchronously from the store (getState()) instead of from refs updated during render. The values are the same or fresher.
  • Safety tag. pre-main-merge-20261018 (e3a7ca9) marks the branch before the second main merge.

Testing

On f147324c (2026-10-19):

  • npx tsc --noEmit (web) and -p host/tsconfig.json: 0 errors
  • npx vitest run: 6157 passed / 13 skipped
  • npm run test:host: 111 passed / 5 skipped
  • npm run check:rust (fmt + clippy -D warnings + cargo test): 714 passed / 1 ignored
  • npm run lint, check:size, check:boundaries, check:moves: pass against the current baselines
  • npx vite build: OK
  • All 5942 tests from main are present in the merged tree.

Outstanding: no manual UI smoke test yet. Areas to cover:

  • Familiar: open/close/shortcut, reopen on launch, reset while shown, habit posts and approvals, completion batches in the Familiar chat.
  • Task Board dock badge.
  • Companion phone requests.
  • BTW side questions: ask, stream, stop, retry, model change, delete.

Applies each upstream path as its own three-way patch so one moved path
no longer aborts the whole sync. Paths Vatra renamed are retargeted via
.github/upstream-moves.json; split, removed and unmapped paths become
pending ports under .upstream-pending/ (exit code 4). moves.mjs
regenerates detected renames and --check validates map coverage in
check:web and CI.
Adds the pending-port and new-upstream-file steps for paths Vatra moved,
split or removed.
Biome lints without formatting. lint, check:size and check:boundaries
freeze today's findings in baselines: new violations fail, fixed ones
must be locked in with --update, so the baselines only shrink. Layer
rules follow app -> features -> integrations -> platform -> domain ->
shared; other features are reachable only through their top-level
files.
Every non-component .ts/.tsx file under src/ moves to kebab-case
(components stay PascalCase.tsx); imports, mocks, worker URLs, comments,
docs, NOTICE and the check baselines follow. The 709 renames are
recorded in .github/upstream-moves.json so upstream edits to the old
paths are retargeted by the sync.
The pending-navigation cases queued a one-off invoke result, which a
watcher stat call could consume before the initial read; they now model
the file like the growth case. Reload waits poll outside act() so React
commits the new document, replacing fixed sleeps, and waits on lazy
CodeMirror chunks get a longer timeout.
Relative imports stay within a feature or layer; 2054 cross-layer
specifiers in 593 files now use @/ so moving a module no longer means
counting ../ segments.
--update --transfer accepts per-file regressions when no kind's repo-wide
total grows, so code (and its existing findings) can move to new modules.
The file-size check never accepts a new oversized file this way.
The move map gains an extractions section; the sync still applies such
paths and reports the moves under extracted, so the agent ports hunks
that touch moved declarations into their new modules.
…main

Session, block, turn, attachment, queue and harness types, plus pure
session helpers, move out of features/sessions/model/session.ts into
src/domain/{harness,session}; the composer card contracts they reference
(inbox, linked work items, notes, orchestration proposals, handoff) move
to src/domain as well. Session factories that read provider settings
stay in the sessions feature. The notes barrel is removed in favour of
direct imports. The move map records the split, the extractions and
the barrel removal.
Merges origin/main (fda78db, PR #39): Vatra -> MOLFAR, version 1.0.0,
MonoCode-era migration code removed, sphere brand visual, legacy
worktree prefixes.

Resolution on the refactor side:
- vatraToolCall moves to molfar-tool-call.ts (kebab-case); the Block
  flag rename (vatra/monocode -> molfar) lands in src/domain/session/block.ts
- operatorUserPrompt is gone with the legacy command aliases
- legacy storage migration modules stay deleted
- main's new hooks use kebab-case file names
- refactor-only files (sync tooling, ratchets, docs) use the new name;
  the legacy vatra/ worktree branch prefixes stay as main has them
- move map: renames regenerated, removed entries for dropped migration
  code and replaced brand assets
- ratchet baselines record main's growth of five Rust files and the
  SphereVisual canvas finding, matching its sibling visuals
Model types and pure id/setting helpers move to src/domain/models; the
bundled catalog, the live overlay store and model resolution move to
src/integrations/harness/core/models, so harness providers and the host
no longer import the sessions feature. Preference persistence and
picker state stay in features/sessions/model/models.ts. HARNESS_ORDER
duplicated HARNESSES and is dropped. The bundled-catalog test suite
moves next to the catalog, and the Pi catalog test mocks the module it
actually imports. The move map records both splits.
…tions

The app <-> host protocol moves to src/domain/remote/protocol.ts, and
the renderer's remote session client (remoteRequest, session sync,
attachment previews) moves to src/integrations/remote, so the host and
its end-to-end sync test no longer reach into the connections feature.

Pure pieces the host and harness providers share move to src/domain:
attachment MIME checks and prompt blocks, worktree branch names, the
GitHub work-item URL parser, project search results, and the file and
working-tree DTOs of the Tauri commands. The IO that produces them
stays where it was.

Tests follow their modules; the Codex attachment test imports prompt
helpers from their new home and the remote command test mocks the
module remote-commands actually imports. The move map records the
renames and extractions.
The automated moves in the 4.1-4.3 extractions left imports on one
long line or split without trailing commas. Only import declarations
that changed since then are reformatted, in Prettier's style; no other
line moves. The file-size baseline records the resulting few lines in
36 files that are already over the limit and get split later.
…low features

Harness providers no longer import the source-control and providers
features:
- git commit/PR/branch prompt helpers move to src/domain/git/git-text.ts
- the runtime provider binary path registry that child.ts reads moves
  to src/integrations/harness/core/provider-binary-paths.ts
- provider account id helpers move to src/domain/harness/provider-account.ts
- rate limit types, windows and states move to src/domain/rate-limits;
  provider usage payload parsing moves to integrations (shared window
  parsing in core/usage-window.ts, Claude, Codex and OpenCode parsers
  next to their providers)

Display formatters, poll intervals and account persistence stay in the
providers feature. Rate limit tests move next to the code they test
(23 before and after). The move map records the renames, the rate
limit split and the account extraction.
Production code:
- isFailedStatus moves to src/domain/session/tool-status.ts
- planTurnKey, isProviderFailureText and isReviewablePlan move to
  src/domain/session/plan.ts
- normalizeProjectPath moves to src/shared/lib/project-path.ts and the
  localStorage flag helpers to src/shared/lib/storage-flags.ts
- the Claude hooks preference moves next to the Claude adapter that
  reads it (providers/claude/claude-hooks-setting.ts)

Tests:
- blankSession (domain) builds the session literal newSession used to
  inline; harness tests use core/test-session.ts, which is newSession
  without saved model preferences
- the Codex approval UI and attachment tests test features, so they
  move under features/sessions
- the plan-key restart test, which needs session persistence, moves to
  features/sessions/data; the transcript folding checks for background
  commands move to a transcript-activity test built from the blocks the
  Claude harness test pins

The move map records the renames and extractions.
IS_MAC, IS_WIN, IS_LINUX, HAS_NATIVE_GLASS and the shortcut glyphs read
navigator only, so they move from src/platform/tauri/platform.ts to
src/shared/lib/platform.ts, where shared UI can use them. Tests mock
the new path.
… to shared

GlassBackdrop, RailAction, ClosingTab (TabWidthMotion) and IconButton
(out of TitleBar) have no app dependencies and are used by features, so
they move to src/shared/ui. The alert/confirm queue moves to
src/shared/lib/app-dialog.ts; the AppDialog host that renders it stays
in the app shell. Tests mock the new app-dialog path.
…a feature

WindowControls (the Tauri window buttons) and the overlay navigation
pieces split out of TitleBar (OverlayNav, TabVisitNav, DevModeLabel,
DevModeSlot) move to src/features/window-chrome/ui, so surface features
no longer import the app shell. Tests mock the new WindowControls path.
… feature

The updater flow, update prompt and install, recheck, installed-update
notice, fork updater policy and release notes model move from
src/app/model to src/features/updates/model with their tests, and
ReleaseNotesSurface to src/features/updates/ui, so Settings, the
workspace tabs and the file pane no longer import the app. The dialogs
and rail cards that present updates stay in the app shell.

The bundled CHANGELOG import is re-pointed by hand (the move tooling
does not rewrite ?raw imports), and app shell tests mock the new paths.
… workspace

The Tab summary TitleBar renders is built by the workspace tab model, so
it moves to src/features/workspace/model/title-tab.ts. Tests that render
ProjectRail or TitleBar move from features to src/app/shell, and the
type-only imports the move produced use import type.
- basename moves from platform/tauri/fs to shared/lib/paths
- attachment size limits move to src/domain/session/attachment-limits.ts,
  and native clipboard paste-to-attachments moves from platform/tauri/
  clipboard to features/sessions/model/clipboard-attachments.ts; the
  clipboard round-trip test, which builds attachments, moves with it
- the Obsidian vault command client moves from platform/tauri/vault to
  features/knowledge/model/vault/vault-client.ts, the only caller
- useColorScheme and PrivateEmail read settings, so they move into the
  settings feature
- the HarnessIcon rendering check leaves the shared icon catalog test

Tests mock the new paths; fs mocks no longer stub basename, which
callers now take from shared.
The Confluence API cache, hidden spaces, kinds, mentions and prompt
helpers lived under inbox/model/confluence, which made them internal to
Inbox while agent-app, sessions and settings use them. They move to
src/features/confluence/model with their tests, clearing the last layer
boundary violations; the boundaries baseline is now empty.
Nothing rendered the real App in tests. app-harness renders it in
happy-dom against Tauri's official IPC, window and event mocks, with a
fake live harness the test drives. ipc-defaults answers every command
with an empty value of the type its invoke call expects.

The first tests pin boot, submitting a prompt and streaming the reply,
persistence (user turn at once, answered transcript after the debounce,
nothing outside a project), opening, closing and cycling tabs, and the
terminal toggle.
The fake harness records steers, cancellations and approval replies.
New Workspace characterization covers a mid-turn follow-up steering the
running turn, Stop cancelling it, answering an approval from the
transcript, split_right, Close Others / Close All (which starts the
last tab over on a blank session), and reordering title tabs by drag
through the real useAnimatedReorder gesture.
The characterization suite failed under machine load because it slept
fixed intervals. The harness now waits for the composer before
returning and before typing, click waits for its button, and every test
waits for the state it expects (until) with a 60s budget for cold boots.
…ate cards and add-to-chat deps in useInboxFlows
…renders

Root cause: useWorkspaceSnapshotAutosave recorded the snapshot key before
its 250 ms timer fired. A re-render inside that window (same snapshot,
any listed dep changed) ran the cleanup, which cleared the timer, and then
returned early because the key already matched, so the save never ran
until the layout changed again.

The key is now recorded when the timer fires, so an unchanged re-render
restarts the debounce instead of dropping it. A failed save clears the key
and logs, so the next render retries instead of treating it as saved.
…e written

Root cause: the close handler fired persistLiveTranscripts and
hideCurrentWindow without awaiting either, and upsertSession does async work
before its IPC write, so hide_window always reached the host before
session_upsert. persistLiveTranscripts also swallowed every write failure.

A running chat (and a kept-alive macOS Familiar) now awaits the transcript
writes before hiding, bounded by a 2 s wait so a hung write cannot keep the
window up. persistLiveTranscripts returns the settled results; failed writes
and a timed-out wait are logged. The close characterization tests now pin
session_upsert before hide_window.
…_ready

Root cause: useSessionAutosave's 650 ms timer stayed armed through the
coordinated quit. handleQuitRequested writes every chat itself (running turns
marked interrupted) and reports quit_ready, then the timer fired and wrote the
pending, unmarked copies, racing the exit and able to overwrite the
interrupted marks.

A write that comes due while the app is quitting now stays pending. The quit
already wrote the same latest state (sessionsRef and the autosave read one
session store), so nothing is lost; if the quit is called off, the next change
writes the pending chats as before.
The project filter lived in BoardView's useState, and LocalSurfaceViews
unmounts the board when another surface opens, so every visit and every
launch started on All projects.

The choice now lives in a board store (model/board-project, the same
module-store pattern as board-focus) persisted under the
molfar.taskBoard.* localStorage keys the board already uses.
useBoardProject reads it with useSyncExternalStore. A saved project that
is no longer listed shows All projects without being erased, so a project
list that is still loading cannot wipe the choice. Failed writes are logged.
Biome reports diagnostics with native separators, so on Windows every
file (host\engine.ts, src\...\stage-fail.ts) missed its forward-slash
baseline entry and all 845 baselined findings showed up as new.
…d Linux

Root cause: happy-dom builds navigator.platform from the host
("X11; Darwin arm64" on macOS), and IS_WIN's /Win/i matches "Darwin",
so the close suite silently ran the Windows tray branch on macOS. On
Linux CI the platform reads as Linux, which has no tray: an idle window
is destroyed and a busy one asks first, so waiting for hide_window
timed out. The app behaves correctly on Linux.

The suite now pins Win32 explicitly, and a Linux variant pins that a
stored close-to-tray choice is ignored there.
…n saves

Root cause: renderApp returns 50 ms after the composer shows, while
boot's first layout save is still on its 250 ms debounce. On a fast
host the file opens inside that window and supersedes it; on a slow
runner (Ubuntu CI) it lands after the mark, so two saves were counted.
Not platform-specific: reproduced on macOS with a 400 ms pause.

The test now waits for boot's save before marking; the single-save
assertion is unchanged.
Root cause: the kebab-case renames (familiarMessaging,
sessionStateStore, streamingPerformance) never reached tests/browser,
which no tsc project includes. Vite failed to build the transcript and
workspace-performance fixture pages, so every test on them timed out.

Also points stale type-only imports (Block, QueuedMessage, Tab) at the
modules that now export them; esbuild erased those, so they built, but
they no longer type-checked.
@deluminor
deluminor merged commit 6c9f6ec into main Oct 25, 2026
11 checks passed
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.

2 participants