arch-design-agent-skill-das.../docs/superpowers/specs/2026-03-24-v2-fix-gaps-design.md
openclaw b67780007c 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 <noreply@anthropic.com>
2026-03-24 07:24:09 +00:00

431 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 `<rect>` 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
<div v-if="isDrillDown" class="drill-down-bar">
<button @click="returnToPanorama">← 返回全景图</button>
<span>当前: {{ drillDownNodeLabel }}</span>
</div>
```
- `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