docs: fix spec issues from review — path normalization, field names, dedup
- Fix doc.source_file → doc.file_path (actual DesignDocument field name)
- Replace lstrip("./") with proper path resolution helpers
- Add deduplication for doc→doc edges (process only downstream)
- Add frontend/src/shared/types/api.ts to files changed
- Add fallback for unknown group_id in forceX/forceY
- Note build_panorama needs design_dir parameter
- Clarify runtime_component/document detail panel behavior
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b67780007c
commit
c97f201a9c
|
|
@ -52,6 +52,8 @@ cross-layer 0.50 0.85
|
|||
|
||||
**Implementation:**
|
||||
|
||||
**Note:** `build_panorama()` will need an additional `design_dir: str` parameter to convert `DesignDocument.file_path` (absolute) to design-dir-relative paths. This parameter is passed from the route handler which already has access to the project's design directory.
|
||||
|
||||
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}
|
||||
|
|
@ -105,16 +107,45 @@ class GraphNode:
|
|||
|
||||
**Implementation in build_panorama():**
|
||||
|
||||
**Path normalization note:** `DesignDocument.file_path` is an **absolute** path (e.g., `/home/user/project/design/business-architecture/01-scope-and-goals.md`), while `FileStatusEntry.path` is **relative** to the design directory (e.g., `business-architecture/01-scope-and-goals.md`). The `upstream`/`downstream` fields in frontmatter are relative paths (e.g., `../business-architecture/01-scope-and-goals.md`). All paths must be normalized to design-dir-relative format before comparison.
|
||||
|
||||
Helper function:
|
||||
```python
|
||||
def _to_rel_path(doc_file_path: str, design_dir: str) -> str:
|
||||
"""Convert absolute doc.file_path to design-dir-relative path."""
|
||||
from pathlib import PurePosixPath
|
||||
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."""
|
||||
from pathlib import PurePosixPath
|
||||
doc_dir = str(PurePosixPath(doc_rel_path).parent)
|
||||
resolved = str(PurePosixPath(doc_dir) / ref_path)
|
||||
# Normalize away ../ segments
|
||||
parts = []
|
||||
for part in PurePosixPath(resolved).parts:
|
||||
if part == '..':
|
||||
if parts:
|
||||
parts.pop()
|
||||
else:
|
||||
parts.append(part)
|
||||
return str(PurePosixPath(*parts)) if parts else ""
|
||||
```
|
||||
|
||||
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
|
||||
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.source_file, "unknown"),
|
||||
status=file_status_map.get(doc_rel, "unknown"),
|
||||
group_id="cross-layer",
|
||||
))
|
||||
node_ids.add(doc.doc_id)
|
||||
|
|
@ -126,20 +157,27 @@ 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:
|
||||
Step 9 — Fix doc→doc edges using file path mapping (with deduplication):
|
||||
```python
|
||||
path_to_doc = {doc.source_file: doc.doc_id for doc in scan_result.design_documents}
|
||||
path_to_doc = {}
|
||||
doc_rel_paths = {}
|
||||
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"))
|
||||
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:
|
||||
normalized = down_path.lstrip("./")
|
||||
down_doc_id = path_to_doc.get(normalized)
|
||||
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"))
|
||||
# Only process downstream to avoid duplicates (A.downstream=B ↔ B.upstream=A)
|
||||
```
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
|
@ -174,8 +212,8 @@ for doc in scan_result.design_documents:
|
|||
}
|
||||
|
||||
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('x', d3.forceX(d => (groupPositions[d.group_id] || groupPositions['cross-layer']).x * width).strength(0.4))
|
||||
.force('y', d3.forceY(d => (groupPositions[d.group_id] || groupPositions['cross-layer']).y * height).strength(0.4))
|
||||
.force('collide', d3.forceCollide(30))
|
||||
.force('link', d3.forceLink(edges).id(d => d.id).distance(60))
|
||||
```
|
||||
|
|
@ -269,6 +307,8 @@ Edge styles:
|
|||
| document | no API call, use node properties | — |
|
||||
| runtime_component | no API call, use node properties | — |
|
||||
|
||||
**Note on non-API types:** For `document` and `runtime_component` nodes that have no detail API, the panel displays the basic `GraphNode` fields (`id`, `type`, `label`, `status`, `group_id`, `parent`). This is acceptable — these types have limited attributes. A detail API for these types can be added later if needed.
|
||||
|
||||
**Panel layout:**
|
||||
```
|
||||
┌─ GraphDetail ──────────────────────────┐
|
||||
|
|
@ -411,6 +451,7 @@ Edge styles:
|
|||
| `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 |
|
||||
| `frontend/src/shared/types/api.ts` | Modify | B3 (add `parent` field to `GraphNode` interface) |
|
||||
|
||||
## 5. Out of Scope
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user