Fix cross-process race condition and remove dead code
Use a separate manual_competitors.json file that the backend agent never overwrites, preventing data loss when users add competitors during analysis. Remove unused removeCompetitor and updateCompetitorAnalysis store actions. Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9b46709508
commit
517892cf31
@@ -31,6 +31,7 @@ class CompetitorAnalyzer:
|
||||
self.refresh = refresh
|
||||
self.agent_executor = agent_executor
|
||||
self.analysis_file = output_dir / "competitor_analysis.json"
|
||||
self.manual_competitors_file = output_dir / "manual_competitors.json"
|
||||
self.discovery_file = output_dir / "roadmap_discovery.json"
|
||||
self.project_index_file = output_dir / "project_index.json"
|
||||
|
||||
@@ -119,26 +120,47 @@ class CompetitorAnalyzer:
|
||||
)
|
||||
|
||||
def _get_manual_competitors(self) -> list[dict]:
|
||||
"""Extract manually-added competitors from the existing analysis file.
|
||||
"""Extract manually-added competitors from the dedicated manual file and analysis file.
|
||||
|
||||
Reads from manual_competitors.json (primary, never overwritten by agent) and
|
||||
falls back to competitor_analysis.json. Deduplicates by competitor ID.
|
||||
Returns a list of competitor dicts where source == 'manual'.
|
||||
Returns an empty list if the file doesn't exist or is malformed.
|
||||
"""
|
||||
if not self.analysis_file.exists():
|
||||
return []
|
||||
competitors_by_id: dict[str, dict] = {}
|
||||
|
||||
try:
|
||||
with open(self.analysis_file, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
# Primary source: dedicated manual competitors file (never overwritten by agent)
|
||||
if self.manual_competitors_file.exists():
|
||||
try:
|
||||
with open(self.manual_competitors_file, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
for c in data.get("competitors", []):
|
||||
if isinstance(c, dict) and c.get("id"):
|
||||
competitors_by_id[c["id"]] = c
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print_status(
|
||||
f"Warning: could not read manual competitors file: {e}", "warning"
|
||||
)
|
||||
|
||||
return [
|
||||
c
|
||||
for c in data.get("competitors", [])
|
||||
if isinstance(c, dict) and c.get("source") == "manual"
|
||||
]
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print_status(f"Warning: could not read manual competitors: {e}", "warning")
|
||||
return []
|
||||
# Fallback: also check analysis file for manual competitors
|
||||
if self.analysis_file.exists():
|
||||
try:
|
||||
with open(self.analysis_file, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
for c in data.get("competitors", []):
|
||||
if (
|
||||
isinstance(c, dict)
|
||||
and c.get("source") == "manual"
|
||||
and c.get("id")
|
||||
and c["id"] not in competitors_by_id
|
||||
):
|
||||
competitors_by_id[c["id"]] = c
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print_status(
|
||||
f"Warning: could not read manual competitors from analysis: {e}",
|
||||
"warning",
|
||||
)
|
||||
|
||||
return list(competitors_by_id.values())
|
||||
|
||||
def _merge_manual_competitors(self, manual_competitors: list[dict]) -> None:
|
||||
"""Merge manual competitors back into the newly-generated analysis file.
|
||||
|
||||
@@ -882,6 +882,44 @@ ${(feature.acceptance_criteria || []).map((c: string) => `- [ ] ${c}`).join("\n"
|
||||
);
|
||||
});
|
||||
|
||||
// Also persist manual competitors to a separate file that the backend
|
||||
// agent never overwrites, preventing data loss during concurrent analysis
|
||||
const manualCompetitors = competitorAnalysis.competitors.filter(
|
||||
(c) => c.source === "manual"
|
||||
);
|
||||
if (manualCompetitors.length > 0) {
|
||||
const manualCompetitorsPath = path.join(
|
||||
roadmapDir,
|
||||
AUTO_BUILD_PATHS.MANUAL_COMPETITORS
|
||||
);
|
||||
const manualSerialized = {
|
||||
competitors: manualCompetitors.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
url: c.url,
|
||||
description: c.description,
|
||||
relevance: c.relevance,
|
||||
pain_points: c.painPoints.map((p) => ({
|
||||
id: p.id,
|
||||
description: p.description,
|
||||
source: p.source,
|
||||
severity: p.severity,
|
||||
frequency: p.frequency,
|
||||
opportunity: p.opportunity,
|
||||
})),
|
||||
strengths: c.strengths,
|
||||
market_position: c.marketPosition,
|
||||
source: c.source,
|
||||
})),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
await writeFileWithRetry(
|
||||
manualCompetitorsPath,
|
||||
JSON.stringify(manualSerialized, null, 2),
|
||||
{ encoding: "utf-8" }
|
||||
);
|
||||
}
|
||||
|
||||
debugLog("[Roadmap Handler] Saved competitor analysis:", { projectId });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
|
||||
@@ -71,8 +71,6 @@ interface RoadmapState {
|
||||
updateFeaturePhase: (featureId: string, newPhaseId: string) => void;
|
||||
addFeature: (feature: Omit<RoadmapFeature, 'id'>) => string;
|
||||
addCompetitor: (input: ManualCompetitorInput) => string;
|
||||
removeCompetitor: (competitorId: string) => void;
|
||||
updateCompetitorAnalysis: (analysis: CompetitorAnalysis) => void;
|
||||
}
|
||||
|
||||
const initialGenerationStatus: RoadmapGenerationStatus = {
|
||||
@@ -322,23 +320,6 @@ export const useRoadmapStore = create<RoadmapState>((set) => ({
|
||||
return newId;
|
||||
},
|
||||
|
||||
// Remove a competitor by ID (used for rollback on save failure)
|
||||
removeCompetitor: (competitorId) => {
|
||||
set((state) => {
|
||||
if (!state.competitorAnalysis) return state;
|
||||
return {
|
||||
competitorAnalysis: {
|
||||
...state.competitorAnalysis,
|
||||
competitors: state.competitorAnalysis.competitors.filter(
|
||||
(c) => c.id !== competitorId
|
||||
)
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Replace the entire competitor analysis object
|
||||
updateCompetitorAnalysis: (analysis) => set({ competitorAnalysis: analysis })
|
||||
}));
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,6 +111,7 @@ export const AUTO_BUILD_PATHS = {
|
||||
ROADMAP_FILE: 'roadmap.json',
|
||||
ROADMAP_DISCOVERY: 'roadmap_discovery.json',
|
||||
COMPETITOR_ANALYSIS: 'competitor_analysis.json',
|
||||
MANUAL_COMPETITORS: 'manual_competitors.json',
|
||||
IDEATION_FILE: 'ideation.json',
|
||||
IDEATION_CONTEXT: 'ideation_context.json',
|
||||
PROJECT_INDEX: '.auto-claude/project_index.json',
|
||||
|
||||
Reference in New Issue
Block a user