Refinement Audit — Cycle 4

Summary

The project is in better shape after cycle 3; the remaining issues are concentrated in boundary enforcement and publish-time repository safety rather than broad structural defects. This cycle found three new problems in files that were not part of prior audits: one git-state bug that can break branch-switch publishes, plus two contract-validation gaps in config persistence and shell path handling.

Critical Findings

None.

Important Findings

I1: Auto-stash omits untracked files before branch-switch publishes

  • File: src/main/services/git/GitService.ts:180
  • Issue: The stash helper uses git stash push -m without including untracked files. During publish, the caller treats any dirty worktree as stashable before checking out the target branch, so untracked documents or sidecars can still block checkout or remain behind on the original branch.
  • Evidence: PublishService decides to stash whenever git.status().isClean() is false at src/main/services/git/PublishService.ts:348, then calls git.stash(...) before branch checkout. But GitService.stash executes this.git.stash(['push', '-m', message]) at src/main/services/git/GitService.ts:184, which does not include untracked paths.
  • Fix: Include --include-untracked (or -u) in the stash command and add regression coverage that verifies branch-switch publishes stash untracked files too.

I2: ConfigService.save() still strips unknown keys and reports success

  • File: src/main/services/config/ConfigService.ts:411
  • Issue: Direct callers of ConfigService.save() can pass objects with unsupported fields and receive a successful save even though those fields are silently dropped. That leaves persistence semantics inconsistent with the stricter update contract added last cycle.
  • Evidence: The save path validates with parseAppConfig(config as unknown, { unknownKeyPolicy: 'strip' }) at src/main/services/config/ConfigService.ts:411, then writes roundTrip.config to disk. Unsupported keys are removed instead of causing CONFIG_CORRUPT.
  • Fix: Validate save() input with unknownKeyPolicy: 'reject' and add a regression test that an extra field is rejected instead of silently stripped.

I3: shell:openPath accepts relative and malformed paths at the main-process boundary

  • File: src/main/ipc/handlers/utilityHandlers.ts:60
  • Issue: The handler only checks that raw.path is a non-empty string before delegating to shell.openPath. A compromised or buggy renderer can send cwd-relative or malformed path strings and the main process will forward them to the OS shell.
  • Evidence: After trimming, the handler passes path straight into shell.openPath(path) at src/main/ipc/handlers/utilityHandlers.ts:67 with no absolute-path or control-character validation.
  • Fix: Reject non-absolute or control-character-containing paths before calling Electron shell, and add direct handler tests for the new validation path.

Minor Findings

None.

Design Improvements

None.

Regressions

None detected.

TOTAL_FINDINGS: 3