auto-claude: 152-persist-tasks-during-roadmap-regeneration (#1463)
* auto-claude: subtask-1-1 - Add _load_existing_features() method to FeaturesPhase * auto-claude: subtask-1-2 - Add _merge_features() method to FeaturesPhase class - Adds _merge_features() method that combines preserved features with newly generated AI features while avoiding duplicates by ID - Preserved features take priority - if a new feature has the same ID, the new one is skipped - Includes debug logging for tracking merge statistics - Method returns merged list with preserved features first, then non-conflicting new features Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-1-3 - Include preserved features in FeaturesPhase context Modified FeaturesPhase._build_context() to: - Load existing preserved features using _load_existing_features() - Include preserved feature IDs and titles in the AI agent context - Instruct the AI to generate complementary features without duplicating - Add explicit instruction to avoid generating features with same IDs This ensures the AI agent is aware of existing features during roadmap regeneration, helping it create new features that complement rather than duplicate the preserved ones. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-1-4 - Modify FeaturesPhase._validate_features() to call _merge_features() After successful validation, the method now merges preserved features (planned/in_progress/done status, linked specs, internal sources) into the final roadmap.json before returning success. * auto-claude: subtask-2-1 - Add intermediate progress print statements in FeaturesPhase Added granular progress logging in FeaturesPhase.execute() for frontend parsing: - "Generating features..." - shown before running the agent - "Prioritizing features..." - shown after agent completes - "Creating roadmap file..." - shown before validation/merge These print_status calls will be parsed by the frontend for real-time progress updates. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-2-2 - Add intermediate progress print statements in DiscoveryPhase Added 'Analyzing project...' print_status call in DiscoveryPhase.execute() to provide intermediate progress feedback between 40% and 50% of roadmap generation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-3-1 - Enhance parseRoadmapProgress() in agent-events.ts Add 16 intermediate progress points for granular roadmap generation feedback: - Phase 1 (Project Analysis): 10%, 15%, 20%, 22%, 25% - Phase 2 (Discovery): 30%, 35%, 40%, 45%, 50% - Phase 3 (Feature Generation): 55%, 60%, 65%, 75%, 85%, 90% - Complete: 100% Progress matches backend log messages from phases.py for accurate tracking. Added safeguard to ensure progress only moves forward, never backward. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * auto-claude: subtask-4-3 - Add i18n translation keys for roadmap generation progress - Add roadmapGeneration.elapsed and roadmapGeneration.stillWorking keys to both EN and FR common.json files - Update RoadmapGenerationProgress.tsx to use t() for elapsed time and stall detection messages * auto-claude: subtask-5-1 - Run existing tests and fix test regressions Updated agent-events.test.ts to match new granular progress values: - PROJECT ANALYSIS: 20% → 10% - PROJECT DISCOVERY: 40% → 30% - FEATURE GENERATION: 70% → 55% Tests now pass for parseRoadmapProgress. Remaining 4 failing tests are pre-existing issues unrelated to this feature: - agent-process.test.ts: Environment-specific CLI path issue - usage-monitor.test.ts: Race condition prevention tests (3 failures) * fix: PR review issues - feature preservation bug, lint errors, and i18n Critical fixes from PR review: 1. Feature preservation data loss bug (phases.py): - Load preserved features ONCE before agent runs and store in instance var - Use stored features in _build_context() and _validate_features() - Prevents data loss when agent overwrites roadmap.json 2. Lint fixes: - Add radix parameter to parseInt() in app-logger.test.ts - Add biome-ignore for intentional control chars in download-python.cjs - Replace non-null assertions with optional chaining in tests - Add biome-ignore comments for test mock types 3. i18n fixes (RoadmapGenerationProgress.tsx): - Add translation keys for Stop button text - Add translation keys for phase labels and descriptions - Update en/common.json and fr/common.json with new keys Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: ruff errors in phases.py - undefined name and f-string - Remove unnecessary f-string prefix (F541) - Fix undefined `preserved_features` to `self._preserved_features` (F821) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * style: fix ruff format - wrap long line in phases.py Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address PR review findings 1. phases.py: Add try-except for OSError on file write after merge 2. RoadmapGenerationProgress.tsx: - Add isMounted ref pattern to prevent state update on unmounted component - Reset elapsedSeconds when generation ends 3. agent-events.ts: Make discovery progress condition more specific - Exclude error/failed logs from triggering 50% progress Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address PR review findings - i18n, stall detection, and debug warning - Add i18n translation key for 'Progress' label (NCR-F01 MEDIUM) Added 'common:roadmapGeneration.progress' to en and fr locales - Optimize stall detection interval (NCR-F02 LOW) Use ref instead of state for lastProgressChange to avoid recreating the interval on every progress update - Add debug warning for features without IDs (NCR-F03 LOW) Warn when features lack IDs as they cannot be deduplicated Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address code quality findings from PR review - Convert startTime state to ref to remove confusing effect dependency - Add title-based fallback deduplication for features without IDs - Add explicit upper bound cap (100) to progress values - Handle write failure gracefully by proceeding with AI-generated version - Only update stall state when value actually changes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address PR review quality findings and ruff formatting - Fix stall detection mount edge case with hasInitializedRef - Add preserved feature count to OSError warning message - Add phase regression prevention to parseRoadmapProgress - Fix ruff formatting issues Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -117,6 +117,9 @@ class DiscoveryPhase:
|
||||
"discovery", True, [str(self.discovery_file)], [], 0
|
||||
)
|
||||
|
||||
# Provide intermediate progress status
|
||||
print_status("Analyzing project...", "progress")
|
||||
|
||||
errors = []
|
||||
for attempt in range(MAX_RETRIES):
|
||||
debug("roadmap_phase", f"Discovery attempt {attempt + 1}/{MAX_RETRIES}")
|
||||
@@ -212,6 +215,146 @@ class FeaturesPhase:
|
||||
self.roadmap_file = output_dir / "roadmap.json"
|
||||
self.discovery_file = output_dir / "roadmap_discovery.json"
|
||||
self.project_index_file = output_dir / "project_index.json"
|
||||
# Preserved features loaded ONCE before agent runs and overwrites the file
|
||||
self._preserved_features: list[dict] = []
|
||||
|
||||
def _load_existing_features(self) -> list[dict]:
|
||||
"""Load features from existing roadmap that should be preserved.
|
||||
|
||||
Preserves features that meet any of these criteria:
|
||||
- status is 'planned', 'in_progress', or 'done'
|
||||
- has a linked_spec_id (converted to task)
|
||||
- source.provider is 'internal' (user-added)
|
||||
|
||||
Returns:
|
||||
List of feature dictionaries to preserve, empty list if no roadmap exists
|
||||
or on error.
|
||||
"""
|
||||
if not self.roadmap_file.exists():
|
||||
debug("roadmap_phase", "No existing roadmap.json to load features from")
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(self.roadmap_file, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
features = data.get("features", [])
|
||||
preserved = []
|
||||
|
||||
for feature in features:
|
||||
# Check if feature should be preserved
|
||||
status = feature.get("status")
|
||||
has_linked_spec = bool(feature.get("linked_spec_id"))
|
||||
source = feature.get("source", {})
|
||||
is_internal = (
|
||||
isinstance(source, dict) and source.get("provider") == "internal"
|
||||
)
|
||||
|
||||
if status in ("planned", "in_progress", "done"):
|
||||
preserved.append(feature)
|
||||
debug_detailed(
|
||||
"roadmap_phase",
|
||||
f"Preserving feature due to status: {status}",
|
||||
feature_id=feature.get("id"),
|
||||
)
|
||||
elif has_linked_spec:
|
||||
preserved.append(feature)
|
||||
debug_detailed(
|
||||
"roadmap_phase",
|
||||
"Preserving feature due to linked_spec_id",
|
||||
feature_id=feature.get("id"),
|
||||
linked_spec_id=feature.get("linked_spec_id"),
|
||||
)
|
||||
elif is_internal:
|
||||
preserved.append(feature)
|
||||
debug_detailed(
|
||||
"roadmap_phase",
|
||||
"Preserving feature due to internal source",
|
||||
feature_id=feature.get("id"),
|
||||
)
|
||||
|
||||
debug(
|
||||
"roadmap_phase",
|
||||
f"Loaded {len(preserved)} features to preserve from existing roadmap",
|
||||
)
|
||||
return preserved
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
debug_error(
|
||||
"roadmap_phase",
|
||||
"Failed to parse existing roadmap.json",
|
||||
error=str(e),
|
||||
)
|
||||
return []
|
||||
except (KeyError, TypeError) as e:
|
||||
debug_error(
|
||||
"roadmap_phase",
|
||||
"Error reading features from roadmap.json",
|
||||
error=str(e),
|
||||
)
|
||||
return []
|
||||
|
||||
def _merge_features(
|
||||
self, new_features: list[dict], preserved: list[dict]
|
||||
) -> list[dict]:
|
||||
"""Merge new AI-generated features with preserved features.
|
||||
|
||||
Preserved features take priority - if a new feature has the same ID
|
||||
as a preserved feature, the new feature is skipped. For features
|
||||
without IDs, title-based deduplication is used as a fallback.
|
||||
|
||||
Args:
|
||||
new_features: List of newly generated features from AI
|
||||
preserved: List of features to preserve from existing roadmap
|
||||
|
||||
Returns:
|
||||
Merged list with preserved features first, then non-conflicting new features
|
||||
"""
|
||||
if not preserved:
|
||||
debug("roadmap_phase", "No preserved features, returning new features only")
|
||||
return new_features
|
||||
|
||||
preserved_ids = {f.get("id") for f in preserved if f.get("id")}
|
||||
# Build normalized title set for fallback deduplication
|
||||
preserved_titles = {
|
||||
f.get("title", "").strip().lower() for f in preserved if f.get("title")
|
||||
}
|
||||
|
||||
# Start with all preserved features
|
||||
merged = list(preserved)
|
||||
added_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
# Add new features that don't conflict with preserved ones
|
||||
for feature in new_features:
|
||||
feature_id = feature.get("id")
|
||||
feature_title = feature.get("title", "").strip()
|
||||
normalized_title = feature_title.lower()
|
||||
|
||||
if feature_id and feature_id in preserved_ids:
|
||||
debug_detailed(
|
||||
"roadmap_phase",
|
||||
"Skipping duplicate feature (by ID)",
|
||||
feature_id=feature_id,
|
||||
)
|
||||
skipped_count += 1
|
||||
elif normalized_title and normalized_title in preserved_titles:
|
||||
# Title-based fallback deduplication for features without IDs
|
||||
debug_detailed(
|
||||
"roadmap_phase",
|
||||
"Skipping duplicate feature (by title)",
|
||||
title=feature_title,
|
||||
)
|
||||
skipped_count += 1
|
||||
else:
|
||||
merged.append(feature)
|
||||
added_count += 1
|
||||
|
||||
debug(
|
||||
"roadmap_phase",
|
||||
f"Merged features: {len(preserved)} preserved, {added_count} new added, {skipped_count} duplicates skipped",
|
||||
)
|
||||
return merged
|
||||
|
||||
async def execute(self) -> RoadmapPhaseResult:
|
||||
"""Generate and prioritize features for the roadmap."""
|
||||
@@ -232,13 +375,20 @@ class FeaturesPhase:
|
||||
print_status("roadmap.json already exists", "success")
|
||||
return RoadmapPhaseResult("features", True, [str(self.roadmap_file)], [], 0)
|
||||
|
||||
# Load preserved features BEFORE the agent runs and overwrites the file
|
||||
# This must happen once, before the retry loop, to capture the original state
|
||||
self._preserved_features = self._load_existing_features()
|
||||
|
||||
errors = []
|
||||
for attempt in range(MAX_RETRIES):
|
||||
debug("roadmap_phase", f"Features attempt {attempt + 1}/{MAX_RETRIES}")
|
||||
print_status(
|
||||
f"Running feature generation agent (attempt {attempt + 1})...",
|
||||
"progress",
|
||||
)
|
||||
if attempt > 0:
|
||||
print_status(
|
||||
f"Retrying feature generation (attempt {attempt + 1})...",
|
||||
"progress",
|
||||
)
|
||||
|
||||
print_status("Generating features...", "progress")
|
||||
|
||||
context = self._build_context()
|
||||
success, output = await self.agent_executor.run_agent(
|
||||
@@ -247,6 +397,8 @@ class FeaturesPhase:
|
||||
)
|
||||
|
||||
if success and self.roadmap_file.exists():
|
||||
print_status("Prioritizing features...", "progress")
|
||||
print_status("Creating roadmap file...", "progress")
|
||||
validation_result = self._validate_features(attempt)
|
||||
if validation_result is not None:
|
||||
return validation_result
|
||||
@@ -266,24 +418,56 @@ class FeaturesPhase:
|
||||
return RoadmapPhaseResult("features", False, [], errors, MAX_RETRIES)
|
||||
|
||||
def _build_context(self) -> str:
|
||||
"""Build context string for the features agent."""
|
||||
"""Build context string for the features agent.
|
||||
|
||||
If there are preserved features from an existing roadmap, includes them
|
||||
in the context so the AI agent can generate complementary features
|
||||
without duplicating existing ones.
|
||||
"""
|
||||
# Use the pre-loaded preserved features (loaded before agent ran)
|
||||
# This ensures we use the original features even on retry attempts
|
||||
# after the file has been overwritten by a failed attempt
|
||||
|
||||
# Build preserved features section if any exist
|
||||
preserved_section = ""
|
||||
if self._preserved_features:
|
||||
preserved_ids = [f.get("id", "unknown") for f in self._preserved_features]
|
||||
preserved_titles = [
|
||||
f.get("title", "Untitled") for f in self._preserved_features
|
||||
]
|
||||
preserved_info = "\n".join(
|
||||
f" - {fid}: {title}"
|
||||
for fid, title in zip(preserved_ids, preserved_titles)
|
||||
)
|
||||
preserved_section = f"""
|
||||
**EXISTING FEATURES TO PRESERVE** (DO NOT regenerate these):
|
||||
The following {len(self._preserved_features)} features already exist and will be preserved.
|
||||
Generate NEW features that complement these, do not duplicate them:
|
||||
{preserved_info}
|
||||
|
||||
"""
|
||||
|
||||
return f"""
|
||||
**Discovery File**: {self.discovery_file}
|
||||
**Project Index**: {self.project_index_file}
|
||||
**Output File**: {self.roadmap_file}
|
||||
|
||||
{preserved_section}
|
||||
Based on the discovery data:
|
||||
1. Generate features that address user pain points
|
||||
2. Prioritize using MoSCoW framework
|
||||
3. Organize into phases
|
||||
4. Create milestones
|
||||
5. Map dependencies
|
||||
{"6. Do NOT generate features with the same IDs as preserved features listed above" if self._preserved_features else ""}
|
||||
|
||||
Output the complete roadmap to roadmap.json.
|
||||
"""
|
||||
|
||||
def _validate_features(self, attempt: int) -> RoadmapPhaseResult | None:
|
||||
"""Validate the roadmap features file.
|
||||
"""Validate the roadmap features file and merge preserved features.
|
||||
|
||||
After successful validation, merges any preserved features from the
|
||||
previous roadmap into the final roadmap.json.
|
||||
|
||||
Returns RoadmapPhaseResult if validation succeeds, None otherwise.
|
||||
"""
|
||||
@@ -314,11 +498,49 @@ Output the complete roadmap to roadmap.json.
|
||||
)
|
||||
|
||||
if not missing and feature_count >= 3:
|
||||
# Merge preserved features into the roadmap
|
||||
# Use the pre-loaded preserved features (loaded before agent ran)
|
||||
if self._preserved_features:
|
||||
new_features = data.get("features", [])
|
||||
merged_features = self._merge_features(
|
||||
new_features, self._preserved_features
|
||||
)
|
||||
data["features"] = merged_features
|
||||
|
||||
# Write back the merged roadmap
|
||||
try:
|
||||
with open(self.roadmap_file, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
debug_success(
|
||||
"roadmap_phase",
|
||||
"Merged preserved features into roadmap.json",
|
||||
preserved_count=len(self._preserved_features),
|
||||
final_count=len(merged_features),
|
||||
)
|
||||
print_status(
|
||||
f"Merged {len(self._preserved_features)} preserved features",
|
||||
"success",
|
||||
)
|
||||
except OSError as e:
|
||||
# Write failed but the original AI-generated roadmap is still valid
|
||||
# Don't fail the whole phase - succeed without the merge
|
||||
preserved_count = len(self._preserved_features)
|
||||
debug_warning(
|
||||
"roadmap_phase",
|
||||
"Failed to write merged roadmap - proceeding with AI-generated version",
|
||||
error=str(e),
|
||||
preserved_features_lost=preserved_count,
|
||||
)
|
||||
print_status(
|
||||
f"Warning: {preserved_count} preserved features could not be saved (disk error: {e})",
|
||||
"warning",
|
||||
)
|
||||
|
||||
debug_success(
|
||||
"roadmap_phase",
|
||||
"Created valid roadmap.json",
|
||||
attempt=attempt + 1,
|
||||
feature_count=feature_count,
|
||||
feature_count=len(data.get("features", [])),
|
||||
)
|
||||
print_status("Created valid roadmap.json", "success")
|
||||
return RoadmapPhaseResult(
|
||||
|
||||
@@ -1118,6 +1118,7 @@ function validateInput(value, validValues, name) {
|
||||
|
||||
// Remove any control characters or newlines (ASCII 0-31 and 127)
|
||||
// eslint-disable-next-line no-control-regex
|
||||
// biome-ignore lint/suspicious/noControlCharactersInRegex: Intentional - sanitizing input by removing control characters
|
||||
const sanitized = String(value).replace(/[\x00-\x1f\x7f]/g, '');
|
||||
|
||||
if (!validValues.includes(sanitized)) {
|
||||
|
||||
@@ -152,6 +152,7 @@ describe('Claude Profile IPC Integration', () => {
|
||||
|
||||
// Import and call the registration function
|
||||
const { registerTerminalHandlers } = await import('../../main/ipc-handlers/terminal-handlers');
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Test mock types don't match production types
|
||||
registerTerminalHandlers(mockTerminalManager as any, () => mockBrowserWindow as any);
|
||||
});
|
||||
|
||||
@@ -171,7 +172,7 @@ describe('Claude Profile IPC Integration', () => {
|
||||
name: 'New Account'
|
||||
});
|
||||
|
||||
const result = await handleProfileSave!(null, newProfile) as IPCResult<ClaudeProfile>;
|
||||
const result = await handleProfileSave?.(null, newProfile) as IPCResult<ClaudeProfile>;
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockProfileManager.generateProfileId).toHaveBeenCalledWith('New Account');
|
||||
@@ -190,7 +191,7 @@ describe('Claude Profile IPC Integration', () => {
|
||||
name: 'Existing Account'
|
||||
});
|
||||
|
||||
const result = await handleProfileSave!(null, existingProfile) as IPCResult<ClaudeProfile>;
|
||||
const result = await handleProfileSave?.(null, existingProfile) as IPCResult<ClaudeProfile>;
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(mockProfileManager.generateProfileId).not.toHaveBeenCalled();
|
||||
@@ -206,8 +207,9 @@ describe('Claude Profile IPC Integration', () => {
|
||||
configDir: path.join(TEST_DIR, 'new-profile-config')
|
||||
});
|
||||
|
||||
await handleProfileSave!(null, profile);
|
||||
await handleProfileSave?.(null, profile);
|
||||
|
||||
// biome-ignore lint/style/noNonNullAssertion: Test file - configDir is set in createTestProfile
|
||||
expect(existsSync(profile.configDir!)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -220,8 +222,9 @@ describe('Claude Profile IPC Integration', () => {
|
||||
configDir: path.join(TEST_DIR, 'should-not-exist')
|
||||
});
|
||||
|
||||
await handleProfileSave!(null, profile);
|
||||
await handleProfileSave?.(null, profile);
|
||||
|
||||
// biome-ignore lint/style/noNonNullAssertion: Test file - configDir is set in createTestProfile
|
||||
expect(existsSync(profile.configDir!)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -234,7 +237,7 @@ describe('Claude Profile IPC Integration', () => {
|
||||
});
|
||||
|
||||
const profile = createTestProfile();
|
||||
const result = await handleProfileSave!(null, profile) as IPCResult;
|
||||
const result = await handleProfileSave?.(null, profile) as IPCResult;
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Database error');
|
||||
|
||||
@@ -482,36 +482,39 @@ describe('AgentEvents', () => {
|
||||
);
|
||||
|
||||
expect(result.phase).toBe('analyzing');
|
||||
expect(result.progress).toBe(20);
|
||||
// Updated to match granular progress values: PROJECT ANALYSIS → 10%
|
||||
expect(result.progress).toBe(10);
|
||||
});
|
||||
|
||||
it('should detect discovering phase', () => {
|
||||
const result = agentEvents.parseRoadmapProgress(
|
||||
'PROJECT DISCOVERY in progress',
|
||||
'analyzing',
|
||||
20
|
||||
25
|
||||
);
|
||||
|
||||
expect(result.phase).toBe('discovering');
|
||||
expect(result.progress).toBe(40);
|
||||
// Updated to match granular progress values: PROJECT DISCOVERY → 30%
|
||||
expect(result.progress).toBe(30);
|
||||
});
|
||||
|
||||
it('should detect generating phase', () => {
|
||||
const result = agentEvents.parseRoadmapProgress(
|
||||
'FEATURE GENERATION starting',
|
||||
'discovering',
|
||||
40
|
||||
50
|
||||
);
|
||||
|
||||
expect(result.phase).toBe('generating');
|
||||
expect(result.progress).toBe(70);
|
||||
// Updated to match granular progress values: FEATURE GENERATION → 55%
|
||||
expect(result.progress).toBe(55);
|
||||
});
|
||||
|
||||
it('should detect complete phase', () => {
|
||||
const result = agentEvents.parseRoadmapProgress(
|
||||
'ROADMAP GENERATED successfully',
|
||||
'generating',
|
||||
70
|
||||
90
|
||||
);
|
||||
|
||||
expect(result.phase).toBe('complete');
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('Application Logger', () => {
|
||||
|
||||
const info = getSystemInfo();
|
||||
|
||||
expect(parseInt(info.cpuCores)).toBeGreaterThan(0);
|
||||
expect(parseInt(info.cpuCores, 10)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -171,25 +171,106 @@ export class AgentEvents {
|
||||
|
||||
/**
|
||||
* Parse roadmap progress from log output
|
||||
* Provides granular progress updates (8+ intermediate points) for better UX feedback
|
||||
*/
|
||||
parseRoadmapProgress(log: string, currentPhase: string, currentProgress: number): { phase: string; progress: number } {
|
||||
// Define roadmap phase order to prevent regression
|
||||
const ROADMAP_PHASE_ORDER: Record<string, number> = {
|
||||
'idle': 0,
|
||||
'analyzing': 1,
|
||||
'discovering': 2,
|
||||
'generating': 3,
|
||||
'complete': 4,
|
||||
'error': 5,
|
||||
};
|
||||
|
||||
const wouldRoadmapPhaseRegress = (current: string, next: string): boolean => {
|
||||
const currentOrder = ROADMAP_PHASE_ORDER[current] ?? -1;
|
||||
const nextOrder = ROADMAP_PHASE_ORDER[next] ?? -1;
|
||||
// Allow progression to error from any phase, but otherwise prevent regression
|
||||
if (next === 'error') return false;
|
||||
return nextOrder < currentOrder;
|
||||
};
|
||||
|
||||
let phase = currentPhase;
|
||||
let progress = currentProgress;
|
||||
let detectedPhase = currentPhase;
|
||||
|
||||
// Phase 1: Project Analysis (10-25%)
|
||||
if (log.includes('PROJECT ANALYSIS')) {
|
||||
phase = 'analyzing';
|
||||
detectedPhase = 'analyzing';
|
||||
progress = 10;
|
||||
} else if (log.includes('Copied existing project_index')) {
|
||||
detectedPhase = 'analyzing';
|
||||
progress = 15;
|
||||
} else if (log.includes('Running project analyzer')) {
|
||||
detectedPhase = 'analyzing';
|
||||
progress = 20;
|
||||
} else if (log.includes('PROJECT DISCOVERY')) {
|
||||
phase = 'discovering';
|
||||
} else if (log.includes('project_index.json already exists')) {
|
||||
detectedPhase = 'analyzing';
|
||||
progress = 22;
|
||||
} else if (log.includes('Created project_index')) {
|
||||
detectedPhase = 'analyzing';
|
||||
progress = 25;
|
||||
}
|
||||
|
||||
// Phase 2: Discovery (30-50%)
|
||||
else if (log.includes('PROJECT DISCOVERY')) {
|
||||
detectedPhase = 'discovering';
|
||||
progress = 30;
|
||||
} else if (log.includes('Analyzing project')) {
|
||||
detectedPhase = 'discovering';
|
||||
progress = 35;
|
||||
} else if (log.includes('Running discovery agent')) {
|
||||
detectedPhase = 'discovering';
|
||||
progress = 40;
|
||||
} else if (log.includes('FEATURE GENERATION')) {
|
||||
phase = 'generating';
|
||||
progress = 70;
|
||||
} else if (log.includes('ROADMAP GENERATED')) {
|
||||
phase = 'complete';
|
||||
} else if (log.includes('Discovery attempt')) {
|
||||
detectedPhase = 'discovering';
|
||||
progress = 45;
|
||||
} else if (
|
||||
log.includes('roadmap_discovery.json') &&
|
||||
!log.toLowerCase().includes('failed') &&
|
||||
!log.toLowerCase().includes('error')
|
||||
) {
|
||||
detectedPhase = 'discovering';
|
||||
progress = 50;
|
||||
}
|
||||
|
||||
// Phase 3: Feature Generation (55-95%)
|
||||
else if (log.includes('FEATURE GENERATION')) {
|
||||
detectedPhase = 'generating';
|
||||
progress = 55;
|
||||
} else if (log.includes('Generating features')) {
|
||||
detectedPhase = 'generating';
|
||||
progress = 60;
|
||||
} else if (log.includes('Features attempt')) {
|
||||
detectedPhase = 'generating';
|
||||
progress = 65;
|
||||
} else if (log.includes('Prioritizing features')) {
|
||||
detectedPhase = 'generating';
|
||||
progress = 75;
|
||||
} else if (log.includes('Creating roadmap file')) {
|
||||
detectedPhase = 'generating';
|
||||
progress = 85;
|
||||
} else if (log.includes('Created valid roadmap')) {
|
||||
detectedPhase = 'generating';
|
||||
progress = 90;
|
||||
}
|
||||
|
||||
// Complete
|
||||
else if (log.includes('ROADMAP GENERATED')) {
|
||||
detectedPhase = 'complete';
|
||||
progress = 100;
|
||||
}
|
||||
|
||||
// Apply phase only if it doesn't regress (prevents visual inconsistency)
|
||||
if (!wouldRoadmapPhaseRegress(currentPhase, detectedPhase)) {
|
||||
phase = detectedPhase;
|
||||
}
|
||||
|
||||
// Ensure progress only moves forward (never backward) and stays within bounds (0-100)
|
||||
progress = Math.min(100, Math.max(progress, currentProgress));
|
||||
|
||||
return { phase, progress };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,6 +551,41 @@
|
||||
"step3": "Click \"Authenticate\" to complete login",
|
||||
"footer": "The account will be available once you complete authentication."
|
||||
},
|
||||
"roadmapGeneration": {
|
||||
"progress": "Progress",
|
||||
"elapsed": "Elapsed: {{time}}",
|
||||
"stillWorking": "Still working...",
|
||||
"stopping": "Stopping...",
|
||||
"stop": "Stop",
|
||||
"stopTooltip": "Stop generation",
|
||||
"phases": {
|
||||
"analyzing": {
|
||||
"label": "Analyzing",
|
||||
"description": "Analyzing project structure and codebase..."
|
||||
},
|
||||
"discovering": {
|
||||
"label": "Discovering",
|
||||
"description": "Discovering target audience and user needs..."
|
||||
},
|
||||
"generating": {
|
||||
"label": "Generating",
|
||||
"description": "Generating feature roadmap..."
|
||||
},
|
||||
"complete": {
|
||||
"label": "Complete",
|
||||
"description": "Roadmap generation complete!"
|
||||
},
|
||||
"error": {
|
||||
"label": "Error",
|
||||
"description": "Generation failed"
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"analyze": "Analyze",
|
||||
"discover": "Discover",
|
||||
"generate": "Generate"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"failure": {
|
||||
"title": "Authentication Required",
|
||||
|
||||
@@ -551,6 +551,41 @@
|
||||
"step3": "Cliquez sur « Authentifier » pour terminer la connexion",
|
||||
"footer": "Le compte sera disponible une fois l'authentification terminée."
|
||||
},
|
||||
"roadmapGeneration": {
|
||||
"progress": "Progression",
|
||||
"elapsed": "Écoulé : {{time}}",
|
||||
"stillWorking": "Toujours en cours...",
|
||||
"stopping": "Arrêt...",
|
||||
"stop": "Arrêter",
|
||||
"stopTooltip": "Arrêter la génération",
|
||||
"phases": {
|
||||
"analyzing": {
|
||||
"label": "Analyse",
|
||||
"description": "Analyse de la structure et du code du projet..."
|
||||
},
|
||||
"discovering": {
|
||||
"label": "Découverte",
|
||||
"description": "Découverte du public cible et des besoins des utilisateurs..."
|
||||
},
|
||||
"generating": {
|
||||
"label": "Génération",
|
||||
"description": "Génération de la feuille de route des fonctionnalités..."
|
||||
},
|
||||
"complete": {
|
||||
"label": "Terminé",
|
||||
"description": "Génération de la feuille de route terminée !"
|
||||
},
|
||||
"error": {
|
||||
"label": "Erreur",
|
||||
"description": "La génération a échoué"
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"analyze": "Analyser",
|
||||
"discover": "Découvrir",
|
||||
"generate": "Générer"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"failure": {
|
||||
"title": "Authentification requise",
|
||||
|
||||
Reference in New Issue
Block a user