Skip to content

fix: prevent roadmap.json cross-project contamination on project switch#1886

Open
Mitsu13Ion wants to merge 3 commits intoAndyMik90:developfrom
Mitsu13Ion:fix/roadmap-cross-project-contamination
Open

fix: prevent roadmap.json cross-project contamination on project switch#1886
Mitsu13Ion wants to merge 3 commits intoAndyMik90:developfrom
Mitsu13Ion:fix/roadmap-cross-project-contamination

Conversation

@Mitsu13Ion
Copy link
Contributor

@Mitsu13Ion Mitsu13Ion commented Feb 21, 2026

Summary

  • Fix race condition where switching projects could overwrite one project's roadmap.json with another project's data
  • Add roadmap.projectId validation before every save in useIpc.ts and roadmap-store.ts
  • Clear roadmap store immediately on project switch (loadRoadmap) to prevent stale saves during async gap

Test plan

  • Open two projects (A and B), each with a roadmap
  • On Project A, start a merge for a task; while running, switch to Project B, then back to A
  • Verify Project A's roadmap.json still has Project A's features after merge completes
  • Verify Project B's roadmap.json is unchanged
  • Run cd apps/frontend && npm test — all 3467 tests pass
  • Run cd apps/frontend && npx tsc --noEmit — no type errors

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Roadmap updates now use the originating event's project ID to prevent cross-project persistence and data corruption.
    • Reconciliation logic avoids saving if the in-memory roadmap no longer belongs to the same project, preventing async cross-project saves.
    • Switching projects immediately clears roadmap and related state to remove stale data and stop inconsistencies from carrying over.

Race condition: when switching projects, the roadmap store holds stale data
during async load. Any save triggered in that gap writes Project A's roadmap
to Project B's file. Fix by validating roadmap.projectId matches the target
projectId before every save, and clearing the store immediately on load.
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @Mitsu13Ion, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical data integrity issue where roadmap.json files could be corrupted due to a race condition during project switching. By introducing robust project ID validation checks before any roadmap save operations and proactively clearing the roadmap store, the changes ensure that each project's roadmap data remains isolated and correct, even in scenarios involving rapid context switching.

Highlights

  • Race Condition Fix: Resolved a race condition that could lead to roadmap.json files being overwritten with data from another project when rapidly switching between projects.
  • Project ID Validation: Implemented roadmap.projectId validation checks before saving the roadmap in both useIpc.ts and roadmap-store.ts to ensure data integrity.
  • Immediate Store Clearing: Modified the loadRoadmap function to immediately clear the roadmap store, preventing stale roadmap data from being saved across different projects during asynchronous operations.
Changelog
  • apps/frontend/src/renderer/hooks/useIpc.ts
    • Modified the saveRoadmap call within the task completion logic to first verify that the projectId from the event matches the projectId of the currently loaded roadmap in the store.
    • Added a subsequent check to ensure the updatedRm also corresponds to the eventProjectId before persisting.
  • apps/frontend/src/renderer/stores/roadmap-store.ts
    • Introduced a guard condition in reconcileLinkedFeatures to only save the updatedRoadmap if its projectId matches the projectId passed to the function, preventing saves of outdated or incorrect project data.
    • Added store.setRoadmap(null) at the beginning of the loadRoadmap function to clear any existing roadmap data, mitigating risks of cross-project data contamination during asynchronous loading.
Activity
  • The pull request was generated using Claude Code.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 21, 2026

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

Replaces use of the store's active project with the event-provided projectId when handling TASK_STATUS_CHANGE and roadmap persistence; adds guards in reconciliation and load to clear or validate roadmap state to prevent cross-project saves.

Changes

Cohort / File(s) Summary
IPC handler updates
apps/frontend/src/renderer/hooks/useIpc.ts
TASK_STATUS_CHANGE handler now reads eventProjectId from the IPC callback and gates roadmap updates/persistence on that ID instead of the store's activeProjectId; persists updated roadmap using eventProjectId.
Roadmap store guards
apps/frontend/src/renderer/stores/roadmap-store.ts
reconcileLinkedFeatures added guard to only save if in-memory roadmap matches the event projectId; loadRoadmap clears roadmap and related state immediately when switching projects to avoid stale cross-project state.

Sequence Diagram(s)

sequenceDiagram
  participant IPC as IPC Event (TASK_STATUS_CHANGE)
  participant UseIpc as useIpc.ts
  participant Store as RoadmapStore
  participant ElectronAPI as window.electronAPI.saveRoadmap

  IPC->>UseIpc: emit TASK_STATUS_CHANGE (specId, projectId)
  UseIpc->>Store: markFeatureDone(specId)
  Store-->>UseIpc: updatedRoadmap (if roadmap.projectId == eventProjectId)
  UseIpc->>ElectronAPI: saveRoadmap(updatedRoadmap, eventProjectId)
  ElectronAPI-->>UseIpc: save result / error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • #1815 — Large refactor of roadmap-store including reconcileLinkedFeatures and load/setRoadmap behavior.
  • #775 — Changes IPC task/status handling to use the event-provided projectId for gating updates.
  • #1464 — Modifies loadRoadmap and roadmap persistence semantics in the store.

Suggested labels

bug, area/frontend, size/S

Suggested reviewers

  • AlexMadera
  • AndyMik90
  • MikeeBuilds

Poem

🐰 I hopped on an event with a bright little grin,

Project IDs tidy — no mixing within.
I guard every roadmap, keep chaos at bay,
Save only the matching one — hip-hip-hooray! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically describes the main change: preventing cross-project contamination of roadmap.json when switching projects, which aligns with the core fix implemented across both modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

The pull request effectively addresses the race condition that leads to roadmap.json cross-project contamination. By validating the projectId from the IPC event against the roadmap currently in the store, and by clearing the roadmap store immediately upon project switching, it ensures that stale data is not persisted to the wrong project file. I have a few suggestions to further improve the robustness of the project switching logic and maintain backward compatibility.

@sentry
Copy link

sentry bot commented Feb 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…itch

Prevents stale competitor analysis and generation progress from the
previous project showing during async load of the new project's data.
@AndyMik90 AndyMik90 self-assigned this Feb 22, 2026
Copy link
Owner

@AndyMik90 AndyMik90 left a comment

Choose a reason for hiding this comment

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

🤖 Auto Claude PR Review

PR #1886 reviewed: 13 findings (0 structural issues). Verdict: approve.

Findings (13 selected of 13 total)

🔵 [SEC-1] [LOW] No validation that eventProjectId is a safe identifier before passing to saveRoadmap

📁 apps/frontend/src/renderer/hooks/useIpc.ts:228

The eventProjectId (aliased from projectId callback parameter) is passed directly to window.electronAPI.saveRoadmap() without any validation that it is a well-formed project identifier. If the IPC event source can be influenced by an attacker, a crafted projectId could potentially cause path traversal in the backend file-save logic (e.g., ../../etc/cron.d/evil). However, the risk is mitigated by the fact that rm.projectId === eventProjectId check requires matching an existing roadmap's projectId, limiting exploitation. The actual severity depends on the backend saveRoadmap implementation.

Suggested fix:

Validate eventProjectId against an allowlist or UUID/slug pattern before passing it to saveRoadmap. E.g., `if (!/^[a-zA-Z0-9_-]+$/.test(eventProjectId)) return;`

🔵 [SEC-2] [LOW] TOCTOU race between projectId check and save in reconcileLinkedFeatures

📁 apps/frontend/src/renderer/stores/roadmap-store.ts:716

In reconcileLinkedFeatures, the code checks updatedRoadmap.projectId === projectId then proceeds to save. Between the check and the saveRoadmap call, another async operation could swap the roadmap in the store. While this PR significantly narrows the window compared to before, a true atomic guard (e.g., a lock or version counter) would fully eliminate the race. This is a defense-in-depth concern rather than a directly exploitable vulnerability.

Suggested fix:

Capture the roadmap reference once and pass that captured reference to saveRoadmap rather than re-reading from the store, or introduce a generation/version counter to detect stale state.

🟡 [QLT-1] [MEDIUM] Redundant variable assignment for projectId

📁 apps/frontend/src/renderer/hooks/useIpc.ts:227

The line const eventProjectId = projectId; creates an unnecessary alias. The projectId parameter from the callback is already available and its name is sufficiently descriptive in context. The added comment explains the intent well, but the alias adds no clarity and introduces a redundant variable.

Suggested fix:

Remove `const eventProjectId = projectId;` and use `projectId` directly in the subsequent checks. The comment explaining not to use the store's activeProjectId is sufficient.

🟡 [QLT-2] [MEDIUM] Double state read and double projectId check is fragile

📁 apps/frontend/src/renderer/hooks/useIpc.ts:229

The code reads useRoadmapStore.getState() twice — once before markFeatureDoneBySpecId and once after — and checks rm.projectId === eventProjectId both times. While the second check guards against a theoretical race during markFeatureDoneBySpecId, markFeatureDoneBySpecId is a synchronous Zustand mutation that cannot change projectId. The double-check pattern adds complexity without real protection. If the concern is truly about concurrency, a more robust pattern (e.g., a mutex or transactional update) would be warranted.

Suggested fix:

Consider reading state once after the mutation. The pre-mutation check is sufficient to decide whether to proceed; the post-mutation check is redundant for a synchronous store update.

🟠 [QLT-3] [HIGH] markFeatureDoneBySpecId silently skipped without logging

📁 apps/frontend/src/renderer/hooks/useIpc.ts:229

When rm is null, eventProjectId is falsy, or the projectId doesn't match, the feature completion is silently dropped with no log or warning. This could make debugging very difficult in production — a task completes successfully but the roadmap is never updated, and there's no trace of why.

Suggested fix:

Add an else branch with a debug/warn log: `console.warn('[useIpc] Skipping roadmap update: project mismatch or missing roadmap', { eventProjectId, roadmapProjectId: rm?.projectId });`

🟡 [QLT-4] [MEDIUM] Missing edge case: roadmap loaded after event arrives

📁 apps/frontend/src/renderer/hooks/useIpc.ts:228

If a task completion event arrives after loadRoadmap has cleared the store (setRoadmap(null)) but before the new roadmap has been loaded, the feature done mark will be permanently lost. The fix prevents cross-project contamination but introduces a window where legitimate same-project events are dropped. Consider queuing events or re-reconciling after load.

Suggested fix:

Consider maintaining a pending operations queue for task completion events that arrive during the async gap of loadRoadmap, and replay them once the roadmap is loaded.

🔵 [QLT-5] [LOW] Clearing store state without checking if already null

📁 apps/frontend/src/renderer/stores/roadmap-store.ts:733

In loadRoadmap, setRoadmap(null), setCompetitorAnalysis(null), and setGenerationStatus are called unconditionally. While not a bug, these trigger Zustand state updates and potential re-renders even when the store is already in the cleared state (e.g., on first load or consecutive calls).

Suggested fix:

Consider guarding with a check: `if (store.roadmap !== null) store.setRoadmap(null);` to avoid unnecessary re-renders.

🔵 [QLT-6] [LOW] Inconsistent error handling pattern between reconcile and IPC save

📁 apps/frontend/src/renderer/stores/roadmap-store.ts:718

Both the reconciliation save (roadmap-store.ts:718) and the IPC save (useIpc.ts:234) use .catch() to log errors but neither propagates or handles them further. While consistent with each other, the lack of any user-facing feedback or retry mechanism could lead to silent data loss.

Suggested fix:

Consider a centralized save-with-retry utility or at minimum a user-facing notification on persistent save failures.

🟠 [DEEP-1] [HIGH] Race condition: roadmap state can change between projectId check and markFeatureDoneBySpecId mutation

📁 apps/frontend/src/renderer/hooks/useIpc.ts:228

Between the check rm.projectId === eventProjectId and the call to markFeatureDoneBySpecId(taskId), another async operation (e.g., loadRoadmap for a different project) could replace the roadmap in the store. The code reads rm from state, validates projectId, then calls markFeatureDoneBySpecId which mutates whatever roadmap is currently in the store — not necessarily the one that was validated. The second check (updatedRm.projectId === eventProjectId) mitigates the save, but the mutation itself (markFeatureDoneBySpecId) would still corrupt the wrong project's in-memory roadmap.

Suggested fix:

Pass the eventProjectId into markFeatureDoneBySpecId and have it internally verify the roadmap's projectId before mutating, or perform the check and mutation atomically inside the store action.

🟡 [DEEP-2] [MEDIUM] reconcileLinkedFeatures applies mutations to store before checking projectId on save

📁 apps/frontend/src/renderer/stores/roadmap-store.ts:715

The guard at line 715 only prevents the save when projectId doesn't match, but the mutations that set hasChanges = true (earlier in the function, not shown in diff) have already been applied to the store's roadmap state. If a project switch happened during the async reconciliation, those mutations would have been applied to the new project's roadmap in memory. The guard prevents persisting the corrupted state, but the in-memory state is still wrong until the next load.

Suggested fix:

Check `updatedRoadmap.projectId === projectId` before applying any mutations to the store, or revert the mutations if the projectId no longer matches after the async gap.

🟡 [DEEP-3] [MEDIUM] Clearing roadmap to null on project switch may cause UI flash or null reference errors

📁 apps/frontend/src/renderer/stores/roadmap-store.ts:734

Setting roadmap to null immediately in loadRoadmap before the new roadmap is loaded creates a window where any component reading useRoadmapStore.getState().roadmap will see null. If components don't defensively handle null roadmap (and the old code didn't need to since roadmap was only replaced, never cleared mid-session), this could cause runtime errors or a visible UI flash/flicker showing an empty state before the new roadmap loads.

Suggested fix:

Ensure all UI consumers of the roadmap store handle null roadmap gracefully, or use a loading flag instead of nulling the roadmap, and only clear the old roadmap when the new one is ready to replace it.

🟡 [DEEP-4] [MEDIUM] Task completion events for previous project are silently dropped instead of deferred

📁 apps/frontend/src/renderer/hooks/useIpc.ts:227

When a task completes for project A but the user has switched to project B, the markFeatureDoneBySpecId call is skipped entirely. The roadmap.json for project A is never updated to reflect the task completion. This means the feature won't be marked as done until some other reconciliation mechanism runs (if one exists). The old bug was that it would write to the wrong project's file; the new behavior is that legitimate updates are lost.

Suggested fix:

Queue the update for the original project and apply it when that project's roadmap is next loaded, or load the correct project's roadmap from disk, apply the mutation, and save it back without going through the store.

🔵 [DEEP-5] [LOW] Redundant double-read of store state after markFeatureDoneBySpecId

📁 apps/frontend/src/renderer/hooks/useIpc.ts:226

The code reads useRoadmapStore.getState().roadmap before the mutation to check projectId, then reads it again after the mutation. The first read's rm variable is never used for the save — only for the guard. While not a bug, it's slightly confusing and the pattern could be simplified by reading state once after mutation and checking projectId there.

Suggested fix:

Simplify to a single state read after the mutation, checking projectId at that point.

This review was generated by Auto Claude.

@Mitsu13Ion
Copy link
Contributor Author

🤖 Auto Claude PR Review

PR #1886 reviewed: 13 findings (0 structural issues). Verdict: approve.

Findings (13 selected of 13 total)

🔵 [SEC-1] [LOW] No validation that eventProjectId is a safe identifier before passing to saveRoadmap

📁 apps/frontend/src/renderer/hooks/useIpc.ts:228

The eventProjectId (aliased from projectId callback parameter) is passed directly to window.electronAPI.saveRoadmap() without any validation that it is a well-formed project identifier. If the IPC event source can be influenced by an attacker, a crafted projectId could potentially cause path traversal in the backend file-save logic (e.g., ../../etc/cron.d/evil). However, the risk is mitigated by the fact that rm.projectId === eventProjectId check requires matching an existing roadmap's projectId, limiting exploitation. The actual severity depends on the backend saveRoadmap implementation.

Suggested fix:

Validate eventProjectId against an allowlist or UUID/slug pattern before passing it to saveRoadmap. E.g., `if (!/^[a-zA-Z0-9_-]+$/.test(eventProjectId)) return;`

🔵 [SEC-2] [LOW] TOCTOU race between projectId check and save in reconcileLinkedFeatures

📁 apps/frontend/src/renderer/stores/roadmap-store.ts:716

In reconcileLinkedFeatures, the code checks updatedRoadmap.projectId === projectId then proceeds to save. Between the check and the saveRoadmap call, another async operation could swap the roadmap in the store. While this PR significantly narrows the window compared to before, a true atomic guard (e.g., a lock or version counter) would fully eliminate the race. This is a defense-in-depth concern rather than a directly exploitable vulnerability.

Suggested fix:

Capture the roadmap reference once and pass that captured reference to saveRoadmap rather than re-reading from the store, or introduce a generation/version counter to detect stale state.

🟡 [QLT-1] [MEDIUM] Redundant variable assignment for projectId

📁 apps/frontend/src/renderer/hooks/useIpc.ts:227

The line const eventProjectId = projectId; creates an unnecessary alias. The projectId parameter from the callback is already available and its name is sufficiently descriptive in context. The added comment explains the intent well, but the alias adds no clarity and introduces a redundant variable.

Suggested fix:

Remove `const eventProjectId = projectId;` and use `projectId` directly in the subsequent checks. The comment explaining not to use the store's activeProjectId is sufficient.

🟡 [QLT-2] [MEDIUM] Double state read and double projectId check is fragile

📁 apps/frontend/src/renderer/hooks/useIpc.ts:229

The code reads useRoadmapStore.getState() twice — once before markFeatureDoneBySpecId and once after — and checks rm.projectId === eventProjectId both times. While the second check guards against a theoretical race during markFeatureDoneBySpecId, markFeatureDoneBySpecId is a synchronous Zustand mutation that cannot change projectId. The double-check pattern adds complexity without real protection. If the concern is truly about concurrency, a more robust pattern (e.g., a mutex or transactional update) would be warranted.

Suggested fix:

Consider reading state once after the mutation. The pre-mutation check is sufficient to decide whether to proceed; the post-mutation check is redundant for a synchronous store update.

🟠 [QLT-3] [HIGH] markFeatureDoneBySpecId silently skipped without logging

📁 apps/frontend/src/renderer/hooks/useIpc.ts:229

When rm is null, eventProjectId is falsy, or the projectId doesn't match, the feature completion is silently dropped with no log or warning. This could make debugging very difficult in production — a task completes successfully but the roadmap is never updated, and there's no trace of why.

Suggested fix:

Add an else branch with a debug/warn log: `console.warn('[useIpc] Skipping roadmap update: project mismatch or missing roadmap', { eventProjectId, roadmapProjectId: rm?.projectId });`

🟡 [QLT-4] [MEDIUM] Missing edge case: roadmap loaded after event arrives

📁 apps/frontend/src/renderer/hooks/useIpc.ts:228

If a task completion event arrives after loadRoadmap has cleared the store (setRoadmap(null)) but before the new roadmap has been loaded, the feature done mark will be permanently lost. The fix prevents cross-project contamination but introduces a window where legitimate same-project events are dropped. Consider queuing events or re-reconciling after load.

Suggested fix:

Consider maintaining a pending operations queue for task completion events that arrive during the async gap of loadRoadmap, and replay them once the roadmap is loaded.

🔵 [QLT-5] [LOW] Clearing store state without checking if already null

📁 apps/frontend/src/renderer/stores/roadmap-store.ts:733

In loadRoadmap, setRoadmap(null), setCompetitorAnalysis(null), and setGenerationStatus are called unconditionally. While not a bug, these trigger Zustand state updates and potential re-renders even when the store is already in the cleared state (e.g., on first load or consecutive calls).

Suggested fix:

Consider guarding with a check: `if (store.roadmap !== null) store.setRoadmap(null);` to avoid unnecessary re-renders.

🔵 [QLT-6] [LOW] Inconsistent error handling pattern between reconcile and IPC save

📁 apps/frontend/src/renderer/stores/roadmap-store.ts:718

Both the reconciliation save (roadmap-store.ts:718) and the IPC save (useIpc.ts:234) use .catch() to log errors but neither propagates or handles them further. While consistent with each other, the lack of any user-facing feedback or retry mechanism could lead to silent data loss.

Suggested fix:

Consider a centralized save-with-retry utility or at minimum a user-facing notification on persistent save failures.

🟠 [DEEP-1] [HIGH] Race condition: roadmap state can change between projectId check and markFeatureDoneBySpecId mutation

📁 apps/frontend/src/renderer/hooks/useIpc.ts:228

Between the check rm.projectId === eventProjectId and the call to markFeatureDoneBySpecId(taskId), another async operation (e.g., loadRoadmap for a different project) could replace the roadmap in the store. The code reads rm from state, validates projectId, then calls markFeatureDoneBySpecId which mutates whatever roadmap is currently in the store — not necessarily the one that was validated. The second check (updatedRm.projectId === eventProjectId) mitigates the save, but the mutation itself (markFeatureDoneBySpecId) would still corrupt the wrong project's in-memory roadmap.

Suggested fix:

Pass the eventProjectId into markFeatureDoneBySpecId and have it internally verify the roadmap's projectId before mutating, or perform the check and mutation atomically inside the store action.

🟡 [DEEP-2] [MEDIUM] reconcileLinkedFeatures applies mutations to store before checking projectId on save

📁 apps/frontend/src/renderer/stores/roadmap-store.ts:715

The guard at line 715 only prevents the save when projectId doesn't match, but the mutations that set hasChanges = true (earlier in the function, not shown in diff) have already been applied to the store's roadmap state. If a project switch happened during the async reconciliation, those mutations would have been applied to the new project's roadmap in memory. The guard prevents persisting the corrupted state, but the in-memory state is still wrong until the next load.

Suggested fix:

Check `updatedRoadmap.projectId === projectId` before applying any mutations to the store, or revert the mutations if the projectId no longer matches after the async gap.

🟡 [DEEP-3] [MEDIUM] Clearing roadmap to null on project switch may cause UI flash or null reference errors

📁 apps/frontend/src/renderer/stores/roadmap-store.ts:734

Setting roadmap to null immediately in loadRoadmap before the new roadmap is loaded creates a window where any component reading useRoadmapStore.getState().roadmap will see null. If components don't defensively handle null roadmap (and the old code didn't need to since roadmap was only replaced, never cleared mid-session), this could cause runtime errors or a visible UI flash/flicker showing an empty state before the new roadmap loads.

Suggested fix:

Ensure all UI consumers of the roadmap store handle null roadmap gracefully, or use a loading flag instead of nulling the roadmap, and only clear the old roadmap when the new one is ready to replace it.

🟡 [DEEP-4] [MEDIUM] Task completion events for previous project are silently dropped instead of deferred

📁 apps/frontend/src/renderer/hooks/useIpc.ts:227

When a task completes for project A but the user has switched to project B, the markFeatureDoneBySpecId call is skipped entirely. The roadmap.json for project A is never updated to reflect the task completion. This means the feature won't be marked as done until some other reconciliation mechanism runs (if one exists). The old bug was that it would write to the wrong project's file; the new behavior is that legitimate updates are lost.

Suggested fix:

Queue the update for the original project and apply it when that project's roadmap is next loaded, or load the correct project's roadmap from disk, apply the mutation, and save it back without going through the store.

🔵 [DEEP-5] [LOW] Redundant double-read of store state after markFeatureDoneBySpecId

📁 apps/frontend/src/renderer/hooks/useIpc.ts:226

The code reads useRoadmapStore.getState().roadmap before the mutation to check projectId, then reads it again after the mutation. The first read's rm variable is never used for the save — only for the guard. While not a bug, it's slightly confusing and the pattern could be simplified by reading state once after mutation and checking projectId there.

Suggested fix:

Simplify to a single state read after the mutation, checking projectId at that point.

This review was generated by Auto Claude.

I think that, on the contrary, changes need to be made to the review agent because it is completely off the mark as a review.

Findings that are wrong

DEEP-1 (HIGH) and SEC-2 — they claim there is a race condition between the check and markFeatureDoneBySpecId… but that is a synchronous mutation in a single-threaded event loop. Nothing can interrupt between the two. False positive.

DEEP-2 — same mistake: the mutations in the for loop of reconcileLinkedFeatures are synchronous after the single await getTasks.

Out-of-scope findings (pre-existing, not introduced by this PR):

SEC-1 — validation of projectId input → this is internal Electron IPC, not user input. And it was already like this before.

QLT-6 — no retry on save → pre-existing pattern, unchanged.

QLT-4 / DEEP-4 — events lost during the async gap → reconcileLinkedFeatures catches that on the next loadRoadmap. Not a new issue.

DEEP-3 — UI flash with null → the store was already doing setRoadmap(null) in the else branch (line 799), so the components already handle null.

minor findings:

QLT-1 — the eventProjectId alias is stylistically debatable, but it clarifies intent. Not a problem.

QLT-3 — a console.warn on the skip could help with debugging. The only potentially useful point, but definitely not HIGH.

QLT-5 — re-render on setRoadmap(null) when already null → negligible micro-optimization.

@Mitsu13Ion Mitsu13Ion requested a review from AndyMik90 February 22, 2026 12:37
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