arch-design-agent-skill-das.../docs/superpowers/plans/2026-03-23-full-implementation.md
openclaw 2d97a3333c docs: add full implementation plan — 32 tasks, TDD approach
Covers all 6 backend modules (design, project, scanner, graph, editor,
impl_tracker), 3 frontend modules (project, graph, editor), build config,
and Docker deployment. Each backend module follows Domain → Infrastructure
→ Application → Interfaces order.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 15:22:38 +00:00

2317 lines
71 KiB
Markdown

# Arch Design Dashboard — Full 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:** Implement the complete Arch Design Dashboard — a web app for visualizing and managing architecture design documents, from empty scaffold to fully working application.
**Architecture:** DDD-layered backend (FastAPI) with 6 modules (design, project, scanner, graph, editor, impl_tracker), Vue 3 SPA frontend with 3 modules (project, graph, editor), D3.js graph visualization. No database — design files are single source of truth, projects persisted as JSON.
**Tech Stack:** Python 3.12 / FastAPI / Uvicorn / uv (backend), Vue 3 / TypeScript / Vite / Pinia / D3.js / Axios (frontend), Docker Compose / Nginx (deployment)
**Spec:** `docs/superpowers/specs/2026-03-23-full-implementation-design.md`
---
## File Map
### Files to Create (new)
```
backend/pyproject.toml
backend/.python-version
backend/tests/__init__.py
backend/tests/test_design_entities.py
backend/tests/test_design_services.py
backend/tests/test_project.py
backend/tests/test_scanner_parsers.py
backend/tests/test_scanner_service.py
backend/tests/test_graph_service.py
backend/tests/test_editor_service.py
backend/tests/test_impl_tracker.py
backend/tests/test_api_project.py
backend/tests/test_api_scanner.py
backend/tests/test_api_graph.py
backend/tests/test_api_editor.py
backend/tests/test_api_impl_tracker.py
backend/app/modules/editor/infrastructure/file_io.py
backend/app/modules/impl_tracker/infrastructure/code_scanner.py
backend/app/modules/impl_tracker/infrastructure/llm_client.py
frontend/package.json
frontend/vite.config.ts
frontend/tsconfig.json
frontend/tsconfig.node.json
frontend/index.html
frontend/src/style.css
docker-compose.yml
backend/Dockerfile
frontend/Dockerfile
frontend/nginx.conf
```
### Files to Populate (exist as 0-byte stubs)
All 86 files under `backend/app/` and `frontend/src/` listed in the scaffold.
---
## Task 1: Backend Build Configuration
**Files:**
- Create: `backend/pyproject.toml`
- Create: `backend/.python-version`
- Create: `backend/tests/__init__.py`
- Create: `backend/tests/conftest.py`
**Note on `__init__.py` files:** The scaffold contains ~25 empty `__init__.py` files across all modules. These remain as empty files unless a task explicitly specifies content for them. They are included in `git add` via directory-level adds.
- [ ] **Step 1: Create pyproject.toml**
```toml
[project]
name = "arch-design-dashboard"
version = "0.1.0"
description = "Architecture Design Dashboard Backend"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.30.0",
"pyyaml>=6.0",
"python-multipart>=0.0.9",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]
[dependency-groups]
dev = [
"pytest>=8.0",
"httpx>=0.27.0",
]
```
- [ ] **Step 2: Create .python-version**
```
3.12
```
- [ ] **Step 3: Create tests/__init__.py (empty)**
- [ ] **Step 4: Create tests/conftest.py (shared fixtures)**
```python
import os
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
@pytest.fixture
def tmp_registry(tmp_path: Path):
"""Set REGISTRY_PATH env var to a temp file for test isolation."""
registry = str(tmp_path / "projects.json")
os.environ["REGISTRY_PATH"] = registry
yield registry
os.environ.pop("REGISTRY_PATH", None)
@pytest.fixture
def client(tmp_registry):
"""Create a test client with isolated registry."""
from app.main import create_app
app = create_app()
return TestClient(app)
@pytest.fixture
def design_dir(tmp_path: Path) -> Path:
"""Create a minimal design directory for testing."""
d = tmp_path / "design"
d.mkdir()
return d
```
- [ ] **Step 4: Install dependencies and verify**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv sync`
Expected: Dependencies installed successfully.
- [ ] **Step 5: Verify pytest runs (no tests yet)**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run pytest --co -q`
Expected: "no tests ran" or similar (no errors).
- [ ] **Step 6: Commit**
```bash
git add backend/pyproject.toml backend/.python-version backend/tests/__init__.py backend/uv.lock
git commit -m "build: add backend pyproject.toml and test infrastructure"
```
---
## Task 2: Shared Kernel — Exceptions
**Files:**
- Modify: `backend/app/shared/__init__.py`
- Modify: `backend/app/shared/kernel/__init__.py`
- Modify: `backend/app/shared/kernel/exceptions.py`
- [ ] **Step 1: Write exceptions.py**
```python
class NotFoundError(Exception):
def __init__(self, entity: str, entity_id: str) -> None:
self.entity = entity
self.entity_id = entity_id
super().__init__(f"{entity} not found: {entity_id}")
class ValidationError(Exception):
def __init__(self, message: str) -> None:
self.message = message
super().__init__(message)
class FileSystemError(Exception):
def __init__(self, path: str, message: str) -> None:
self.path = path
self.message = message
super().__init__(f"Filesystem error at {path}: {message}")
```
- [ ] **Step 2: Verify import works**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run python -c "from app.shared.kernel.exceptions import NotFoundError, ValidationError, FileSystemError; print('OK')"`
Expected: `OK`
- [ ] **Step 3: Commit**
```bash
git add backend/app/shared/
git commit -m "feat(shared): add kernel exceptions"
```
---
## Task 3: Shared Infrastructure — Config & Filesystem
**Files:**
- Modify: `backend/app/shared/infrastructure/__init__.py`
- Modify: `backend/app/shared/infrastructure/config.py`
- Modify: `backend/app/shared/infrastructure/filesystem.py`
- [ ] **Step 1: Write config.py**
```python
from dataclasses import dataclass, field
from pathlib import Path
@dataclass
class Settings:
registry_path: Path = field(
default_factory=lambda: Path.home() / ".arch-design-dashboard" / "projects.json"
)
```
- [ ] **Step 2: Write filesystem.py**
```python
from pathlib import Path
from app.shared.kernel.exceptions import FileSystemError
def read_text(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except OSError as e:
raise FileSystemError(str(path), str(e)) from e
def write_text(path: Path, content: str) -> None:
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
except OSError as e:
raise FileSystemError(str(path), str(e)) from e
def list_files(directory: Path, extensions: list[str] | None = None) -> list[Path]:
if not directory.is_dir():
raise FileSystemError(str(directory), "Not a directory")
files: list[Path] = []
for p in sorted(directory.rglob("*")):
if p.is_file():
if extensions is None or p.suffix in extensions:
files.append(p)
return files
def file_exists(path: Path) -> bool:
return path.is_file()
```
- [ ] **Step 3: Verify imports**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run python -c "from app.shared.infrastructure.config import Settings; from app.shared.infrastructure.filesystem import read_text, write_text, list_files, file_exists; print('OK')"`
Expected: `OK`
- [ ] **Step 4: Commit**
```bash
git add backend/app/shared/
git commit -m "feat(shared): add config and filesystem utilities"
```
---
## Task 4: MOD-DESIGN Domain — Value Objects
**Files:**
- Modify: `backend/app/modules/design/__init__.py`
- Modify: `backend/app/modules/design/domain/__init__.py`
- Modify: `backend/app/modules/design/domain/value_objects.py`
- [ ] **Step 1: Write the test**
Create: `backend/tests/test_design_entities.py`
```python
from app.modules.design.domain.value_objects import (
ArchitectureLayer,
FileStatus,
ModuleLayer,
)
def test_file_status_values():
assert FileStatus.OK == "ok"
assert FileStatus.SPARSE == "sparse"
assert FileStatus.MISSING == "missing"
assert FileStatus.TEMPLATE_RESIDUE == "template-residue"
assert FileStatus.PLACEHOLDER_HEAVY == "placeholder-heavy"
def test_architecture_layer_values():
assert ArchitectureLayer.BUSINESS == "business"
assert ArchitectureLayer.APPLICATION == "application"
assert ArchitectureLayer.DATA == "data"
assert ArchitectureLayer.TECHNOLOGY == "technology"
def test_module_layer_values():
assert ModuleLayer.DOMAIN == "domain"
assert ModuleLayer.APPLICATION == "application"
assert ModuleLayer.INFRASTRUCTURE == "infrastructure"
assert ModuleLayer.INTERFACES == "interfaces"
```
- [ ] **Step 2: Run test — should fail**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run pytest tests/test_design_entities.py -v`
Expected: FAIL (empty module)
- [ ] **Step 3: Write value_objects.py**
```python
from enum import Enum
class FileStatus(str, Enum):
OK = "ok"
SPARSE = "sparse"
MISSING = "missing"
TEMPLATE_RESIDUE = "template-residue"
PLACEHOLDER_HEAVY = "placeholder-heavy"
class ArchitectureLayer(str, Enum):
BUSINESS = "business"
APPLICATION = "application"
DATA = "data"
TECHNOLOGY = "technology"
class ModuleLayer(str, Enum):
DOMAIN = "domain"
APPLICATION = "application"
INFRASTRUCTURE = "infrastructure"
INTERFACES = "interfaces"
```
- [ ] **Step 4: Run test — should pass**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run pytest tests/test_design_entities.py -v`
Expected: 3 passed
- [ ] **Step 5: Commit**
```bash
git add backend/app/modules/design/ backend/tests/test_design_entities.py
git commit -m "feat(design): add value objects — FileStatus, ArchitectureLayer, ModuleLayer"
```
---
## Task 5: MOD-DESIGN Domain — Entities (31 dataclasses)
**Files:**
- Modify: `backend/app/modules/design/domain/entities.py`
- Modify: `backend/tests/test_design_entities.py` (append tests)
- [ ] **Step 1: Add entity creation tests**
Append to `backend/tests/test_design_entities.py`:
```python
from app.modules.design.domain.entities import (
ADR,
ApiContract,
Capability,
ChangeLogEntry,
CodebaseAlignment,
DataFlow,
DataSecurity,
DesignDocument,
Domain,
DomainEntity,
DomainModule,
Entity,
Environment,
ExternalSystem,
Integration,
Module,
ModuleBoundaryRule,
OperationalBaseline,
ReleasePlan,
RuntimeComponent,
RuntimeTopology,
Scenario,
ScopeAndGoals,
SharedTerm,
SolutionLayer,
SystemContext,
TechSelection,
TraceabilityLink,
UbiquitousTerm,
UserJourney,
ValueFlow,
)
def test_capability_creation():
cap = Capability(
capability_id="CAP-01",
name="test",
description="desc",
priority="must",
phase="MVP",
related_value_flows=["VF-01"],
)
assert cap.capability_id == "CAP-01"
assert cap.related_value_flows == ["VF-01"]
def test_module_creation():
mod = Module(
module_id="MOD-01",
name="test",
layer="backend",
description="desc",
phase="MVP",
depends_on=["MOD-02"],
capabilities=["CAP-01"],
)
assert mod.depends_on == ["MOD-02"]
def test_traceability_link_list_fields():
tl = TraceabilityLink(
trace_id="TR-01",
capability_id="CAP-01",
module_id="MOD-01",
entity_ids=["ENT-01", "ENT-02"],
value_flow_ids=["VF-01"],
notes="test",
)
assert len(tl.entity_ids) == 2
def test_design_document_list_fields():
dd = DesignDocument(
doc_id="DOC-01",
title="test",
version="0.1",
status="draft",
owners=["owner1"],
upstream=["a.md"],
downstream=["b.md"],
file_path="test.md",
)
assert dd.owners == ["owner1"]
def test_all_31_entities_importable():
"""Verify all 31 entity classes can be imported."""
entities = [
Capability, ValueFlow, UserJourney, ScopeAndGoals,
Module, Integration, ExternalSystem, ApiContract,
CodebaseAlignment, ModuleBoundaryRule, SystemContext, SolutionLayer,
Entity, DataFlow, DataSecurity,
TechSelection, RuntimeComponent, RuntimeTopology, Environment,
OperationalBaseline, ReleasePlan,
TraceabilityLink, ChangeLogEntry, ADR, DesignDocument,
Domain, UbiquitousTerm, SharedTerm, Scenario, DomainModule, DomainEntity,
]
assert len(entities) == 31
```
- [ ] **Step 2: Run tests — should fail**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run pytest tests/test_design_entities.py -v`
Expected: FAIL (empty entities.py)
- [ ] **Step 3: Write entities.py with all 31 dataclasses**
Write `backend/app/modules/design/domain/entities.py` with all 31 `@dataclass` definitions exactly as specified in the spec (Section 3.2). Every `list[str]` field that comes from CSV space-delimited values must use `list[str]` type.
Dataclasses to implement (grouped by architecture layer):
- **Business:** Capability, ValueFlow, UserJourney, ScopeAndGoals
- **Application:** Module, Integration, ExternalSystem, ApiContract, CodebaseAlignment, ModuleBoundaryRule, SystemContext, SolutionLayer
- **Data:** Entity, DataFlow, DataSecurity
- **Technology:** TechSelection, RuntimeComponent, RuntimeTopology, Environment, OperationalBaseline, ReleasePlan
- **Cross-layer:** TraceabilityLink, ChangeLogEntry, ADR, DesignDocument
- **Domain:** Domain, UbiquitousTerm, SharedTerm, Scenario, DomainModule, DomainEntity
Refer to spec Section 3.2 for exact field definitions of each class.
- [ ] **Step 4: Run tests — should pass**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run pytest tests/test_design_entities.py -v`
Expected: All passed
- [ ] **Step 5: Commit**
```bash
git add backend/app/modules/design/domain/entities.py backend/tests/test_design_entities.py
git commit -m "feat(design): add 31 design entity dataclasses"
```
---
## Task 6: MOD-DESIGN Domain — Validation Service
**Files:**
- Modify: `backend/app/modules/design/domain/services.py`
- Create: `backend/tests/test_design_services.py`
- [ ] **Step 1: Write the tests**
```python
import pytest
from app.modules.design.domain.entities import (
Capability,
Entity,
Module,
TraceabilityLink,
)
from app.modules.design.domain.services import DesignValidationService
from app.modules.design.domain.value_objects import FileStatus
class TestFileStatusDetermination:
def test_empty_content_is_missing(self):
assert DesignValidationService.determine_file_status("", "test.csv") == FileStatus.MISSING
def test_csv_header_only_is_sparse(self):
assert DesignValidationService.determine_file_status("id,name\n", "test.csv") == FileStatus.SPARSE
def test_csv_with_data_is_ok(self):
content = "id,name\n1,foo\n2,bar\n"
assert DesignValidationService.determine_file_status(content, "test.csv") == FileStatus.OK
def test_md_too_short_is_sparse(self):
assert DesignValidationService.determine_file_status("# Title\n\nShort.\n", "test.md") == FileStatus.SPARSE
def test_template_residue_detected(self):
content = "id,name\nTODO,<replace this>\nreal,data\n"
assert DesignValidationService.determine_file_status(content, "test.csv") == FileStatus.TEMPLATE_RESIDUE
def test_placeholder_heavy(self):
content = "id,name,desc\nTODO,TODO,TODO\nTODO,TODO,TODO\n"
assert DesignValidationService.determine_file_status(content, "test.csv") == FileStatus.PLACEHOLDER_HEAVY
def test_ok_md_file(self):
content = "---\ndoc_id: DOC-01\ntitle: Test\n---\n\n# Title\n\nThis is a real document with enough content.\nLine 4.\nLine 5.\nLine 6.\n"
assert DesignValidationService.determine_file_status(content, "test.md") == FileStatus.OK
class TestConstraintValidation:
def _make_cap(self, cap_id: str) -> Capability:
return Capability(cap_id, "n", "d", "must", "MVP", [])
def _make_mod(self, mod_id: str) -> Module:
return Module(mod_id, "n", "backend", "d", "MVP", [], [])
def _make_ent(self, ent_id: str, owner: str) -> Entity:
return Entity(ent_id, "n", "d", owner, "d", "MVP", "f.csv")
def _make_link(self, trace_id: str, cap: str, mod: str, ents: list[str]) -> TraceabilityLink:
return TraceabilityLink(trace_id, cap, mod, ents, [], "")
def test_capability_without_module_link_is_violation(self):
caps = [self._make_cap("CAP-01")]
links = [] # no link references CAP-01
violations = DesignValidationService.check_capability_module_linkage(caps, links)
assert len(violations) == 1
def test_entity_without_owner_is_violation(self):
entities = [self._make_ent("ENT-01", "")]
violations = DesignValidationService.check_entity_owner(entities)
assert len(violations) == 1
def test_valid_traceability_passes(self):
caps = [self._make_cap("CAP-01")]
mods = [self._make_mod("MOD-01")]
ents = [self._make_ent("ENT-01", "MOD-01")]
links = [self._make_link("TR-01", "CAP-01", "MOD-01", ["ENT-01"])]
violations = DesignValidationService.check_traceability_references(links, caps, mods, ents)
assert len(violations) == 0
def test_broken_traceability_reference_is_violation(self):
caps = [self._make_cap("CAP-01")]
mods = [self._make_mod("MOD-01")]
ents = [self._make_ent("ENT-01", "MOD-01")]
links = [self._make_link("TR-01", "CAP-MISSING", "MOD-01", ["ENT-01"])]
violations = DesignValidationService.check_traceability_references(links, caps, mods, ents)
assert len(violations) >= 1
```
- [ ] **Step 2: Run tests — should fail**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run pytest tests/test_design_services.py -v`
Expected: FAIL
- [ ] **Step 3: Implement services.py**
```python
from dataclasses import dataclass
from app.modules.design.domain.entities import (
Capability,
Entity,
Module,
TraceabilityLink,
)
from app.modules.design.domain.value_objects import FileStatus
TEMPLATE_MARKERS = ["TODO", "EXAMPLE", "<replace", "{{", "Lorem ipsum"]
@dataclass
class ConstraintViolation:
rule: str
entity_id: str
message: str
class DesignValidationService:
@staticmethod
def determine_file_status(content: str, file_path: str) -> FileStatus:
if not content or not content.strip():
return FileStatus.MISSING
lines = [l for l in content.strip().splitlines() if l.strip()]
is_csv = file_path.endswith(".csv")
# Sparse check
if is_csv and len(lines) < 2:
return FileStatus.SPARSE
if not is_csv and len(lines) < 5:
return FileStatus.SPARSE
# Count placeholder tokens
total_cells = 0
placeholder_cells = 0
for line in lines:
cells = line.split(",") if is_csv else [line]
for cell in cells:
total_cells += 1
if any(m.lower() in cell.lower() for m in TEMPLATE_MARKERS):
placeholder_cells += 1
# Template residue: any marker present
if placeholder_cells > 0 and placeholder_cells / total_cells <= 0.3:
return FileStatus.TEMPLATE_RESIDUE
# Placeholder heavy: >30%
if total_cells > 0 and placeholder_cells / total_cells > 0.3:
return FileStatus.PLACEHOLDER_HEAVY
return FileStatus.OK
@staticmethod
def check_capability_module_linkage(
capabilities: list[Capability],
traceability_links: list[TraceabilityLink],
) -> list[ConstraintViolation]:
linked_caps = {link.capability_id for link in traceability_links}
return [
ConstraintViolation(
rule="capability_module_linkage",
entity_id=cap.capability_id,
message=f"Capability {cap.capability_id} has no TraceabilityLink to any module",
)
for cap in capabilities
if cap.capability_id not in linked_caps
]
@staticmethod
def check_entity_owner(entities: list[Entity]) -> list[ConstraintViolation]:
return [
ConstraintViolation(
rule="entity_owner",
entity_id=ent.entity_id,
message=f"Entity {ent.entity_id} has no owner module",
)
for ent in entities
if not ent.owner_module
]
@staticmethod
def check_traceability_references(
links: list[TraceabilityLink],
capabilities: list[Capability],
modules: list[Module],
entities: list[Entity],
) -> list[ConstraintViolation]:
cap_ids = {c.capability_id for c in capabilities}
mod_ids = {m.module_id for m in modules}
ent_ids = {e.entity_id for e in entities}
violations: list[ConstraintViolation] = []
for link in links:
if link.capability_id not in cap_ids:
violations.append(ConstraintViolation(
"traceability_ref", link.trace_id,
f"References unknown capability {link.capability_id}",
))
if link.module_id not in mod_ids:
violations.append(ConstraintViolation(
"traceability_ref", link.trace_id,
f"References unknown module {link.module_id}",
))
for eid in link.entity_ids:
if eid not in ent_ids:
violations.append(ConstraintViolation(
"traceability_ref", link.trace_id,
f"References unknown entity {eid}",
))
return violations
@classmethod
def validate_all(
cls,
capabilities: list[Capability],
modules: list[Module],
entities: list[Entity],
traceability_links: list[TraceabilityLink],
) -> list[ConstraintViolation]:
violations: list[ConstraintViolation] = []
violations.extend(cls.check_capability_module_linkage(capabilities, traceability_links))
violations.extend(cls.check_entity_owner(entities))
violations.extend(cls.check_traceability_references(
traceability_links, capabilities, modules, entities,
))
return violations
```
- [ ] **Step 4: Run tests — should pass**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run pytest tests/test_design_services.py -v`
Expected: All passed
- [ ] **Step 5: Commit**
```bash
git add backend/app/modules/design/domain/services.py backend/tests/test_design_services.py
git commit -m "feat(design): add DesignValidationService — file status detection and constraint rules"
```
---
## Task 7: MOD-PROJECT Domain
**Files:**
- Modify: `backend/app/modules/project/domain/entities.py`
- Modify: `backend/app/modules/project/domain/repositories.py`
- Modify: `backend/app/modules/project/__init__.py`
- Modify: `backend/app/modules/project/domain/__init__.py`
- [ ] **Step 1: Write entities.py**
```python
from dataclasses import dataclass
from datetime import datetime
@dataclass
class Project:
id: str
name: str
design_dir: str
code_dir: str | None
created_at: datetime
```
- [ ] **Step 2: Write repositories.py**
```python
from abc import ABC, abstractmethod
from app.modules.project.domain.entities import Project
class ProjectRepository(ABC):
@abstractmethod
def list_all(self) -> list[Project]:
...
@abstractmethod
def get_by_id(self, project_id: str) -> Project | None:
...
@abstractmethod
def save(self, project: Project) -> None:
...
@abstractmethod
def delete(self, project_id: str) -> None:
...
```
- [ ] **Step 3: Verify import**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run python -c "from app.modules.project.domain.entities import Project; from app.modules.project.domain.repositories import ProjectRepository; print('OK')"`
Expected: `OK`
- [ ] **Step 4: Commit**
```bash
git add backend/app/modules/project/
git commit -m "feat(project): add Project entity and ProjectRepository interface"
```
---
## Task 8: MOD-PROJECT Infrastructure — JsonProjectRepository
**Files:**
- Modify: `backend/app/modules/project/infrastructure/__init__.py`
- Modify: `backend/app/modules/project/infrastructure/json_repository.py`
- Create: `backend/tests/test_project.py`
- [ ] **Step 1: Write the test**
```python
import json
from pathlib import Path
import pytest
from app.modules.project.domain.entities import Project
from app.modules.project.infrastructure.json_repository import JsonProjectRepository
@pytest.fixture
def repo(tmp_path: Path) -> JsonProjectRepository:
return JsonProjectRepository(tmp_path / "projects.json")
def test_empty_repo_returns_empty_list(repo: JsonProjectRepository):
assert repo.list_all() == []
def test_save_and_get(repo: JsonProjectRepository):
from datetime import datetime
p = Project(id="id1", name="test", design_dir="/tmp/d", code_dir=None, created_at=datetime(2026, 1, 1))
repo.save(p)
assert repo.get_by_id("id1") is not None
assert repo.get_by_id("id1").name == "test"
def test_list_all(repo: JsonProjectRepository):
from datetime import datetime
p1 = Project(id="id1", name="a", design_dir="/d1", code_dir=None, created_at=datetime(2026, 1, 1))
p2 = Project(id="id2", name="b", design_dir="/d2", code_dir=None, created_at=datetime(2026, 1, 2))
repo.save(p1)
repo.save(p2)
assert len(repo.list_all()) == 2
def test_delete(repo: JsonProjectRepository):
from datetime import datetime
p = Project(id="id1", name="test", design_dir="/d", code_dir=None, created_at=datetime(2026, 1, 1))
repo.save(p)
repo.delete("id1")
assert repo.get_by_id("id1") is None
def test_get_nonexistent_returns_none(repo: JsonProjectRepository):
assert repo.get_by_id("nope") is None
```
- [ ] **Step 2: Run test — should fail**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run pytest tests/test_project.py -v`
Expected: FAIL
- [ ] **Step 3: Implement json_repository.py**
```python
import json
from datetime import datetime
from pathlib import Path
from app.modules.project.domain.entities import Project
from app.modules.project.domain.repositories import ProjectRepository
class JsonProjectRepository(ProjectRepository):
def __init__(self, path: Path) -> None:
self._path = path
def _load(self) -> list[dict]:
if not self._path.exists():
return []
return json.loads(self._path.read_text(encoding="utf-8"))
def _save(self, data: list[dict]) -> None:
self._path.parent.mkdir(parents=True, exist_ok=True)
self._path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
@staticmethod
def _to_dict(p: Project) -> dict:
return {
"id": p.id,
"name": p.name,
"design_dir": p.design_dir,
"code_dir": p.code_dir,
"created_at": p.created_at.isoformat(),
}
@staticmethod
def _from_dict(d: dict) -> Project:
return Project(
id=d["id"],
name=d["name"],
design_dir=d["design_dir"],
code_dir=d.get("code_dir"),
created_at=datetime.fromisoformat(d["created_at"]),
)
def list_all(self) -> list[Project]:
return [self._from_dict(d) for d in self._load()]
def get_by_id(self, project_id: str) -> Project | None:
for d in self._load():
if d["id"] == project_id:
return self._from_dict(d)
return None
def save(self, project: Project) -> None:
data = self._load()
data = [d for d in data if d["id"] != project.id]
data.append(self._to_dict(project))
self._save(data)
def delete(self, project_id: str) -> None:
data = [d for d in self._load() if d["id"] != project_id]
self._save(data)
```
- [ ] **Step 4: Run tests — should pass**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run pytest tests/test_project.py -v`
Expected: All passed
- [ ] **Step 5: Commit**
```bash
git add backend/app/modules/project/infrastructure/ backend/tests/test_project.py
git commit -m "feat(project): add JsonProjectRepository with JSON file persistence"
```
---
## Task 9: MOD-PROJECT Application — ProjectService
**Files:**
- Modify: `backend/app/modules/project/application/__init__.py`
- Modify: `backend/app/modules/project/application/services.py`
- Modify: `backend/tests/test_project.py` (append)
- [ ] **Step 1: Add service tests**
Append to `backend/tests/test_project.py`:
```python
from app.modules.project.application.services import ProjectService
from app.shared.kernel.exceptions import NotFoundError, ValidationError
@pytest.fixture
def service(tmp_path: Path) -> ProjectService:
repo = JsonProjectRepository(tmp_path / "projects.json")
return ProjectService(repo)
def test_create_project_validates_design_dir(service: ProjectService, tmp_path: Path):
design_dir = tmp_path / "design"
design_dir.mkdir()
project = service.create_project("test", str(design_dir))
assert project.name == "test"
assert project.id # UUID generated
def test_create_project_rejects_missing_dir(service: ProjectService):
with pytest.raises(ValidationError):
service.create_project("test", "/nonexistent/path")
def test_get_project_not_found(service: ProjectService):
with pytest.raises(NotFoundError):
service.get_project("nonexistent")
def test_delete_project(service: ProjectService, tmp_path: Path):
design_dir = tmp_path / "design"
design_dir.mkdir()
p = service.create_project("test", str(design_dir))
service.delete_project(p.id)
with pytest.raises(NotFoundError):
service.get_project(p.id)
```
- [ ] **Step 2: Run tests — should fail**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run pytest tests/test_project.py -v -k "service"`
Expected: FAIL
- [ ] **Step 3: Implement services.py**
```python
import uuid
from datetime import datetime, timezone
from pathlib import Path
from app.modules.project.domain.entities import Project
from app.modules.project.domain.repositories import ProjectRepository
from app.shared.kernel.exceptions import NotFoundError, ValidationError
class ProjectService:
def __init__(self, repository: ProjectRepository) -> None:
self._repo = repository
def list_projects(self) -> list[Project]:
return self._repo.list_all()
def create_project(
self, name: str, design_dir: str, code_dir: str | None = None,
) -> Project:
if not Path(design_dir).is_dir():
raise ValidationError(f"Design directory does not exist: {design_dir}")
project = Project(
id=str(uuid.uuid4()),
name=name,
design_dir=design_dir,
code_dir=code_dir,
created_at=datetime.now(timezone.utc),
)
self._repo.save(project)
return project
def get_project(self, project_id: str) -> Project:
project = self._repo.get_by_id(project_id)
if project is None:
raise NotFoundError("Project", project_id)
return project
def delete_project(self, project_id: str) -> None:
project = self._repo.get_by_id(project_id)
if project is None:
raise NotFoundError("Project", project_id)
self._repo.delete(project_id)
```
- [ ] **Step 4: Run tests — should pass**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run pytest tests/test_project.py -v`
Expected: All passed
- [ ] **Step 5: Commit**
```bash
git add backend/app/modules/project/application/ backend/tests/test_project.py
git commit -m "feat(project): add ProjectService with CRUD and path validation"
```
---
## Task 10: MOD-PROJECT Interfaces — HTTP Router
**Files:**
- Modify: `backend/app/modules/project/interfaces/http/router.py`
- Modify: `backend/app/modules/project/interfaces/__init__.py`
- Modify: `backend/app/modules/project/interfaces/http/__init__.py`
- Modify: `backend/app/main.py`
- Create: `backend/tests/test_api_project.py`
- [ ] **Step 1: Write API tests**
Uses `client` and `design_dir` fixtures from `conftest.py`.
```python
import pytest
def test_health(client):
r = client.get("/api/health")
assert r.status_code == 200
assert r.json()["status"] == "ok"
def test_list_projects_empty(client):
r = client.get("/api/projects")
assert r.status_code == 200
assert r.json() == []
def test_create_and_get_project(client, design_dir):
r = client.post("/api/projects", json={"name": "test", "design_dir": str(design_dir)})
assert r.status_code == 201
pid = r.json()["id"]
r = client.get(f"/api/projects/{pid}")
assert r.status_code == 200
assert r.json()["name"] == "test"
def test_create_project_invalid_dir(client):
r = client.post("/api/projects", json={"name": "test", "design_dir": "/nonexistent"})
assert r.status_code == 400
def test_delete_project(client, design_dir):
r = client.post("/api/projects", json={"name": "test", "design_dir": str(design_dir)})
pid = r.json()["id"]
r = client.delete(f"/api/projects/{pid}")
assert r.status_code == 204
r = client.get(f"/api/projects/{pid}")
assert r.status_code == 404
def test_get_nonexistent_project(client):
r = client.get("/api/projects/nonexistent")
assert r.status_code == 404
```
- [ ] **Step 2: Write router.py**
```python
from fastapi import APIRouter, Response
from pydantic import BaseModel
from app.modules.project.application.services import ProjectService
router = APIRouter(prefix="/projects", tags=["project"])
_service: ProjectService | None = None
def init_router(service: ProjectService) -> None:
global _service
_service = service
class CreateProjectRequest(BaseModel):
name: str
design_dir: str
code_dir: str | None = None
class ProjectResponse(BaseModel):
id: str
name: str
design_dir: str
code_dir: str | None
created_at: str
def _to_response(p) -> dict:
return {
"id": p.id,
"name": p.name,
"design_dir": p.design_dir,
"code_dir": p.code_dir,
"created_at": p.created_at.isoformat(),
}
@router.get("")
def list_projects():
return [_to_response(p) for p in _service.list_projects()]
@router.post("", status_code=201)
def create_project(req: CreateProjectRequest):
p = _service.create_project(req.name, req.design_dir, req.code_dir)
return _to_response(p)
@router.get("/{project_id}")
def get_project(project_id: str):
p = _service.get_project(project_id)
return _to_response(p)
@router.delete("/{project_id}", status_code=204)
def delete_project(project_id: str):
_service.delete_project(project_id)
return Response(status_code=204)
```
- [ ] **Step 3: Write main.py with app factory**
```python
import os
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from app.shared.kernel.exceptions import NotFoundError, ValidationError
from app.shared.infrastructure.config import Settings
from app.modules.project.infrastructure.json_repository import JsonProjectRepository
from app.modules.project.application.services import ProjectService
from app.modules.project.interfaces.http.router import router as project_router, init_router as init_project_router
def create_app() -> FastAPI:
app = FastAPI(title="Arch Design Dashboard API", version="0.1.0")
# Settings
registry_path = Path(os.environ.get("REGISTRY_PATH", str(Settings().registry_path)))
# Wire Project module
project_repo = JsonProjectRepository(registry_path)
project_service = ProjectService(project_repo)
init_project_router(project_service)
# Register routers
app.include_router(project_router, prefix="/api")
# Health check
@app.get("/api/health")
def health():
return {"status": "ok"}
# Exception handlers
@app.exception_handler(NotFoundError)
async def not_found_handler(request: Request, exc: NotFoundError):
return JSONResponse(status_code=404, content={"detail": str(exc)})
@app.exception_handler(ValidationError)
async def validation_handler(request: Request, exc: ValidationError):
return JSONResponse(status_code=400, content={"detail": exc.message})
return app
# For uvicorn: use `uvicorn app.main:create_app --factory`
# or for simple usage:
app = create_app()
```
Note: Tests must call `create_app()` directly to get a fresh app instance with test-specific config. The module-level `app` is only used for `uvicorn app.main:app` convenience. Tests override `REGISTRY_PATH` env var before calling `create_app()`.
- [ ] **Step 4: Run tests — should pass**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run pytest tests/test_api_project.py -v`
Expected: All passed
- [ ] **Step 5: Verify server starts**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && timeout 5 uv run uvicorn app.main:app --port 8900 || true`
Expected: Server starts (timeout kills it after 5s)
- [ ] **Step 6: Commit**
```bash
git add backend/app/modules/project/interfaces/ backend/app/main.py backend/tests/test_api_project.py
git commit -m "feat(project): add REST API — CRUD endpoints with FastAPI"
```
---
## Task 11: MOD-SCANNER Domain — Entities
**Files:**
- Modify: `backend/app/modules/scanner/domain/__init__.py`
- Modify: `backend/app/modules/scanner/domain/entities.py`
- Modify: `backend/app/modules/scanner/__init__.py`
- [ ] **Step 1: Write scanner domain entities**
Write `backend/app/modules/scanner/domain/entities.py` with `FileStatusEntry`, `ScanSummary`, and `ScanResult` exactly as specified in spec Section 3.4. ScanResult carries all parsed Design entity lists plus file status info.
- [ ] **Step 2: Verify import**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run python -c "from app.modules.scanner.domain.entities import ScanResult, FileStatusEntry, ScanSummary; print('OK')"`
Expected: `OK`
- [ ] **Step 3: Commit**
```bash
git add backend/app/modules/scanner/
git commit -m "feat(scanner): add ScanResult, FileStatusEntry, ScanSummary domain entities"
```
---
## Task 12: MOD-SCANNER Infrastructure — CSV Parser
**Files:**
- Modify: `backend/app/modules/scanner/infrastructure/parsers/csv_parser.py`
- Modify: `backend/app/modules/scanner/infrastructure/parsers/__init__.py`
- Modify: `backend/app/modules/scanner/infrastructure/__init__.py`
- Create: `backend/tests/test_scanner_parsers.py`
- [ ] **Step 1: Write CSV parser tests**
Test that the CSV parser can read the real design CSV files and produce the correct Design entity types. Use the actual `design/business-architecture/02-capability-map.csv` as test input.
Key tests:
- Parse capability-map.csv → list[Capability] with correct field mapping
- Parse modules.csv → list[Module] with list[str] fields split from spaces
- Parse traceability.csv → list[TraceabilityLink] with space-split entity_ids
- Parse unknown CSV filename → returns empty list (graceful)
- [ ] **Step 2: Run tests — should fail**
- [ ] **Step 3: Implement csv_parser.py**
The parser must:
1. Read CSV with Python's `csv.DictReader`
2. Map file basename to entity type (e.g., `02-capability-map.csv` → Capability)
3. Split space-delimited fields into `list[str]`
4. Return a dict of entity type → list of instances
File-to-entity mapping:
- `*capability-map*` → Capability
- `*modules*` → Module (application-architecture only)
- `*value-flows*` → ValueFlow
- `*user-journeys*` → UserJourney
- `*integrations*` → Integration
- `*external-systems*` → ExternalSystem
- `*entities*` → Entity (data-architecture only)
- `*data-flows*` → DataFlow
- `*data-security*` → DataSecurity
- `*technology-selection*` → TechSelection
- `*runtime-components*` → RuntimeComponent
- `*environments*` → Environment
- `*codebase-alignment*` → CodebaseAlignment
- `*change-log*` → ChangeLogEntry
- `traceability.csv` → TraceabilityLink
- `*shared-terminology*` → SharedTerm
- `*ubiquitous-language*` → UbiquitousTerm
- `*scenarios-and-flows*` → Scenario
- `*domain-modules*` → DomainModule
- `*domain-entities*` → DomainEntity
- [ ] **Step 4: Run tests — should pass**
- [ ] **Step 5: Commit**
```bash
git add backend/app/modules/scanner/infrastructure/parsers/csv_parser.py backend/tests/test_scanner_parsers.py
git commit -m "feat(scanner): add CSV parser — maps 20 CSV file types to Design entities"
```
---
## Task 13: MOD-SCANNER Infrastructure — MD Parser
**Files:**
- Modify: `backend/app/modules/scanner/infrastructure/parsers/md_parser.py`
- Modify: `backend/tests/test_scanner_parsers.py` (append)
- [ ] **Step 1: Write MD parser tests**
Test that:
- Parse MD with YAML frontmatter → DesignDocument (doc_id, title, status, upstream, downstream, file_path)
- Parse scope-and-goals.md → ScopeAndGoals
- Parse system-context.md → SystemContext
- Parse MD without frontmatter → DesignDocument with minimal fields
- Parse ADR template → ADR entity
- Parse domain-overview.md → Domain entity
- [ ] **Step 2: Run tests — should fail**
- [ ] **Step 3: Implement md_parser.py**
The parser must:
1. Extract YAML frontmatter between `---` markers using `yaml.safe_load`
2. Produce a DesignDocument for every MD file that has frontmatter
3. For specific files, also produce specialized entities:
- `*scope-and-goals*` → ScopeAndGoals
- `*system-context*` → SystemContext
- `*solution-layering*` → SolutionLayer
- `*module-boundary*` → ModuleBoundaryRule
- `*runtime-topology*` → RuntimeTopology
- `*operational-baseline*` → OperationalBaseline
- `*release-and-rollback*` → ReleasePlan
- `*domain-overview*` → Domain
- `*domain-decisions*` → (skip, no entity needed beyond DesignDocument)
- `ADR-*` (not template) → ADR
- [ ] **Step 4: Run tests — should pass**
- [ ] **Step 5: Commit**
```bash
git add backend/app/modules/scanner/infrastructure/parsers/md_parser.py backend/tests/test_scanner_parsers.py
git commit -m "feat(scanner): add Markdown parser — frontmatter extraction and specialized entity mapping"
```
---
## Task 14: MOD-SCANNER Infrastructure — YAML & OpenAPI Parsers
**Files:**
- Modify: `backend/app/modules/scanner/infrastructure/parsers/yaml_parser.py`
- Modify: `backend/app/modules/scanner/infrastructure/parsers/openapi_parser.py`
- Modify: `backend/tests/test_scanner_parsers.py` (append)
- [ ] **Step 1: Write tests for OpenAPI parser**
Test that parsing `04-api-contracts.openapi.yaml` produces a list of ApiContract with correct path/method/operationId.
- [ ] **Step 2: Implement yaml_parser.py** (thin wrapper around yaml.safe_load)
- [ ] **Step 3: Implement openapi_parser.py**
Parse OpenAPI YAML → iterate `paths` → for each path+method, create an `ApiContract(doc_id, path, method, operation_id, summary)`.
- [ ] **Step 4: Run tests — should pass**
- [ ] **Step 5: Commit**
```bash
git add backend/app/modules/scanner/infrastructure/parsers/ backend/tests/test_scanner_parsers.py
git commit -m "feat(scanner): add YAML and OpenAPI parsers"
```
---
## Task 15: MOD-SCANNER Application — ScanService
**Files:**
- Modify: `backend/app/modules/scanner/application/__init__.py`
- Modify: `backend/app/modules/scanner/application/services.py`
- Create: `backend/tests/test_scanner_service.py`
- [ ] **Step 1: Write ScanService tests**
Test using the real `design/` directory from the project:
- `scan()` produces a ScanResult with populated entity lists
- `scan()` produces file_statuses for all design files
- `scan()` summary counts match file_statuses
- `get_latest_scan()` returns None before first scan
- `get_latest_scan()` returns cached result after scan
- [ ] **Step 2: Run tests — should fail**
- [ ] **Step 3: Implement services.py**
```python
from datetime import datetime, timezone
from pathlib import Path
from app.modules.design.domain.entities import * # all 31 entities
from app.modules.design.domain.services import DesignValidationService
from app.modules.design.domain.value_objects import FileStatus
from app.modules.project.domain.entities import Project
from app.modules.scanner.domain.entities import FileStatusEntry, ScanResult, ScanSummary
from app.modules.scanner.infrastructure.parsers.csv_parser import CsvParser
from app.modules.scanner.infrastructure.parsers.md_parser import MdParser
from app.modules.scanner.infrastructure.parsers.openapi_parser import OpenapiParser
class ScanService:
def __init__(self) -> None:
self._cache: dict[str, ScanResult] = {}
def scan(self, project: Project) -> ScanResult:
design_dir = Path(project.design_dir)
csv_parser = CsvParser()
md_parser = MdParser()
openapi_parser = OpenapiParser()
# Collect all parsed entities
all_entities: dict[str, list] = {} # entity_type_name -> list
# Scan all files and collect file statuses
file_statuses: list[FileStatusEntry] = []
for file_path in sorted(design_dir.rglob("*")):
if not file_path.is_file():
continue
if file_path.name.startswith("."):
continue
relative = str(file_path.relative_to(design_dir))
content = file_path.read_text(encoding="utf-8")
status = DesignValidationService.determine_file_status(content, file_path.name)
lines = len([l for l in content.splitlines() if l.strip()])
file_statuses.append(FileStatusEntry(path=relative, status=status, content_lines=lines))
# Parse by type
if file_path.suffix == ".csv":
parsed = csv_parser.parse(file_path)
for key, items in parsed.items():
all_entities.setdefault(key, []).extend(items)
elif file_path.suffix == ".md":
parsed = md_parser.parse(file_path)
for key, items in parsed.items():
all_entities.setdefault(key, []).extend(items)
elif file_path.suffix in (".yaml", ".yml"):
if "openapi" in file_path.name or "api-contracts" in file_path.name:
parsed = openapi_parser.parse(file_path)
for key, items in parsed.items():
all_entities.setdefault(key, []).extend(items)
# Build summary
summary = ScanSummary(
total_files=len(file_statuses),
ok=sum(1 for f in file_statuses if f.status == FileStatus.OK),
sparse=sum(1 for f in file_statuses if f.status == FileStatus.SPARSE),
missing=sum(1 for f in file_statuses if f.status == FileStatus.MISSING),
placeholder_heavy=sum(1 for f in file_statuses if f.status == FileStatus.PLACEHOLDER_HEAVY),
template_residue=sum(1 for f in file_statuses if f.status == FileStatus.TEMPLATE_RESIDUE),
)
# Assemble ScanResult — map entity type names to ScanResult fields
result = ScanResult(
project_id=project.id,
scanned_at=datetime.now(timezone.utc),
file_statuses=file_statuses,
summary=summary,
capabilities=all_entities.get("capabilities", []),
modules=all_entities.get("modules", []),
entities=all_entities.get("entities", []),
value_flows=all_entities.get("value_flows", []),
user_journeys=all_entities.get("user_journeys", []),
integrations=all_entities.get("integrations", []),
data_flows=all_entities.get("data_flows", []),
traceability_links=all_entities.get("traceability_links", []),
external_systems=all_entities.get("external_systems", []),
runtime_components=all_entities.get("runtime_components", []),
tech_selections=all_entities.get("tech_selections", []),
environments=all_entities.get("environments", []),
design_documents=all_entities.get("design_documents", []),
change_log_entries=all_entities.get("change_log_entries", []),
adrs=all_entities.get("adrs", []),
shared_terms=all_entities.get("shared_terms", []),
domains=all_entities.get("domains", []),
ubiquitous_terms=all_entities.get("ubiquitous_terms", []),
scenarios=all_entities.get("scenarios", []),
domain_modules=all_entities.get("domain_modules", []),
domain_entities=all_entities.get("domain_entities", []),
data_securities=all_entities.get("data_securities", []),
codebase_alignments=all_entities.get("codebase_alignments", []),
api_contracts=all_entities.get("api_contracts", []),
scope_and_goals=next(iter(all_entities.get("scope_and_goals", [])), None),
system_context=next(iter(all_entities.get("system_context", [])), None),
solution_layer=next(iter(all_entities.get("solution_layer", [])), None),
module_boundary_rule=next(iter(all_entities.get("module_boundary_rule", [])), None),
runtime_topology=next(iter(all_entities.get("runtime_topology", [])), None),
operational_baseline=next(iter(all_entities.get("operational_baseline", [])), None),
release_plan=next(iter(all_entities.get("release_plan", [])), None),
)
self._cache[project.id] = result
return result
def get_latest_scan(self, project_id: str) -> ScanResult | None:
return self._cache.get(project_id)
```
- [ ] **Step 4: Run tests — should pass**
- [ ] **Step 5: Commit**
```bash
git add backend/app/modules/scanner/application/ backend/tests/test_scanner_service.py
git commit -m "feat(scanner): add ScanService — orchestrates parsers, file status, and entity collection"
```
---
## Task 16: MOD-SCANNER Interfaces — HTTP Router
**Files:**
- Modify: `backend/app/modules/scanner/interfaces/http/router.py`
- Modify: `backend/app/modules/scanner/interfaces/__init__.py`
- Modify: `backend/app/modules/scanner/interfaces/http/__init__.py`
- Modify: `backend/app/main.py` (wire scanner)
- Create: `backend/tests/test_api_scanner.py`
- [ ] **Step 1: Write API tests**
Test scan trigger, get latest scan, and all 13 entity query endpoints (10 list + 3 detail). Use a real design/ directory as fixture.
- [ ] **Step 2: Implement router.py**
The scanner router provides:
- `POST /projects/{project_id}/scan` → trigger scan → return ScanResultResponse (trimmed: no entity lists)
- `GET /projects/{project_id}/scan` → get cached scan → return ScanResultResponse
- 10 list endpoints for entity types
- 3 detail endpoints (CapabilityDetail, ModuleDetail, EntityDetail) with join logic
The router needs access to both ProjectService (to resolve project_id → Project) and ScanService.
- [ ] **Step 3: Wire scanner into main.py**
Add ScanService instantiation and wire the scanner router in `create_app()`.
- [ ] **Step 4: Run tests — should pass**
- [ ] **Step 5: Commit**
```bash
git add backend/app/modules/scanner/interfaces/ backend/app/main.py backend/tests/test_api_scanner.py
git commit -m "feat(scanner): add REST API — scan trigger, entity query endpoints"
```
---
## Task 17: MOD-GRAPH Domain — Entities
**Files:**
- Modify: `backend/app/modules/graph/domain/entities.py`
- Modify: `backend/app/modules/graph/domain/__init__.py`
- Modify: `backend/app/modules/graph/__init__.py`
- [ ] **Step 1: Write graph domain entities**
Write `GraphNode`, `GraphEdge`, `GraphGroup`, `GraphView` exactly as specified in spec Section 3.5.
- [ ] **Step 2: Verify import**
- [ ] **Step 3: Commit**
```bash
git add backend/app/modules/graph/
git commit -m "feat(graph): add GraphNode, GraphEdge, GraphGroup, GraphView domain entities"
```
---
## Task 18: MOD-GRAPH Application — GraphService
**Files:**
- Modify: `backend/app/modules/graph/application/services.py`
- Modify: `backend/app/modules/graph/application/__init__.py`
- Create: `backend/tests/test_graph_service.py`
- [ ] **Step 1: Write tests**
Test `build_panorama()`:
- Creates 5 groups (business, application, data, technology, cross-layer)
- Capabilities become nodes in business group
- Modules become nodes in application group
- Entities become nodes in data group
- RuntimeComponents become nodes in technology group
- TraceabilityLinks generate edges (capability→module, module→entity)
- Module.depends_on generates edges
- Integration generates edges
Test `get_neighbors()`:
- Returns only direct neighbor nodes and connecting edges
- Returns empty GraphView for unknown node_id
- [ ] **Step 2: Run tests — should fail**
- [ ] **Step 3: Implement services.py**
Build the panorama by iterating over ScanResult entities and constructing nodes, edges, and groups. See spec Section 3.5 for the 9-step algorithm.
For `get_neighbors()`: filter the full graph to only nodes that share an edge with the target node.
- [ ] **Step 4: Run tests — should pass**
- [ ] **Step 5: Commit**
```bash
git add backend/app/modules/graph/application/ backend/tests/test_graph_service.py
git commit -m "feat(graph): add GraphService — panorama construction and neighbor query"
```
---
## Task 19: MOD-GRAPH Interfaces — HTTP Router
**Files:**
- Modify: `backend/app/modules/graph/interfaces/http/router.py`
- Modify: `backend/app/main.py` (wire graph)
- Create: `backend/tests/test_api_graph.py`
- [ ] **Step 1: Write API tests**
- [ ] **Step 2: Implement router.py**
- `GET /projects/{project_id}/graph` → trigger scan if needed → build panorama → return GraphView JSON
- `GET /projects/{project_id}/graph/nodes/{node_id}/neighbors` → get neighbors → return GraphView JSON
- [ ] **Step 3: Wire graph into main.py**
- [ ] **Step 4: Run tests — should pass**
- [ ] **Step 5: Commit**
```bash
git add backend/app/modules/graph/ backend/app/main.py backend/tests/test_api_graph.py
git commit -m "feat(graph): add REST API — panorama and neighbor query endpoints"
```
---
## Task 20: MOD-EDITOR Domain + Infrastructure
**Files:**
- Modify: `backend/app/modules/editor/domain/entities.py`
- Create: `backend/app/modules/editor/infrastructure/file_io.py`
- [ ] **Step 1: Write editor domain entities**
`EditableFile`, `AffectedFile`, `ImpactResult` as specified in spec Section 3.6.
- [ ] **Step 2: Write file_io.py**
Read/write design files using `app.shared.infrastructure.filesystem`.
- [ ] **Step 3: Commit**
```bash
git add backend/app/modules/editor/
git commit -m "feat(editor): add domain entities and file I/O infrastructure"
```
---
## Task 21: MOD-EDITOR Application + Interfaces
**Files:**
- Modify: `backend/app/modules/editor/application/services.py`
- Modify: `backend/app/modules/editor/interfaces/http/router.py`
- Modify: `backend/app/main.py` (wire editor)
- Create: `backend/tests/test_editor_service.py`
- Create: `backend/tests/test_api_editor.py`
- [ ] **Step 1: Write EditorService tests**
Test:
- `get_file()` returns EditableFile with correct format detection
- `save_file()` writes content and triggers rescan
- `get_impact()` finds downstream affected files via DesignDocument relationships
- [ ] **Step 2: Implement EditorService**
```python
class EditorService:
def __init__(self, scan_service: ScanService) -> None:
self._scan_service = scan_service
def get_file(self, project: Project, relative_path: str) -> EditableFile:
... # Read file, detect format from extension, return EditableFile
def save_file(self, project: Project, relative_path: str, content: str) -> ScanResult:
... # Write file, trigger rescan, return new ScanResult
def get_impact(self, project: Project, relative_path: str, scan_result: ScanResult) -> ImpactResult:
... # Walk DesignDocument.downstream + TraceabilityLink to find affected files
```
- [ ] **Step 3: Write router.py and API tests**
- [ ] **Step 4: Wire into main.py**
- [ ] **Step 5: Run all tests — should pass**
- [ ] **Step 6: Commit**
```bash
git add backend/app/modules/editor/ backend/app/main.py backend/tests/test_editor_service.py backend/tests/test_api_editor.py
git commit -m "feat(editor): add EditorService and REST API — file read/write and impact analysis"
```
---
## Task 22: MOD-IMPL-TRACKER Domain + Infrastructure
**Files:**
- Modify: `backend/app/modules/impl_tracker/domain/entities.py`
- Create: `backend/app/modules/impl_tracker/infrastructure/code_scanner.py`
- Create: `backend/app/modules/impl_tracker/infrastructure/llm_client.py`
- [ ] **Step 1: Write domain entities**
`ImplProgress`, `CodeStructure` as specified in spec Section 3.7.
- [ ] **Step 2: Write code_scanner.py**
Scan a code directory → return CodeStructure (directories, files, matched modules by cross-referencing CodebaseAlignment).
- [ ] **Step 3: Write llm_client.py**
Stub LLM client that returns a placeholder response. Real LLM integration is optional — the system works without it (falls back to auto-scan only).
- [ ] **Step 4: Commit**
```bash
git add backend/app/modules/impl_tracker/
git commit -m "feat(impl_tracker): add domain entities and infrastructure — code scanner, LLM client stub"
```
---
## Task 23: MOD-IMPL-TRACKER Application + Interfaces
**Files:**
- Modify: `backend/app/modules/impl_tracker/application/services.py`
- Modify: `backend/app/modules/impl_tracker/interfaces/http/router.py`
- Modify: `backend/app/main.py` (wire impl_tracker)
- Create: `backend/tests/test_impl_tracker.py`
- Create: `backend/tests/test_api_impl_tracker.py`
- [ ] **Step 1: Write ImplTrackerService tests**
Test:
- `evaluate()` with code_dir=None returns empty list
- `evaluate()` with real code dir returns ImplProgress per module
- `set_manual_progress()` overrides auto value
- `get_progress()` returns cached results
- [ ] **Step 2: Implement ImplTrackerService**
Three-tier evaluation as per spec Section 3.7.
- [ ] **Step 3: Write router.py and API tests**
Endpoints:
- `POST /projects/{project_id}/impl-progress` → evaluate
- `GET /projects/{project_id}/impl-progress` → get cached
- `PUT /projects/{project_id}/impl-progress/{module_id}` → manual override
- [ ] **Step 4: Wire into main.py**
- [ ] **Step 5: Run all tests — should pass**
- [ ] **Step 6: Commit**
```bash
git add backend/app/modules/impl_tracker/ backend/app/main.py backend/tests/test_impl_tracker.py backend/tests/test_api_impl_tracker.py
git commit -m "feat(impl_tracker): add ImplTrackerService and REST API — auto/llm/manual progress evaluation"
```
---
## Task 24: Backend Integration Test — Full API
**Files:**
- No new files, run existing tests
- [ ] **Step 1: Run all backend tests**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run pytest -v`
Expected: All tests pass.
- [ ] **Step 2: Verify API starts and health responds**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && timeout 3 uv run uvicorn app.main:app --port 8900 &; sleep 2; curl -s http://localhost:8900/api/health; kill %1`
Expected: `{"status":"ok"}`
- [ ] **Step 3: Commit (if any fixes needed)**
---
## Task 25: Frontend Build Configuration
**Files:**
- Create: `frontend/package.json`
- Create: `frontend/vite.config.ts`
- Create: `frontend/tsconfig.json`
- Create: `frontend/tsconfig.node.json`
- Create: `frontend/index.html`
- [ ] **Step 1: Create package.json**
```json
{
"name": "arch-design-dashboard",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.0",
"d3": "^7.9.0",
"pinia": "^2.2.0",
"vue": "^3.5.0",
"vue-router": "^4.4.0"
},
"devDependencies": {
"@types/d3": "^7.4.0",
"@vitejs/plugin-vue": "^5.1.0",
"typescript": "~5.6.0",
"vite": "^6.0.0",
"vue-tsc": "^2.1.0"
}
}
```
- [ ] **Step 2: Create vite.config.ts**
```typescript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8900',
changeOrigin: true,
},
},
},
})
```
- [ ] **Step 3: Create tsconfig.json**
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"noEmit": true,
"paths": {
"@/*": ["./src/*"]
},
"baseUrl": "."
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
```
- [ ] **Step 4: Create tsconfig.node.json**
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["vite.config.ts"]
}
```
- [ ] **Step 5: Create index.html**
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Arch Design Dashboard</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
```
- [ ] **Step 6: Install dependencies**
Run: `cd /workspace/arch-design-agent-skill-dashboard/frontend && npm install`
Expected: Dependencies installed successfully.
- [ ] **Step 7: Commit**
```bash
git add frontend/package.json frontend/vite.config.ts frontend/tsconfig.json frontend/tsconfig.node.json frontend/index.html frontend/package-lock.json
git commit -m "build: add frontend configuration — Vite, TypeScript, Vue 3"
```
---
## Task 26: Frontend Shared Layer
**Files:**
- Modify: `frontend/src/main.ts`
- Modify: `frontend/src/App.vue`
- Modify: `frontend/src/shared/api.ts`
- Modify: `frontend/src/shared/types/api.ts`
- Modify: `frontend/src/router/index.ts`
- Create: `frontend/src/style.css`
- [ ] **Step 1: Write shared/types/api.ts**
Define all TypeScript interfaces matching the backend API response schemas:
`Project`, `ScanResult`, `ScanSummary`, `FileStatusEntry`, `GraphView`, `GraphNode`, `GraphEdge`, `GraphGroup`, `Capability`, `Module`, `Entity`, `Integration`, `ValueFlow`, `UserJourney`, `DataFlow`, `ExternalSystem`, `TraceabilityLink`, `RuntimeComponent`, `FileContent`, `ImpactResult`, `ImplProgress`, `CapabilityDetail`, `ModuleDetail`, `EntityDetail`.
- [ ] **Step 2: Write shared/api.ts**
```typescript
import axios from 'axios'
const api = axios.create({ baseURL: '/api' })
export default api
```
- [ ] **Step 3: Write router/index.ts**
3 routes: `/` → ProjectList, `/projects/:id` → GraphPanorama, `/projects/:id/editor` → EditorPage (Phase 2).
- [ ] **Step 4: Write main.ts**
Create Vue app, install Pinia and Router.
- [ ] **Step 5: Write App.vue**
Layout with sidebar (always visible project list) and main content area with `<router-view />`.
- [ ] **Step 6: Write style.css**
Basic CSS for app layout (sidebar + content grid), colors, typography.
- [ ] **Step 7: Verify build**
Run: `cd /workspace/arch-design-agent-skill-dashboard/frontend && npx vue-tsc --noEmit && npx vite build`
Expected: Build succeeds (components are empty stubs, but types/config should work).
- [ ] **Step 8: Commit**
```bash
git add frontend/src/
git commit -m "feat(frontend): add shared layer — types, API client, router, app layout"
```
---
## Task 27: MOD-FE-PROJECT — Project Management UI
**Files:**
- Modify: `frontend/src/modules/project/types/index.ts`
- Modify: `frontend/src/modules/project/api/index.ts`
- Modify: `frontend/src/modules/project/composables/useProject.ts`
- Modify: `frontend/src/modules/project/components/ProjectList.vue`
- Modify: `frontend/src/modules/project/components/ProjectOverview.vue`
- [ ] **Step 1: Write project types**
Re-export or extend shared Project type.
- [ ] **Step 2: Write project API functions**
`listProjects`, `createProject`, `getProject`, `deleteProject` — all calling backend via shared api client.
- [ ] **Step 3: Write useProject Pinia store**
State: `projects: Project[]`, `currentProject: Project | null`, `loading: boolean`, `error: string | null`.
Actions: `fetchProjects`, `createProject`, `selectProject`, `deleteProject`.
- [ ] **Step 4: Write ProjectOverview.vue**
Card component showing project name, design_dir, created_at. Click selects project. Delete button with confirmation.
- [ ] **Step 5: Write ProjectList.vue**
- List of ProjectOverview cards
- "Add Project" button → inline form (name + design_dir + optional code_dir)
- On create success → navigate to `/projects/:id`
- Empty state message when no projects
- [ ] **Step 6: Verify frontend builds**
Run: `cd /workspace/arch-design-agent-skill-dashboard/frontend && npx vue-tsc --noEmit`
- [ ] **Step 7: Commit**
```bash
git add frontend/src/modules/project/
git commit -m "feat(fe-project): add project management UI — list, create, delete"
```
---
## Task 28: MOD-FE-GRAPH — Graph Visualization
**Files:**
- Modify: `frontend/src/modules/graph/types/index.ts`
- Modify: `frontend/src/modules/graph/api/index.ts`
- Modify: `frontend/src/modules/graph/composables/useGraph.ts`
- Modify: `frontend/src/modules/graph/components/GraphPanorama.vue`
- Modify: `frontend/src/modules/graph/components/GraphDetail.vue`
- [ ] **Step 1: Write graph types**
Re-export GraphView, GraphNode, GraphEdge, GraphGroup types.
- [ ] **Step 2: Write graph API functions**
`triggerScan`, `getLatestScan`, `getGraph`, `getNodeNeighbors`, plus all entity query endpoints.
- [ ] **Step 3: Write useGraph Pinia store**
State: `graphView`, `selectedNode`, `scanResult`, `loading`.
Actions: `loadGraph` (triggerScan → getGraph), `selectNode`, `loadNeighbors`, `clearSelection`.
- [ ] **Step 4: Write GraphPanorama.vue**
This is the core visualization component:
- On mount: call `loadGraph(projectId)` → trigger scan → get graph data
- D3.js force simulation with group clustering
- Node rendering:
- Colors: ok=#4CAF50, sparse=#FFC107, missing=#F44336, template-residue=#FF9800, placeholder-heavy=#9C27B0, unknown=#9E9E9E
- Shapes: capability=circle, module=rect, entity=diamond, other=circle
- Edge rendering:
- traces_to=solid, depends_on=dashed, owns=thick solid
- Interactions:
- Hover → tooltip (id, type, status, label)
- Click → select node → show GraphDetail
- Double-click → load neighbors (drill-down)
- Zoom/pan via D3 zoom behavior
- Scan summary panel (top-right corner): total/ok/sparse/missing counts
- [ ] **Step 5: Write GraphDetail.vue**
Slide-out side panel:
- Node properties (all fields)
- Related entities list (clickable)
- Phase 2 placeholders: Edit button, Impact Analysis button
- [ ] **Step 6: Verify frontend builds**
- [ ] **Step 7: Commit**
```bash
git add frontend/src/modules/graph/
git commit -m "feat(fe-graph): add D3.js graph visualization — panorama, drill-down, status colors"
```
---
## Task 29: MOD-FE-EDITOR — File Editor UI (Phase 2)
**Files:**
- Modify: `frontend/src/modules/editor/types/index.ts`
- Modify: `frontend/src/modules/editor/api/index.ts`
- Modify: `frontend/src/modules/editor/composables/useEditor.ts`
- Modify: `frontend/src/modules/editor/components/CsvEditor.vue`
- Modify: `frontend/src/modules/editor/components/MdEditor.vue`
- [ ] **Step 1: Write editor types**
FileContent, ImpactResult, ImplProgress types.
- [ ] **Step 2: Write editor API functions**
`getFile`, `saveFile`, `getFileImpact`.
- [ ] **Step 3: Write useEditor Pinia store**
State: `currentFile`, `impactResult`, `saving`, `error`.
Actions: `loadFile`, `saveFile`, `analyzeImpact`.
- [ ] **Step 4: Write CsvEditor.vue**
- Parse CSV content into rows/columns
- Render as HTML `<table>` with contenteditable cells
- Add/remove row buttons
- Save button → serialize back to CSV string → call saveFile API
- On save success → trigger graph refresh
- [ ] **Step 5: Write MdEditor.vue**
- Split view: left textarea, right preview
- Render Markdown preview (basic: headers, lists, code blocks — use simple regex or a lightweight lib)
- Save button → call saveFile API
- [ ] **Step 6: Update router to add editor route**
Add `/projects/:id/editor` route pointing to an EditorPage that conditionally renders CsvEditor or MdEditor based on file format.
- [ ] **Step 7: Verify frontend builds**
- [ ] **Step 8: Commit**
```bash
git add frontend/src/modules/editor/ frontend/src/router/
git commit -m "feat(fe-editor): add CSV table editor and Markdown editor components"
```
---
## Task 30: Frontend Full Build Verification
- [ ] **Step 1: Run TypeScript check**
Run: `cd /workspace/arch-design-agent-skill-dashboard/frontend && npx vue-tsc --noEmit`
Expected: No errors
- [ ] **Step 2: Run production build**
Run: `cd /workspace/arch-design-agent-skill-dashboard/frontend && npx vite build`
Expected: Build succeeds, output in `dist/`
- [ ] **Step 3: Commit any fixes**
---
## Task 31: Docker Deployment Configuration
**Files:**
- Create: `docker-compose.yml`
- Create: `backend/Dockerfile`
- Create: `frontend/Dockerfile`
- Create: `frontend/nginx.conf`
- [ ] **Step 1: Create backend/Dockerfile**
```dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
COPY app/ app/
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8900"]
```
- [ ] **Step 2: Create frontend/Dockerfile**
```dockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
```
- [ ] **Step 3: Create frontend/nginx.conf**
```nginx
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass http://backend:8900;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
try_files $uri $uri/ /index.html;
}
}
```
- [ ] **Step 4: Create docker-compose.yml**
```yaml
services:
backend:
build: ./backend
ports:
- "8900:8900"
volumes:
- ${DESIGN_DIR:-.}:/data/design:rw
- ${CODE_DIR:-/dev/null}:/data/code:ro
- registry-data:/data/registry
environment:
- REGISTRY_PATH=/data/registry/projects.json
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
volumes:
registry-data:
```
- [ ] **Step 5: Commit**
```bash
git add docker-compose.yml backend/Dockerfile frontend/Dockerfile frontend/nginx.conf
git commit -m "build: add Docker deployment — Compose, Dockerfiles, Nginx config"
```
---
## Task 32: End-to-End Verification
- [ ] **Step 1: Run all backend tests**
Run: `cd /workspace/arch-design-agent-skill-dashboard/backend && uv run pytest -v`
Expected: All tests pass
- [ ] **Step 2: Run frontend build**
Run: `cd /workspace/arch-design-agent-skill-dashboard/frontend && npm run build`
Expected: Build succeeds
- [ ] **Step 3: Start backend and verify key API flows**
Start server, create a project pointing to this repo's own `design/` directory, trigger scan, get graph, verify entities are populated.
- [ ] **Step 4: Final commit if any fixes needed**