Refinement Audit — Cycle 1

Summary

The project is generally in solid shape: the main-process services are typed, most IPC paths have tests, and the renderer is using a reasonably disciplined bridge/query setup. The main issues are edge-case contract gaps where the UI depends on IPC channels that were never wired, plus a couple of desktop-specific behaviors that break on real operator workflows.

Critical Findings

C1: Publish recovery banner calls two IPC channels that are still stubbed

  • File: src/main/ipc/router.ts:229
  • Issue: The renderer’s post-timeout recovery flow depends on config:getPublic and shell:openPath, but registerAllIpcHandlers never installs real handlers for either channel, so both fall through to the generic NOT_IMPLEMENTED stub.
  • Evidence: PublishUnknownStateBanner invokes getTropicalApi().configGetPublic() and then getTropicalApi().shellOpenPath(...) at src/renderer/components/PublishUnknownStateBanner.tsx:40 and src/renderer/components/PublishUnknownStateBanner.tsx:45, while router.ts jumps from the dialog/first-run/settings branches to later channel families and finally stubs unmatched channels at src/main/ipc/router.ts:361.
  • Fix: Add typed handlers for config:getPublic and shell:openPath, wire them in registerAllIpcHandlers, and cover both channels in router tests so the banner’s “Open repo folder” path is executable in production.

C2: File copy and publish disagree about the configured incoming posts folder

  • File: src/main/index.ts:84
  • Issue: Copied documents always go to PathService’s hard-coded default incoming/posts, while PublishService stages files from the configurable config.incomingPostsPath. If an operator changes that setting, copied files land in one folder and publish looks in another.
  • Evidence: createFileCopyService is wired with getIncomingDir: (): string => pathService.getFullIncomingPath() at src/main/index.ts:86, but publish derives staging paths from config.incomingPostsPath in src/main/services/git/PublishService.ts:150 and src/main/services/git/PublishService.ts:380.
  • Fix: Resolve the incoming copy destination from the current config, not from the PathService default, and add coverage proving copy + publish stay aligned when incomingPostsPath is customized.

Important Findings

I1: Storm folder renames fail when only the folder casing needs to change

  • File: src/main/services/storms/StormFolderService.ts:538
  • Issue: updateFolderName and syncAllFolderNames stat the target path before renaming and reject with STORM_FOLDER_TARGET_EXISTS whenever it exists. On case-insensitive filesystems, a rename like 09L_milton09L_Milton resolves to the same on-disk directory and is incorrectly blocked.
  • Evidence: updateFolderName rejects as soon as deps.fs.stat(destAbs) succeeds at src/main/services/storms/StormFolderService.ts:538, and the same pattern appears in bulk sync at src/main/services/storms/StormFolderService.ts:672.
  • Fix: Detect case-only renames on case-insensitive platforms, treat the source directory as the same target instead of a collision, and add regression coverage for both single-folder and sync flows.

I2: Second launch does not refocus the first-run window

  • File: src/main/app/lifecycle.ts:43
  • Issue: The single-instance handler only asks mainWindowFactory() for a window, so during the first-run gate, when the main window does not exist yet, a second launch exits without focusing the already-open first-run window.
  • Evidence: enforceSingleInstance resolves only const win = mainWindowFactory() and returns early when it is null at src/main/app/lifecycle.ts:43-44; src/main/index.ts only assigns mainWindowRef for the post-gate main window, not the first-run shell.
  • Fix: Fall back to any existing non-destroyed BrowserWindow when the factory returns null, and update the single-instance tests to cover the first-run-window case.

Minor Findings

M1: Router tests never assert the renderer-visible config/shell utility channels

  • File: tests/main/ipcRouter.test.ts:78
  • Issue: The router suite verifies a generic stub path and several feature families, but it never checks that config:getPublic and shell:openPath are wired when their dependencies are present. That gap let a user-visible recovery action ship against stubs.
  • Fix: Add explicit router tests for both channels so future refactors cannot silently regress them back to NOT_IMPLEMENTED.

Design Improvements

D1: Give renderer-facing utility IPC calls dedicated handlers instead of relying on router special cases

  • Scope: src/main/ipc/router.ts, new utility handler module(s), tests/main/ipcRouter.test.ts
  • Current: The router contains ad hoc inline branches for a handful of channels, while other renderer-visible utility channels are easy to forget and silently drop to the stub handler.
  • Proposed: Extract small typed handlers for utility channels such as public config snapshots and shell path opening, then wire and test them as first-class handler units.
  • Benefit: Reduces stub drift, keeps the router easier to audit, and makes renderer-facing contracts harder to break accidentally.

Regressions

None detected.

TOTAL_FINDINGS: 6