Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
ad7cea9
docs: add dependency visualization design document
ajianaz Jan 5, 2026
e711a8d
docs: add dependency visualization implementation plan
ajianaz Jan 5, 2026
1a840c7
feat(backend): add dependency validator with missing and circular dep…
ajianaz Jan 5, 2026
1399e0e
feat(frontend): add reverse dependencies and validation fields to Roa…
ajianaz Jan 5, 2026
09b3dd9
feat(frontend): add dependencies section to FeatureCard component
ajianaz Jan 5, 2026
3324304
feat(frontend): add DependencyDetailSidePanel component
ajianaz Jan 5, 2026
d743638
fix(frontend): add i18n, accessibility, and keyboard navigation to De…
ajianaz Jan 5, 2026
188392d
fix(frontend): register roadmap i18n namespace
ajianaz Jan 5, 2026
33b941a
feat(frontend): integrate DependencyDetailSidePanel with roadmap views
ajianaz Jan 5, 2026
b8b3ca9
fix(frontend): make dependency indicators clickable and add i18n for …
ajianaz Jan 5, 2026
e575475
fix(backend): deduplicate circular dependency paths to prevent duplic…
ajianaz Jan 5, 2026
977daf4
feat(backend): integrate dependency validator into roadmap generation
ajianaz Jan 5, 2026
6af9728
fix(backend): resolve missing imports, test coverage, and performance…
ajianaz Jan 5, 2026
cf955ad
test(frontend): add unit tests for FeatureCard dependencies section
ajianaz Jan 5, 2026
b7f3990
docs: add dependency visualization testing checklist
ajianaz Jan 5, 2026
ae3c37e
docs: update roadmap documentation with dependency visualization
ajianaz Jan 5, 2026
60f748e
polish: complete dependency visualization feature implementation
ajianaz Jan 5, 2026
c88fd2a
fix(frontend): unify dependency click behavior across all roadmap views
ajianaz Jan 5, 2026
5eee441
feat(frontend): enhance dependency navigation experience
ajianaz Jan 6, 2026
92074cb
fix(backend): resolve import issues and add roadmap enrichment script
ajianaz Jan 6, 2026
c89f6c4
fix(frontend): add reverseDependencies support to roadmap data flow
ajianaz Jan 6, 2026
b5d4375
feat(frontend): improve dependency display with feature titles
ajianaz Jan 6, 2026
f74a687
style(frontend): unify Dependencies and Required By styling
ajianaz Jan 6, 2026
a828f7c
Merge upstream/develop into feature branch
ajianaz Jan 6, 2026
df6f6aa
feat(roadmap): implement bidirectional dependency visualization
ajianaz Jan 6, 2026
639e344
fix(frontend): add type="button" to all button elements
ajianaz Jan 6, 2026
6d19468
Merge branch 'develop' into feature/roadmap-dependency-visualization
ajianaz Jan 6, 2026
5cdeb04
fix(frontend): display feature titles in DependencyDetailSidePanel
ajianaz Jan 6, 2026
aa6c48e
perf(backend): optimize feature lookup in roadmap enrichment O(N²) → …
ajianaz Jan 6, 2026
27974c8
fix(backend): surface enrichment errors instead of swallowing them
ajianaz Jan 6, 2026
8d59cad
fix(frontend): remove redundant isOpen check in DependencyDetailSideP…
ajianaz Jan 6, 2026
1eb8e9f
refactor(tests): use truthiness checks for boolean assertions
ajianaz Jan 6, 2026
f36ff4f
refactor(backend): use PEP 8 snake_case for RoadmapFeature fields
ajianaz Jan 6, 2026
3ff2384
refactor(frontend): extract getFeatureById to shared utility
ajianaz Jan 6, 2026
5016b8f
i18n(frontend): internationalize SortableFeatureCard dependency labels
ajianaz Jan 6, 2026
8af25a8
Merge branch 'develop' into feature/roadmap-dependency-visualization
ajianaz Jan 6, 2026
7a35849
fix(frontend): include reverseDependencies and dependencyValidation i…
ajianaz Jan 6, 2026
34a5401
fix(tests): move test_full_validation_workflow before __main__ block
ajianaz Jan 6, 2026
85827de
fix: address CodeQL and lint warnings
ajianaz Jan 6, 2026
6cf657e
fix(ci): resolve all CI failures
ajianaz Jan 6, 2026
f7cf2f2
fix(codeql): make all returns explicit in _enrich_roadmap_features
ajianaz Jan 6, 2026
9474e6c
fix(tests): add proper Roadmap type to mockRoadmap
ajianaz Jan 6, 2026
03a1144
fix(frontend): add runtime validation for source provider in transfor…
ajianaz Jan 6, 2026
1cf47b2
i18n(frontend): internationalize dependency button tooltips
ajianaz Jan 6, 2026
c3fa5a7
refactor(tests): extract mock store factory and fix type safety
ajianaz Jan 6, 2026
b8b759a
test(backend): make reverse dependencies assertions order-independent
ajianaz Jan 6, 2026
4dc5025
refactor(frontend): i18n improvements and store refactoring
ajianaz Jan 6, 2026
fa78a50
Merge branch 'develop' into feature/roadmap-dependency-visualization
ajianaz Jan 6, 2026
812813b
i18n(frontend): internationalize action button labels in FeatureDetai…
ajianaz Jan 6, 2026
72caed3
i18n(frontend): internationalize remaining hardcoded labels in Featur…
ajianaz Jan 6, 2026
d8f2302
i18n(frontend): internationalize Acceptance Criteria label in Feature…
ajianaz Jan 6, 2026
21cef5b
i18n(frontend): add Indonesian (ID) language support for roadmap
ajianaz Jan 6, 2026
3bb2d53
Merge branch 'origin/develop' into feature/roadmap-dependency-visuali…
ajianaz Jan 9, 2026
4ce8ab7
Merge remote-tracking branch 'origin/develop' into feature/roadmap-de…
ajianaz Jan 9, 2026
abf0950
fix(tests): use patch context manager for is_build_complete mocking
ajianaz Jan 9, 2026
848ff65
refactor(frontend): remove redundant features prop from FeatureCard
ajianaz Jan 9, 2026
03b0c8e
Merge branch 'develop' into feature/roadmap-dependency-visualization
AlexMadera Jan 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
## [Unreleased]

### Added
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Minor: Add blank lines around section headings.

Per MD022, headings should be surrounded by blank lines for better readability and markdown best practices.

🔎 Proposed fixes
 ## [Unreleased]
 
+
 ### Added
 - **Roadmap:** Dependency visualization in all roadmap views
 - **Roadmap:** Reverse dependency calculation and display
 
+
 ### Changed
 - Enhanced roadmap data model with dependency metadata
 - Improved roadmap refresh with dependency preservation (TODO in future phase)
 
+
 ### Fixed
 - Duplicate circular dependency path reporting in validator

Also applies to: 11-11, 15-15

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

3-3: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
In @CHANGELOG.md around line 3, Add blank lines before and after Markdown
section headings in CHANGELOG.md (e.g., the "### Added" heading and the other
headings noted at lines 11 and 15) so each heading is surrounded by a single
empty line; update the file by inserting a blank line above and below each
heading to satisfy MD022 and improve readability.

- **Roadmap:** Dependency visualization in all roadmap views
- **Roadmap:** Bidirectional dependency display (dependencies and reverse dependencies)
- **Roadmap:** Dependency validation with circular and missing dependency detection
- **Roadmap:** Clickable dependency chips with detail side panel
- **Roadmap:** Dependency status indicators (completed, in-progress, planned, missing)
- **Roadmap:** Reverse dependency calculation and display

### Changed
- Enhanced roadmap data model with dependency metadata
- Improved roadmap refresh with dependency preservation (TODO in future phase)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Document the TODO or track it separately.

The line mentions "TODO in future phase" for dependency preservation. Consider creating a tracking issue for this future work or clarifying the timeline.

Would you like me to help create an issue to track this future enhancement?

🤖 Prompt for AI Agents
In @CHANGELOG.md around line 13, The CHANGELOG entry "Improved roadmap refresh
with dependency preservation (TODO in future phase)" leaves the TODO untracked;
either replace that parenthetical with a link/reference to a created tracking
issue or add a brief timeline and issue ID, e.g., create an issue in the repo
titled "Dependency preservation for roadmap refresh" and update the CHANGELOG
line to reference that issue (or include an ETA), so the future work is
discoverable and actionable.


### Fixed
- Duplicate circular dependency path reporting in validator
- Missing i18n namespace registration for roadmap translations

## 2.7.2 - Stability & Performance Enhancements

### ✨ New Features
Expand Down
15 changes: 15 additions & 0 deletions apps/backend/runners/roadmap/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,18 @@ class RoadmapConfig:
model: str = "sonnet" # Changed from "opus" (fix #433)
refresh: bool = False # Force regeneration even if roadmap exists
enable_competitor_analysis: bool = False # Enable competitor analysis phase


@dataclass
class RoadmapFeature:
"""A feature in the roadmap with dependencies."""

id: str
title: str
description: str
dependencies: list[str] # List of feature IDs this feature depends on
status: str # e.g., "planned", "in_progress", "completed"
reverse_dependencies: list[str] | None = (
None # List of feature IDs that depend on this feature
)
dependency_validation: dict | None = None # Validation metadata for dependencies
125 changes: 124 additions & 1 deletion apps/backend/runners/roadmap/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,24 @@
from pathlib import Path

from client import create_client
from debug import debug, debug_error, debug_section, debug_success
from debug import (
debug,
debug_detailed,
debug_error,
debug_section,
debug_success,
debug_warning,
)
from init import init_auto_claude_dir
from phase_config import get_thinking_budget
from ui import Icons, box, icon, muted, print_section, print_status

from .competitor_analyzer import CompetitorAnalyzer
from .executor import AgentExecutor, ScriptExecutor
from .graph_integration import GraphHintsProvider
from .models import RoadmapFeature
from .phases import DiscoveryPhase, FeaturesPhase, ProjectIndexPhase
from .validators import DependencyValidator


class RoadmapOrchestrator:
Expand Down Expand Up @@ -188,10 +197,124 @@ async def run(self) -> bool:
return False
debug_success("roadmap_orchestrator", "Phase 3 complete")

# Enrich features with dependency validation
debug(
"roadmap_orchestrator",
"Enriching features with dependency validation",
)
self._enrich_roadmap_features()
debug_success("roadmap_orchestrator", "Feature enrichment complete")

# Summary
self._print_summary()
return True

def _enrich_roadmap_features(self):
"""Enrich features with dependency validation and reverse dependencies."""
roadmap_file = self.output_dir / "roadmap.json"
if not roadmap_file.exists():
debug_warning(
"roadmap_orchestrator", "Roadmap file not found for enrichment"
)
return None

try:
with open(roadmap_file) as f:
roadmap_data = json.load(f)

features_data = roadmap_data.get("features", [])
if not features_data:
debug_warning("roadmap_orchestrator", "No features found in roadmap")
return None

# Convert dict features to RoadmapFeature objects
features = []
for feat_dict in features_data:
feature = RoadmapFeature(
id=feat_dict.get("id", ""),
title=feat_dict.get("title", ""),
description=feat_dict.get("description", ""),
dependencies=feat_dict.get("dependencies", []),
status=feat_dict.get("status", "planned"),
)
features.append(feature)

# Run validator
validator = DependencyValidator()
validation_result = validator.validate_all(features)

debug_detailed(
"roadmap_orchestrator",
"Validation results",
has_missing=validation_result.has_missing,
has_circular=validation_result.has_circular,
missing_ids=validation_result.missing_ids,
circular_paths_count=len(validation_result.circular_paths),
)

# Pre-compute all dependent IDs for efficient lookup
all_dependent_ids = {dep for f in features for dep in f.dependencies}

# Create a mapping of feature IDs to feature data for O(1) lookup
# This avoids O(N^2) complexity from repeated linear searches
features_by_id = {f.get("id"): f for f in features_data}

# Enrich each feature
enriched_features = []
for feature in features:
# Find the original feature dict using O(1) lookup
feat_dict = features_by_id.get(feature.id, {})

# Add reverse dependencies (snake_case for JSON, IPC handlers convert to camelCase)
feat_dict["reverse_dependencies"] = (
validation_result.reverse_deps_map.get(feature.id, [])
)

# Add validation metadata for features with dependencies
if feature.id in all_dependent_ids or len(feature.dependencies) > 0:
feat_dict["dependency_validation"] = {
"has_missing": validation_result.has_missing,
"has_circular": validation_result.has_circular,
"missing_ids": [
mid
for mid in validation_result.missing_ids
if mid in feature.dependencies
],
"circular_paths": [
cp
for cp in validation_result.circular_paths
if feature.id in cp
],
}

enriched_features.append(feat_dict)

# Update roadmap with enriched features
roadmap_data["features"] = enriched_features

# Write back to file
with open(roadmap_file, "w") as f:
json.dump(roadmap_data, f, indent=2)

debug_success(
"roadmap_orchestrator",
"Enriched roadmap features",
features_count=len(enriched_features),
)
return True

except Exception as e:
# Log enrichment failure - surface error to user rather than hiding it
# Enrichment adds reverse dependencies and validation metadata that users expect
debug_error(
"roadmap_orchestrator",
"Failed to enrich roadmap features",
error=str(e),
)
print_status("Feature enrichment failed", "error")
print(f" {muted('Error:')} {e}")
return False

def _print_summary(self):
"""Print the final roadmap generation summary."""
roadmap_file = self.output_dir / "roadmap.json"
Expand Down
149 changes: 149 additions & 0 deletions apps/backend/runners/roadmap/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""
Dependency validators for roadmap features.
"""

from dataclasses import dataclass

# Import RoadmapFeature based on execution context
# When run as module: use relative import
# When run as script: use absolute import
if __package__:
from .models import RoadmapFeature
else:
from runners.roadmap.models import RoadmapFeature


@dataclass
class ValidationResult:
"""Result of dependency validation."""

has_missing: bool
has_circular: bool
missing_ids: list[str]
circular_paths: list[list[str]]
reverse_deps_map: dict[str, list[str]]


class DependencyValidator:
"""Validates and enriches feature dependencies."""

def validate_all(self, features: list[RoadmapFeature]) -> ValidationResult:
"""
Validates all dependencies in the roadmap.

Args:
features: List of features to validate

Returns:
ValidationResult with validation metadata
"""
# Find missing dependencies
missing_ids = self._find_missing_deps(features)

# Detect circular dependencies
circular_paths = self._detect_circular_deps(features)

# Calculate reverse dependencies
reverse_deps_map = self._calculate_reverse_deps(features)

return ValidationResult(
has_missing=len(missing_ids) > 0,
has_circular=len(circular_paths) > 0,
missing_ids=missing_ids,
circular_paths=circular_paths,
reverse_deps_map=reverse_deps_map,
)

def _find_missing_deps(self, features: list[RoadmapFeature]) -> list[str]:
"""Find dependencies that reference non-existent features."""
valid_ids = {f.id for f in features}
missing = set()

for feature in features:
for dep_id in feature.dependencies:
if dep_id not in valid_ids:
missing.add(dep_id)

return sorted(missing)

def _detect_circular_deps(self, features: list[RoadmapFeature]) -> list[list[str]]:
"""
Detect circular dependencies using three-color DFS.

Uses WHITE (0), GRAY (1), BLACK (2) marking:
- WHITE: Not yet visited
- GRAY: Currently being processed (in current path)
- BLACK: Fully processed

Time complexity: O(V + E) where V = features, E = dependencies
"""
graph = {f.id: f.dependencies for f in features}
circular_paths = []
seen_cycles = set() # Track normalized cycles for deduplication

# Three-color DFS: WHITE=0, GRAY=1, BLACK=2
WHITE, GRAY, BLACK = 0, 1, 2
color = {f.id: WHITE for f in features}

def normalize_cycle(cycle: list[str]) -> str:
"""Rotate cycle to start from smallest ID for deduplication."""
if not cycle or len(cycle) < 2:
return ""
# Remove last element (duplicate of first)
cycle_without_dup = cycle[:-1]
min_idx = cycle_without_dup.index(min(cycle_without_dup))
# Rotate to start from minimal element
rotated = cycle_without_dup[min_idx:] + cycle_without_dup[:min_idx]
return ",".join(rotated)

def dfs(node: str, path: list[str]) -> None:
"""DFS with backtracking - O(V + E) complexity."""
color[node] = GRAY
path.append(node)

for neighbor in graph.get(node, []):
if neighbor not in graph:
continue # Skip non-existent nodes

if color[neighbor] == GRAY:
# Found a cycle - neighbor is in current path
cycle_start = path.index(neighbor)
cycle = path[cycle_start:] + [neighbor]
# Normalize and deduplicate
normalized = normalize_cycle(cycle)
if normalized not in seen_cycles:
seen_cycles.add(normalized)
circular_paths.append(cycle)
elif color[neighbor] == WHITE:
# Recurse into unvisited nodes
dfs(neighbor, path)

# Backtrack
path.pop()
color[node] = BLACK

# Run DFS from each unvisited node
for feature_id in graph:
if color[feature_id] == WHITE:
dfs(feature_id, [])

return circular_paths

def _calculate_reverse_deps(
self, features: list[RoadmapFeature]
) -> dict[str, list[str]]:
"""Calculate which features depend on each feature."""
reverse_deps: dict[str, list[str]] = {}

# Initialize all features with empty list
for feature in features:
reverse_deps[feature.id] = []

# Build reverse dependency map
for feature in features:
for dep_id in feature.dependencies:
if dep_id not in reverse_deps:
reverse_deps[dep_id] = []
reverse_deps[dep_id].append(feature.id)

return reverse_deps
2 changes: 1 addition & 1 deletion apps/backend/runners/roadmap_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from debug import debug, debug_error, debug_warning

# Import from refactored roadmap package
from roadmap import RoadmapOrchestrator
from runners.roadmap import RoadmapOrchestrator


def main():
Expand Down
10 changes: 10 additions & 0 deletions apps/frontend/src/main/ipc-handlers/roadmap-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function registerRoadmapHandlers(
async (_, projectId: string): Promise<IPCResult<Roadmap | null>> => {
const project = projectStore.getProject(projectId);
if (!project) {
debugLog('[Roadmap GET] Project not found:', projectId);
return { success: false, error: 'Project not found' };
}

Expand Down Expand Up @@ -170,6 +171,13 @@ export function registerRoadmapHandlers(
impact: feature.impact || 'medium',
phaseId: feature.phase_id,
dependencies: feature.dependencies || [],
reverseDependencies: (feature.reverse_dependencies as string[]) || undefined,
dependencyValidation: feature.dependency_validation as {
hasMissing: boolean;
hasCircular: boolean;
missingIds: string[];
circularPaths: string[][];
} | undefined,
status: feature.status || 'under_review',
acceptanceCriteria: feature.acceptance_criteria || [],
userStories: feature.user_stories || [],
Expand Down Expand Up @@ -377,6 +385,8 @@ export function registerRoadmapHandlers(
impact: feature.impact,
phase_id: feature.phaseId,
dependencies: feature.dependencies || [],
reverse_dependencies: feature.reverseDependencies || [],
dependency_validation: feature.dependencyValidation || undefined,
status: feature.status,
acceptance_criteria: feature.acceptanceCriteria || [],
user_stories: feature.userStories || [],
Expand Down
Loading
Loading