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>
This commit is contained in:
parent
1a92c91591
commit
b67780007c
430
docs/superpowers/specs/2026-03-24-v2-fix-gaps-design.md
Normal file
430
docs/superpowers/specs/2026-03-24-v2-fix-gaps-design.md
Normal file
|
|
@ -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 `<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
|
||||
Loading…
Reference in New Issue
Block a user