From ac5b7bccc76ec687fb6ba37879bffea40e6fd30b Mon Sep 17 00:00:00 2001 From: openclaw Date: Tue, 24 Mar 2026 08:01:59 +0000 Subject: [PATCH] docs: add v2 gap fix implementation plan (10 tasks) Co-Authored-By: Claude Opus 4.6 --- .../plans/2026-03-24-v2-fix-gaps.md | 847 ++++++++++++++++++ 1 file changed, 847 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-24-v2-fix-gaps.md diff --git a/docs/superpowers/plans/2026-03-24-v2-fix-gaps.md b/docs/superpowers/plans/2026-03-24-v2-fix-gaps.md new file mode 100644 index 0000000..6fd2c5a --- /dev/null +++ b/docs/superpowers/plans/2026-03-24-v2-fix-gaps.md @@ -0,0 +1,847 @@ +# V2 Gap Fix Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix 7 gaps (P0+P1) so the graph visualization shows grouped layout, real status colors, working document edges, rich detail panel, legend, back button, and edit shortcut. + +**Architecture:** Backend changes add `parent` field to `GraphNode`, status mapping from `FileStatusEntry`, and document nodes with proper edge resolution. Frontend changes replace the single-center D3 layout with per-group forceX/forceY, add compound document view toggle, enrich the detail panel with API calls, and add legend/back-button UI. + +**Tech Stack:** Python 3.12 / FastAPI / dataclasses (backend), Vue 3 / TypeScript / D3.js v7 / Pinia (frontend), pytest (backend tests) + +**Spec:** `docs/superpowers/specs/2026-03-24-v2-fix-gaps-design.md` + +--- + +## File Structure + +### Files to Modify +| File | Responsibility | Tasks | +|------|---------------|-------| +| `backend/app/modules/graph/domain/entities.py` | GraphNode dataclass — add `parent` field | 1 | +| `backend/app/modules/graph/application/services.py` | build_panorama — status mapping, document nodes, edge fix | 2, 3 | +| `backend/app/modules/graph/interfaces/http/router.py` | Pass `design_dir` to build_panorama | 4 | +| `backend/tests/test_graph_service.py` | Update existing tests + add new tests | 1, 2, 3, 4 | +| `frontend/src/shared/types/api.ts` | Add `parent` to GraphNode interface | 5 | +| `frontend/src/modules/graph/components/GraphPanorama.vue` | Group layout, compound layout, toggle, back button | 6, 8 | +| `frontend/src/modules/graph/components/GraphDetail.vue` | Rich detail panel, edit button | 7 | + +### Files to Create +| File | Responsibility | Tasks | +|------|---------------|-------| +| `frontend/src/modules/graph/components/GraphLegend.vue` | Legend component | 9 | + +--- + +### Task 1: Domain — Add `parent` field to GraphNode + +**Files:** +- Modify: `backend/app/modules/graph/domain/entities.py:4-10` +- Test: `backend/tests/test_graph_service.py` + +- [ ] **Step 1: Write the failing test** + +Add to `backend/tests/test_graph_service.py`: + +```python +def test_graph_node_has_parent_field(graph_service, scan_result): + view = graph_service.build_panorama(scan_result) + # All nodes should have a parent attribute (None for most, doc_id for some) + for node in view.nodes: + assert hasattr(node, 'parent') +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && python -m pytest tests/test_graph_service.py::test_graph_node_has_parent_field -v` +Expected: FAIL — `GraphNode` has no `parent` attribute + +- [ ] **Step 3: Add `parent` field to GraphNode** + +Edit `backend/app/modules/graph/domain/entities.py` — add to the GraphNode dataclass: + +```python +@dataclass +class GraphNode: + id: str + type: str # capability, module, entity, runtime_component, document + label: str + status: str # FileStatus value or "unknown" + group_id: str + parent: str | None = None # doc_id of containing document, if any +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && python -m pytest tests/test_graph_service.py::test_graph_node_has_parent_field -v` +Expected: PASS + +- [ ] **Step 5: Run all existing graph tests to verify no regression** + +Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && python -m pytest tests/test_graph_service.py -v` +Expected: All 9 tests PASS (existing 8 + new 1) + +- [ ] **Step 6: Commit** + +```bash +git add backend/app/modules/graph/domain/entities.py backend/tests/test_graph_service.py +git commit -m "feat(graph): add parent field to GraphNode domain entity" +``` + +--- + +### Task 2: Application — Status mapping in build_panorama (GAP-B1) + +**Files:** +- Modify: `backend/app/modules/graph/application/services.py:22-77` +- Test: `backend/tests/test_graph_service.py` + +- [ ] **Step 1: Add design_dir fixture and write the failing tests** + +First, add a `design_dir` fixture to `backend/tests/test_graph_service.py` (will be used by all subsequent tests): + +```python +@pytest.fixture +def design_dir(): + return "/workspace/arch-design-agent-skill-dashboard/design" +``` + +Then add the new tests (note: all new tests from this point forward take `design_dir` and pass it to `build_panorama`): + +```python +def test_panorama_nodes_have_real_status(graph_service, scan_result, design_dir): + view = graph_service.build_panorama(scan_result, design_dir=design_dir) + statuses = {n.status for n in view.nodes} + # At least some nodes should NOT be "unknown" since we have real file statuses + assert statuses != {"unknown"}, "All nodes still have status='unknown' — status mapping not working" + + +def test_panorama_status_values_are_valid(graph_service, scan_result, design_dir): + view = graph_service.build_panorama(scan_result, design_dir=design_dir) + valid_statuses = {"ok", "sparse", "missing", "template-residue", "placeholder-heavy", "unknown"} + for node in view.nodes: + assert node.status in valid_statuses, f"Node {node.id} has invalid status '{node.status}'" +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && python -m pytest tests/test_graph_service.py::test_panorama_nodes_have_real_status tests/test_graph_service.py::test_panorama_status_values_are_valid -v` +Expected: `test_panorama_nodes_have_real_status` FAILS (all statuses are "unknown") + +- [ ] **Step 3: Implement status mapping** + +Edit `backend/app/modules/graph/application/services.py`. Add the `_SOURCE_FILES` constant after `_GROUPS` and modify `build_panorama` to build the file status map: + +```python +_SOURCE_FILES: dict[str, str] = { + "capability": "business-architecture/02-capability-map.csv", + "module": "application-architecture/02-modules.csv", + "entity": "data-architecture/01-entities.csv", + "runtime_component": "technology-architecture/01-runtime-components.csv", +} +``` + +At the top of `build_panorama()`, add `design_dir` parameter with default so existing tests still work: +```python +def build_panorama(self, scan_result: ScanResult, design_dir: str = "") -> GraphView: + # Build file path -> status mapping + file_status_map: dict[str, str] = { + fs.path: fs.status.value for fs in scan_result.file_statuses + } +``` + +Replace each `status="unknown"` with: +```python +status=file_status_map.get(_SOURCE_FILES.get("capability", ""), "unknown"), # for caps +status=file_status_map.get(_SOURCE_FILES.get("module", ""), "unknown"), # for modules +status=file_status_map.get(_SOURCE_FILES.get("entity", ""), "unknown"), # for entities +status=file_status_map.get(_SOURCE_FILES.get("runtime_component", ""), "unknown"), # for runtime +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && python -m pytest tests/test_graph_service.py -v` +Expected: All tests PASS including the two new status tests + +- [ ] **Step 5: Commit** + +```bash +git add backend/app/modules/graph/application/services.py backend/tests/test_graph_service.py +git commit -m "feat(graph): map node status from FileStatus via source_file (GAP-B1)" +``` + +--- + +### Task 3: Application — Document nodes + doc→doc edges (GAP-B3) + +**Files:** +- Modify: `backend/app/modules/graph/application/services.py` +- Test: `backend/tests/test_graph_service.py` + +- [ ] **Step 1: Write the failing tests** + +Add to `backend/tests/test_graph_service.py`: + +```python +def test_panorama_has_document_nodes(graph_service, scan_result, design_dir): + view = graph_service.build_panorama(scan_result, design_dir=design_dir) + doc_nodes = [n for n in view.nodes if n.type == "document"] + assert len(doc_nodes) > 0, "No document nodes found" + assert all(n.group_id == "cross-layer" for n in doc_nodes) + + +def test_panorama_document_edges(graph_service, scan_result, design_dir): + view = graph_service.build_panorama(scan_result, design_dir=design_dir) + doc_edges = [e for e in view.edges if e.relation == "documents"] + assert len(doc_edges) > 0, "No document edges found" + + +def test_panorama_capability_nodes_have_parent(graph_service, scan_result, design_dir): + view = graph_service.build_panorama(scan_result, design_dir=design_dir) + cap_nodes = [n for n in view.nodes if n.type == "capability"] + # Capability nodes should have parent pointing to a document + nodes_with_parent = [n for n in cap_nodes if n.parent is not None] + assert len(nodes_with_parent) > 0, "No capability nodes have a parent document" +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && python -m pytest tests/test_graph_service.py::test_panorama_has_document_nodes tests/test_graph_service.py::test_panorama_document_edges tests/test_graph_service.py::test_panorama_capability_nodes_have_parent -v` +Expected: All 3 FAIL + +- [ ] **Step 3: Implement document nodes and edge resolution** + +Edit `backend/app/modules/graph/application/services.py`. + +Add path helper functions before the class: + +```python +from pathlib import PurePosixPath + + +def _to_rel_path(doc_file_path: str, design_dir: str) -> str: + """Convert absolute doc.file_path to design-dir-relative path.""" + try: + return str(PurePosixPath(doc_file_path).relative_to(design_dir)) + except ValueError: + return doc_file_path + + +def _resolve_ref_path(ref_path: str, doc_rel_path: str) -> str: + """Resolve a relative upstream/downstream ref against the doc's directory.""" + doc_dir = str(PurePosixPath(doc_rel_path).parent) + resolved = str(PurePosixPath(doc_dir) / ref_path) + parts: list[str] = [] + for part in PurePosixPath(resolved).parts: + if part == '..': + if parts: + parts.pop() + else: + parts.append(part) + return str(PurePosixPath(*parts)) if parts else "" +``` + +**Important ordering:** Build `file_to_doc` mapping BEFORE Steps 2-5 so entity nodes can get their `parent`. Restructure `build_panorama` to: + +1. Build `file_status_map` (already done in Task 2) +2. Build `file_to_doc` from `scan_result.design_documents` + create document nodes +3. Then create entity nodes (Steps 2-5) with `parent` set via `file_to_doc` +4. Then create edges (Steps 6-9) with fixed Step 9 + +Step 5.5 (now moved before Steps 2-5): +```python + # Build document node mapping first (needed for parent refs) + file_to_doc: dict[str, str] = {} + for doc in scan_result.design_documents: + doc_rel = _to_rel_path(doc.file_path, design_dir) + file_to_doc[doc_rel] = doc.doc_id + nodes.append(GraphNode( + id=doc.doc_id, + type="document", + label=doc.title or doc.doc_id, + status=file_status_map.get(doc_rel, "unknown"), + group_id="cross-layer", + )) + node_ids.add(doc.doc_id) +``` + +In each entity creation (Steps 2-5), add parent: +```python + # e.g. for capability: + parent_doc_id = file_to_doc.get(_SOURCE_FILES.get("capability")) + nodes.append(GraphNode( + id=node_id, type="capability", label=cap.name, + status=file_status_map.get(_SOURCE_FILES.get("capability", ""), "unknown"), + group_id="business", + parent=parent_doc_id, + )) +``` + +Replace Step 9 with path resolution + deduplication: +```python + # Step 9: DesignDocument.downstream → doc-to-doc edges (deduplicated) + path_to_doc: dict[str, str] = {} + doc_rel_paths: dict[str, str] = {} + for doc in scan_result.design_documents: + doc_rel = _to_rel_path(doc.file_path, design_dir) + path_to_doc[doc_rel] = doc.doc_id + doc_rel_paths[doc.doc_id] = doc_rel + + seen_edges: set[tuple[str, str]] = set() + for doc in scan_result.design_documents: + doc_rel = doc_rel_paths[doc.doc_id] + for down_path in doc.downstream: + resolved = _resolve_ref_path(down_path, doc_rel) + down_doc_id = path_to_doc.get(resolved) + if down_doc_id and down_doc_id in node_ids: + edge_key = (doc.doc_id, down_doc_id) + if edge_key not in seen_edges: + seen_edges.add(edge_key) + edges.append(GraphEdge( + source=doc.doc_id, target=down_doc_id, + relation="documents", + )) +``` + +- [ ] **Step 4: Run new tests to verify they pass** + +Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && python -m pytest tests/test_graph_service.py::test_panorama_has_document_nodes tests/test_graph_service.py::test_panorama_document_edges tests/test_graph_service.py::test_panorama_capability_nodes_have_parent -v` +Expected: All 3 PASS + +- [ ] **Step 5: Run ALL graph tests to check no regression** + +Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && python -m pytest tests/test_graph_service.py tests/test_api_graph.py -v` +Expected: All tests PASS + +- [ ] **Step 6: Commit** + +```bash +git add backend/app/modules/graph/application/services.py backend/tests/test_graph_service.py +git commit -m "feat(graph): add document nodes, parent refs, and fixed doc edges (GAP-B3)" +``` + +--- + +### Task 4: Interfaces — Pass design_dir from router to build_panorama + +**Files:** +- Modify: `backend/app/modules/graph/interfaces/http/router.py:40-56` +- Test: `backend/tests/test_api_graph.py` + +- [ ] **Step 1: Update the router to pass design_dir** + +Edit `backend/app/modules/graph/interfaces/http/router.py`: + +In `get_graph()`: +```python +@router.get("") +def get_graph(project_id: str): + """Build and return the full panorama graph for a project.""" + project = _project_service.get_project(project_id) + scan_result = _get_or_trigger_scan(project_id) + view = _graph_service.build_panorama(scan_result, design_dir=project.design_dir) + return asdict(view) +``` + +In `get_neighbors()`: +```python +@router.get("/nodes/{node_id}/neighbors") +def get_neighbors(project_id: str, node_id: str): + """Return the subgraph of neighbors for a given node.""" + project = _project_service.get_project(project_id) + scan_result = _get_or_trigger_scan(project_id) + view = _graph_service.build_panorama(scan_result, design_dir=project.design_dir) + neighbors = _graph_service.get_neighbors(view, node_id) + return asdict(neighbors) +``` + +- [ ] **Step 2: Update test_graph_service.py to pass design_dir** + +Add a `design_dir` fixture and update all `build_panorama` calls: + +```python +@pytest.fixture +def design_dir(): + return "/workspace/arch-design-agent-skill-dashboard/design" +``` + +Update all test function signatures to include `design_dir` parameter. Update all calls from: +```python +view = graph_service.build_panorama(scan_result) +``` +to: +```python +view = graph_service.build_panorama(scan_result, design_dir=design_dir) +``` + +- [ ] **Step 3: Run ALL backend tests** + +Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && python -m pytest tests/ -v` +Expected: All tests PASS + +- [ ] **Step 4: Commit** + +```bash +git add backend/app/modules/graph/interfaces/http/router.py backend/tests/test_graph_service.py +git commit -m "feat(graph): pass design_dir from router to build_panorama" +``` + +--- + +### Task 5: Frontend types — Add `parent` to GraphNode interface + +**Files:** +- Modify: `frontend/src/shared/types/api.ts:31-37` + +- [ ] **Step 1: Add parent field** + +Edit `frontend/src/shared/types/api.ts`: + +```typescript +export interface GraphNode { + id: string + type: string + label: string + status: string + group_id: string + parent: string | null +} +``` + +- [ ] **Step 2: Run type check** + +Run: `cd /workspace/arch-design-agent-skill-dashboard/frontend && npx vue-tsc --noEmit` +Expected: No errors + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/shared/types/api.ts +git commit -m "feat(graph): add parent field to GraphNode TypeScript interface" +``` + +--- + +### Task 6: Frontend — Group-partitioned layout + compound layout + toggle (GAP-F1) + +**Files:** +- Modify: `frontend/src/modules/graph/components/GraphPanorama.vue` + +This is the largest task. The entire `drawGraph()` function needs rewriting. + +- [ ] **Step 1: Add state variables and update constants** + +Add after existing refs in ` + + +``` + +- [ ] **Step 2: Import GraphLegend in GraphPanorama.vue** + +Add import: +```typescript +import GraphLegend from './GraphLegend.vue' +``` + +Add to template (inside `.graph-panorama` div, after ``): +```html + +``` + +- [ ] **Step 3: Run type check** + +Run: `cd /workspace/arch-design-agent-skill-dashboard/frontend && npx vue-tsc --noEmit` +Expected: No errors + +- [ ] **Step 4: Commit** + +```bash +git add frontend/src/modules/graph/components/GraphLegend.vue frontend/src/modules/graph/components/GraphPanorama.vue +git commit -m "feat(graph): add collapsible legend component (GAP-F3)" +``` + +--- + +### Task 10: Final verification + +- [ ] **Step 1: Run all backend tests** + +Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && python -m pytest tests/ -v` +Expected: All tests PASS + +- [ ] **Step 2: Run frontend type check** + +Run: `cd /workspace/arch-design-agent-skill-dashboard/frontend && npx vue-tsc --noEmit` +Expected: No errors + +- [ ] **Step 3: Run frontend build** + +Run: `cd /workspace/arch-design-agent-skill-dashboard/frontend && npm run build` +Expected: Build succeeds + +- [ ] **Step 4: Verify git status is clean** + +Run: `git status` +Expected: No uncommitted changes