From b67780007c2e0ecfc1fb02d933c16488f675c442 Mon Sep 17 00:00:00 2001 From: openclaw Date: Tue, 24 Mar 2026 07:24:09 +0000 Subject: [PATCH] docs: add v2 gap fix design spec for 7 gaps (P0+P1) Covers GAP-B1 (status mapping), GAP-B3 (compound graph with document nodes), GAP-F1 (group-partitioned layout), GAP-F2 (rich detail panel), GAP-F3 (legend), GAP-F4 (back button), GAP-F5 (edit shortcut). Co-Authored-By: Claude Opus 4.6 --- .../specs/2026-03-24-v2-fix-gaps-design.md | 430 ++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-24-v2-fix-gaps-design.md diff --git a/docs/superpowers/specs/2026-03-24-v2-fix-gaps-design.md b/docs/superpowers/specs/2026-03-24-v2-fix-gaps-design.md new file mode 100644 index 0000000..e09f895 --- /dev/null +++ b/docs/superpowers/specs/2026-03-24-v2-fix-gaps-design.md @@ -0,0 +1,430 @@ +# V2 Gap Fix Design — 7 Gaps (P0+P1) + +Date: 2026-03-24 +Branch: `feat/v2-fix-gaps` +Status: Design + +## 1. Problem Statement + +The current graph visualization has three critical issues: +1. All 63 nodes share a single `forceCenter`, producing a "hairball" layout with no group separation +2. All node statuses are hardcoded to `"unknown"` — node colors are meaningless grey +3. DesignDocument upstream/downstream edges are dead code (file paths vs node IDs mismatch) + +Additionally, the detail panel is nearly empty, there's no way to return from drill-down, no edit shortcut, and no legend. + +## 2. Design Decisions + +### DD-1: Architecture Layer Layout (方案 A) +Y-axis represents abstraction level, matching TOGAF/ArchiMate conventions: + +``` +group_id targetX(ratio) targetY(ratio) +business 0.50 0.15 +application 0.50 0.38 +data 0.30 0.65 +technology 0.70 0.65 +cross-layer 0.50 0.85 +``` + +### DD-2: Compound Graph for Document View +- Document nodes are containers; entity nodes nest inside their parent document +- Toggle between default (group-partitioned) and document view modes +- Both doc→doc edges and entity→entity edges coexist + +### DD-3: Status Mapping via source_file +- Entity type → source CSV file → FileStatus from ScanResult +- Same-CSV entities share that file's status (one CSV = one entity type's completeness) + +## 3. Gap Specifications + +--- + +### 3.1 GAP-B1: Node Status Mapping + +**Layer:** Backend +**Priority:** P0 +**File:** `backend/app/modules/graph/application/services.py` + +**Current behavior:** `GraphService.build_panorama()` hardcodes `status="unknown"` for all nodes. + +**Target behavior:** Each node gets its status from the Scanner's `FileStatus` for the source CSV file that contains that entity type. + +**Implementation:** + +1. Build `file_status_map: dict[str, str]` from `scan_result.file_statuses`: + ```python + file_status_map = {fs.path: fs.status.value for fs in scan_result.file_statuses} + ``` + +2. Define entity-type-to-source-file mapping (from `design/data-architecture/01-entities.csv`): + ```python + _SOURCE_FILES = { + "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", + } + ``` + +3. When creating each node: + ```python + status = file_status_map.get(_SOURCE_FILES.get(node_type, ""), "unknown") + ``` + +**Acceptance criteria:** +- Nodes have real status values (ok/sparse/missing/template-residue/placeholder-heavy) instead of "unknown" +- Nodes without a known source file retain "unknown" + +--- + +### 3.2 GAP-B3: DesignDocument Edges — Compound Graph + +**Layer:** Backend +**Priority:** P0 +**File:** `backend/app/modules/graph/domain/entities.py`, `backend/app/modules/graph/application/services.py` + +**Current behavior:** Step 9 in `build_panorama()` tries to match `doc.upstream`/`doc.downstream` (file paths like `./02-capability-map.csv`) against node IDs. Always fails because (a) paths ≠ IDs and (b) DesignDocuments are never added as nodes. + +**Target behavior:** +1. DesignDocument objects become graph nodes (type="document", group="cross-layer") +2. Entity nodes get a `parent` field pointing to their containing document's `doc_id` +3. Two edge types: doc→doc (relation="documents") and entity→entity (existing relations unchanged) + +**Domain model change — GraphNode:** +```python +@dataclass +class GraphNode: + id: str + type: str # capability | module | entity | runtime_component | document + label: str + status: str + group_id: str + parent: str | None = None # doc_id of containing document, if any +``` + +**Implementation in build_panorama():** + +Step 5.5 — Create document nodes: +```python +file_to_doc: dict[str, str] = {} +for doc in scan_result.design_documents: + file_to_doc[doc.source_file] = 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.source_file, "unknown"), + group_id="cross-layer", + )) + node_ids.add(doc.doc_id) +``` + +Entity nodes get parent: +```python +parent_doc_id = file_to_doc.get(_SOURCE_FILES.get(node_type)) +node = GraphNode(..., parent=parent_doc_id) +``` + +Step 9 — Fix doc→doc edges using file path mapping: +```python +path_to_doc = {doc.source_file: doc.doc_id for doc in scan_result.design_documents} +for doc in scan_result.design_documents: + for up_path in doc.upstream: + normalized = up_path.lstrip("./") + up_doc_id = path_to_doc.get(normalized) + if up_doc_id and up_doc_id in node_ids: + edges.append(GraphEdge(source=up_doc_id, target=doc.doc_id, relation="documents")) + for down_path in doc.downstream: + normalized = down_path.lstrip("./") + down_doc_id = path_to_doc.get(normalized) + if down_doc_id and down_doc_id in node_ids: + edges.append(GraphEdge(source=doc.doc_id, target=down_doc_id, relation="documents")) +``` + +**Acceptance criteria:** +- Document nodes appear in the graph (type="document", group="cross-layer") +- Entity nodes have correct `parent` references +- doc→doc edges are created based on upstream/downstream file path resolution +- API response includes `parent` field (null for nodes without a parent) + +--- + +### 3.3 GAP-F1: Group-Partitioned Layout + Compound Layout + +**Layer:** Frontend +**Priority:** P0 +**File:** `frontend/src/modules/graph/components/GraphPanorama.vue` + +**Current behavior:** Single `d3.forceCenter()` for all nodes — all groups overlap. + +**Target behavior:** Two layout modes controlled by a toggle. + +#### Default Mode (Document View OFF) + +- Filter out nodes where `type === "document"` and edges where `relation === "documents"` +- Use `d3.forceX` / `d3.forceY` per group with strength ~0.3-0.5: + ```js + const groupPositions = { + business: { x: 0.50, y: 0.15 }, + application: { x: 0.50, y: 0.38 }, + data: { x: 0.30, y: 0.65 }, + technology: { x: 0.70, y: 0.65 }, + 'cross-layer': { x: 0.50, y: 0.85 }, + } + + simulation + .force('x', d3.forceX(d => groupPositions[d.group_id].x * width).strength(0.4)) + .force('y', d3.forceY(d => groupPositions[d.group_id].y * height).strength(0.4)) + .force('collide', d3.forceCollide(30)) + .force('link', d3.forceLink(edges).id(d => d.id).distance(60)) + ``` +- Remove the single `forceCenter` — forceX/forceY provide the centering per group + +#### Document View Mode (Toggle ON) + +- Show all nodes including documents +- Document nodes rendered as large `` containers, sized by child count: + ```js + const childCount = nodes.filter(n => n.parent === doc.id).length + doc.width = Math.max(150, childCount * 60) + doc.height = Math.max(100, childCount * 40) + ``` +- Document containers participate in force simulation (with high mass to resist movement) +- Entity nodes with `parent` are constrained inside their parent's bounds on each tick: + ```js + simulation.on('tick', () => { + nodes.forEach(n => { + if (n.parent) { + const p = nodeMap[n.parent] + const pad = 20 + n.x = clamp(n.x, p.x - p.width/2 + pad, p.x + p.width/2 - pad) + n.y = clamp(n.y, p.y - p.height/2 + pad, p.y + p.height/2 - pad) + } + }) + }) + ``` +- doc→doc edges rendered between container borders +- entity→entity edges rendered normally (may cross container boundaries) + +#### Toggle + +- State: `showDocumentView: ref(false)` +- UI: Toggle button in toolbar area +- Switching destroys current simulation and reinitializes with the appropriate mode + +#### Node Rendering + +Shapes (both modes): +| type | shape | size | +|------|-------|------| +| capability | circle | r=18 | +| module | rect | 28×28 | +| entity | diamond (rotated rect) | 24×24 | +| runtime_component | circle | r=14 | +| document | large rect container | dynamic | + +Colors (by status): +| status | color | +|--------|-------| +| ok | #4CAF50 | +| sparse | #FFC107 | +| missing | #F44336 | +| template-residue | #FF9800 | +| placeholder-heavy | #9C27B0 | +| unknown | #9E9E9E | + +Edge styles: +| relation | style | +|----------|-------| +| traces_to | solid, #666 | +| depends_on | dashed, #999 | +| documents | solid, #42A5F5 (blue) | +| integrates_with | dotted, #AB47BC | + +**Acceptance criteria:** +- Default mode: nodes cluster by group in 5 distinct regions, no "hairball" +- Document view: documents render as containers with entities nested inside +- Toggle smoothly switches between modes +- Nodes have correct shapes and colors + +--- + +### 3.4 GAP-F2: Rich GraphDetail Panel + +**Layer:** Frontend +**Priority:** P1 +**File:** `frontend/src/modules/graph/components/GraphDetail.vue` + +**Current behavior:** Shows only `id, type, status, group_id`. + +**Target behavior:** Full attribute display + related entity list, fetched from detail APIs. + +**API calls by node type:** +| node.type | endpoint | response type | +|-----------|----------|---------------| +| capability | `GET /entities/capabilities/{id}` | CapabilityDetail | +| module | `GET /entities/modules/{id}` | ModuleDetail | +| entity | `GET /entities/entities/{id}` | EntityDetail | +| document | no API call, use node properties | — | +| runtime_component | no API call, use node properties | — | + +**Panel layout:** +``` +┌─ GraphDetail ──────────────────────────┐ +│ ✕ Close │ +│ │ +│ ● CAP-001 [Edit] │ +│ capability · business │ +│ │ +│ ── Attributes ── │ +│ name: 项目管理 │ +│ description: 管理项目生命周期... │ +│ owner: ... │ +│ │ +│ ── Related Entities ── │ +│ → MOD-001 项目管理模块 (traces_to) │ +│ → MOD-003 扫描服务 (traces_to) │ +│ ← DOC-005 scope文档 (documents) │ +│ │ +└────────────────────────────────────────┘ +``` + +**Behavior:** +- On node selection: if type has detail API, fetch it (show spinner while loading) +- Error fallback: show basic 4 fields +- Clicking a related entity → updates selected node → panel refreshes +- Related entities are derived from `graphView.edges` where source or target matches the selected node + +**Acceptance criteria:** +- Detail API is called for capability/module/entity nodes +- All attributes from the API response are displayed +- Related entities list is clickable and navigates the graph +- Graceful fallback on API error + +--- + +### 3.5 GAP-F5: Edit Button in GraphDetail + +**Layer:** Frontend +**Priority:** P1 +**File:** `frontend/src/modules/graph/components/GraphDetail.vue` + +**Current behavior:** No way to jump from graph node to editor. + +**Target behavior:** "Edit" button in the detail panel header, navigating to the editor page. + +**Implementation:** +- Button visible only when `source_file` is available: + - From detail API response (CapabilityDetail, ModuleDetail, EntityDetail have this field) + - For document nodes: use node properties + - For runtime_component: use static `_SOURCE_FILES` mapping +- Click action: + ```js + router.push({ + path: `/projects/${projectId}/editor`, + query: { file: sourceFilePath } + }) + ``` + +**Acceptance criteria:** +- Edit button appears for nodes with known source files +- Clicking navigates to editor with correct file pre-selected +- Button hidden for nodes without source file info + +--- + +### 3.6 GAP-F4: Back to Panorama Button + +**Layer:** Frontend +**Priority:** P1 +**File:** `frontend/src/modules/graph/components/GraphPanorama.vue` + +**Current behavior:** Double-click drills down to neighbor subgraph; no way to return. + +**Target behavior:** Floating "back" button in drill-down mode. + +**Implementation:** +- State: `isDrillDown: ref(false)`, `drillDownNodeLabel: ref('')` +- On double-click drill-down: set `isDrillDown = true`, store the node label +- Render (conditionally): + ```html +
+ + 当前: {{ drillDownNodeLabel }} +
+ ``` +- `returnToPanorama()`: calls existing `loadGraph()` method, sets `isDrillDown = false` + +**Acceptance criteria:** +- Button appears only in drill-down mode +- Shows which node was drilled into +- Clicking restores the full panorama graph + +--- + +### 3.7 GAP-F3: Graph Legend + +**Layer:** Frontend +**Priority:** P1 +**File:** New `frontend/src/modules/graph/components/GraphLegend.vue`, imported in `GraphPanorama.vue` + +**Design:** +``` +┌─ Legend ────────────────────┐ +│ Shapes Status │ +│ ○ Capability ● OK │ +│ ■ Module ● Sparse │ +│ ◇ Entity ● Missing │ +│ ○ Runtime Comp ● Template│ +│ ▭ Document ● Placeholder │ +│ ● Unknown │ +│ Edges │ +│ ── traces_to │ +│ -- depends_on │ +│ ━━ documents │ +│ ·· integrates_with │ +└────────────────────────────┘ +``` + +**Implementation:** +- Positioned bottom-right of the graph canvas, `position: absolute` +- Semi-transparent background (`rgba(255,255,255,0.9)`) +- Collapsible: click title to toggle expand/collapse +- Default: expanded +- Uses actual SVG shapes/colors matching the graph rendering + +**Acceptance criteria:** +- Legend renders in bottom-right corner +- Shows all node shapes, status colors, and edge styles +- Collapsible +- Does not block graph interaction (pointer-events on legend only) + +--- + +## 4. Files Changed + +| File | Change Type | Gaps | +|------|-------------|------| +| `backend/app/modules/graph/domain/entities.py` | Modify | B3 | +| `backend/app/modules/graph/application/services.py` | Modify | B1, B3 | +| `frontend/src/modules/graph/components/GraphPanorama.vue` | Modify | F1, F4 | +| `frontend/src/modules/graph/components/GraphDetail.vue` | Modify | F2, F5 | +| `frontend/src/modules/graph/components/GraphLegend.vue` | New | F3 | + +## 5. Out of Scope + +The following gaps from the full analysis are NOT addressed in this spec: +- GAP-X1 (layered drill-down) — Phase 2 +- GAP-X2 (implementation progress on nodes) — Phase 2 +- GAP-B2 (more entity types as nodes) — separate effort +- GAP-B4 (recursive impact traversal) — separate effort +- GAP-B5 (LLM-based impl tracking) — separate effort +- GAP-F6 (route path alignment) — cosmetic +- GAP-F7 (panel position conflict) — cosmetic + +## 6. Constraints + +1. Do not modify files under `design/` — those are the source of truth +2. All changes must pass `vue-tsc` (frontend) and Python type checking (backend) +3. Implementation order follows DDD layers: Domain → Infrastructure → Application → Interfaces