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>
2317 lines
71 KiB
Markdown
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**
|