feat(impl_tracker): add ImplTrackerService and REST API — progress evaluation and manual override
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e523d5b31c
commit
b77bae709b
|
|
@ -15,6 +15,8 @@ from app.modules.graph.application.services import GraphService
|
||||||
from app.modules.graph.interfaces.http.router import router as graph_router, init_router as init_graph_router
|
from app.modules.graph.interfaces.http.router import router as graph_router, init_router as init_graph_router
|
||||||
from app.modules.editor.application.services import EditorService
|
from app.modules.editor.application.services import EditorService
|
||||||
from app.modules.editor.interfaces.http.router import router as editor_router, init_router as init_editor_router
|
from app.modules.editor.interfaces.http.router import router as editor_router, init_router as init_editor_router
|
||||||
|
from app.modules.impl_tracker.application.services import ImplTrackerService
|
||||||
|
from app.modules.impl_tracker.interfaces.http.router import router as impl_tracker_router, init_router as init_impl_tracker_router
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> FastAPI:
|
def create_app() -> FastAPI:
|
||||||
|
|
@ -40,11 +42,16 @@ def create_app() -> FastAPI:
|
||||||
editor_service = EditorService(scan_service)
|
editor_service = EditorService(scan_service)
|
||||||
init_editor_router(project_service, scan_service, editor_service)
|
init_editor_router(project_service, scan_service, editor_service)
|
||||||
|
|
||||||
|
# Wire Impl-tracker module
|
||||||
|
impl_tracker_service = ImplTrackerService()
|
||||||
|
init_impl_tracker_router(project_service, scan_service, impl_tracker_service)
|
||||||
|
|
||||||
# Register routers
|
# Register routers
|
||||||
app.include_router(project_router, prefix="/api")
|
app.include_router(project_router, prefix="/api")
|
||||||
app.include_router(scanner_router, prefix="/api")
|
app.include_router(scanner_router, prefix="/api")
|
||||||
app.include_router(graph_router, prefix="/api")
|
app.include_router(graph_router, prefix="/api")
|
||||||
app.include_router(editor_router, prefix="/api")
|
app.include_router(editor_router, prefix="/api")
|
||||||
|
app.include_router(impl_tracker_router, prefix="/api")
|
||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
@app.get("/api/health")
|
@app.get("/api/health")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from app.modules.impl_tracker.domain.entities import ImplProgress
|
||||||
|
from app.modules.impl_tracker.infrastructure.code_scanner import scan_code_directory
|
||||||
|
from app.modules.project.domain.entities import Project
|
||||||
|
from app.modules.scanner.domain.entities import ScanResult
|
||||||
|
|
||||||
|
|
||||||
|
class ImplTrackerService:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._cache: dict[str, list[ImplProgress]] = {}
|
||||||
|
self._manual_overrides: dict[str, dict[str, float]] = {} # project_id -> {module_id: percentage}
|
||||||
|
|
||||||
|
def evaluate(self, project: Project, scan_result: ScanResult) -> list[ImplProgress]:
|
||||||
|
progress_list: list[ImplProgress] = []
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
if not project.code_dir:
|
||||||
|
# No code dir -> all modules at 0%
|
||||||
|
for mod in scan_result.modules:
|
||||||
|
progress_list.append(ImplProgress(
|
||||||
|
module_id=mod.module_id, percentage=0.0, source="auto", evaluated_at=now,
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
code_structure = scan_code_directory(project.code_dir, scan_result)
|
||||||
|
for mod in scan_result.modules:
|
||||||
|
if mod.module_id in code_structure.matched_modules:
|
||||||
|
percentage = 50.0 # Basic: module directory exists
|
||||||
|
else:
|
||||||
|
percentage = 0.0
|
||||||
|
progress_list.append(ImplProgress(
|
||||||
|
module_id=mod.module_id, percentage=percentage, source="auto", evaluated_at=now,
|
||||||
|
))
|
||||||
|
|
||||||
|
# Apply manual overrides
|
||||||
|
overrides = self._manual_overrides.get(project.id, {})
|
||||||
|
for p in progress_list:
|
||||||
|
if p.module_id in overrides:
|
||||||
|
p.percentage = overrides[p.module_id]
|
||||||
|
p.source = "manual"
|
||||||
|
|
||||||
|
self._cache[project.id] = progress_list
|
||||||
|
return progress_list
|
||||||
|
|
||||||
|
def get_progress(self, project_id: str) -> list[ImplProgress] | None:
|
||||||
|
return self._cache.get(project_id)
|
||||||
|
|
||||||
|
def set_manual_progress(self, project_id: str, module_id: str, percentage: float) -> None:
|
||||||
|
if project_id not in self._manual_overrides:
|
||||||
|
self._manual_overrides[project_id] = {}
|
||||||
|
self._manual_overrides[project_id][module_id] = percentage
|
||||||
|
# Update cache if exists
|
||||||
|
if project_id in self._cache:
|
||||||
|
for p in self._cache[project_id]:
|
||||||
|
if p.module_id == module_id:
|
||||||
|
p.percentage = percentage
|
||||||
|
p.source = "manual"
|
||||||
|
p.evaluated_at = datetime.now(timezone.utc)
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
"""Impl-tracker HTTP router — progress evaluation and manual override endpoints."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from app.modules.impl_tracker.application.services import ImplTrackerService
|
||||||
|
from app.modules.impl_tracker.domain.entities import ImplProgress
|
||||||
|
from app.modules.project.application.services import ProjectService
|
||||||
|
from app.modules.scanner.application.services import ScanService
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/projects/{project_id}/impl-progress", tags=["impl-tracker"])
|
||||||
|
|
||||||
|
_project_service: ProjectService | None = None
|
||||||
|
_scan_service: ScanService | None = None
|
||||||
|
_impl_tracker_service: ImplTrackerService | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def init_router(
|
||||||
|
project_service: ProjectService,
|
||||||
|
scan_service: ScanService,
|
||||||
|
impl_tracker_service: ImplTrackerService,
|
||||||
|
) -> None:
|
||||||
|
global _project_service, _scan_service, _impl_tracker_service
|
||||||
|
_project_service = project_service
|
||||||
|
_scan_service = scan_service
|
||||||
|
_impl_tracker_service = impl_tracker_service
|
||||||
|
|
||||||
|
|
||||||
|
class ManualProgressRequest(BaseModel):
|
||||||
|
percentage: float
|
||||||
|
|
||||||
|
|
||||||
|
def _progress_to_dict(p) -> dict:
|
||||||
|
d = asdict(p)
|
||||||
|
d["evaluated_at"] = p.evaluated_at.isoformat()
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("")
|
||||||
|
def evaluate_progress(project_id: str):
|
||||||
|
"""Evaluate implementation progress for all modules."""
|
||||||
|
project = _project_service.get_project(project_id)
|
||||||
|
scan_result = _scan_service.get_latest_scan(project_id)
|
||||||
|
if scan_result is None:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=404,
|
||||||
|
content={"detail": "No scan available. Run POST /scan first."},
|
||||||
|
)
|
||||||
|
progress = _impl_tracker_service.evaluate(project, scan_result)
|
||||||
|
return [_progress_to_dict(p) for p in progress]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
def get_progress(project_id: str):
|
||||||
|
"""Get cached implementation progress."""
|
||||||
|
_project_service.get_project(project_id) # Ensure project exists (raises 404)
|
||||||
|
progress = _impl_tracker_service.get_progress(project_id)
|
||||||
|
if progress is None:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=404,
|
||||||
|
content={"detail": "No progress evaluated yet. Run POST /impl-progress first."},
|
||||||
|
)
|
||||||
|
return [_progress_to_dict(p) for p in progress]
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{module_id}")
|
||||||
|
def set_manual_progress(project_id: str, module_id: str, body: ManualProgressRequest):
|
||||||
|
"""Set manual progress override for a module."""
|
||||||
|
_project_service.get_project(project_id) # Ensure project exists (raises 404)
|
||||||
|
_impl_tracker_service.set_manual_progress(project_id, module_id, body.percentage)
|
||||||
|
|
||||||
|
# Return the updated progress entry
|
||||||
|
progress = _impl_tracker_service.get_progress(project_id)
|
||||||
|
if progress:
|
||||||
|
for p in progress:
|
||||||
|
if p.module_id == module_id:
|
||||||
|
return _progress_to_dict(p)
|
||||||
|
|
||||||
|
# If no cached progress, return a constructed response
|
||||||
|
return _progress_to_dict(
|
||||||
|
ImplProgress(
|
||||||
|
module_id=module_id,
|
||||||
|
percentage=body.percentage,
|
||||||
|
source="manual",
|
||||||
|
evaluated_at=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
)
|
||||||
46
backend/tests/test_api_impl_tracker.py
Normal file
46
backend/tests/test_api_impl_tracker.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
DESIGN_DIR = "/workspace/arch-design-agent-skill-dashboard/design"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def project_id(client):
|
||||||
|
r = client.post("/api/projects", json={"name": "test", "design_dir": DESIGN_DIR})
|
||||||
|
return r.json()["id"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_evaluate_progress(client, project_id):
|
||||||
|
# Need to scan first
|
||||||
|
client.post(f"/api/projects/{project_id}/scan")
|
||||||
|
r = client.post(f"/api/projects/{project_id}/impl-progress")
|
||||||
|
assert r.status_code == 200
|
||||||
|
data = r.json()
|
||||||
|
assert isinstance(data, list)
|
||||||
|
assert len(data) > 0
|
||||||
|
assert "module_id" in data[0]
|
||||||
|
assert "percentage" in data[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_progress_not_evaluated(client, project_id):
|
||||||
|
r = client.get(f"/api/projects/{project_id}/impl-progress")
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_progress_after_evaluate(client, project_id):
|
||||||
|
client.post(f"/api/projects/{project_id}/scan")
|
||||||
|
client.post(f"/api/projects/{project_id}/impl-progress")
|
||||||
|
r = client.get(f"/api/projects/{project_id}/impl-progress")
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert isinstance(r.json(), list)
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_manual_progress(client, project_id):
|
||||||
|
client.post(f"/api/projects/{project_id}/scan")
|
||||||
|
client.post(f"/api/projects/{project_id}/impl-progress")
|
||||||
|
r = client.put(
|
||||||
|
f"/api/projects/{project_id}/impl-progress/MOD-PROJECT",
|
||||||
|
json={"percentage": 80.0},
|
||||||
|
)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["percentage"] == 80.0
|
||||||
|
assert r.json()["source"] == "manual"
|
||||||
59
backend/tests/test_impl_tracker.py
Normal file
59
backend/tests/test_impl_tracker.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import pytest
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app.modules.impl_tracker.application.services import ImplTrackerService
|
||||||
|
from app.modules.project.domain.entities import Project
|
||||||
|
from app.modules.scanner.application.services import ScanService
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def impl_service():
|
||||||
|
return ImplTrackerService()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def scan_result():
|
||||||
|
svc = ScanService()
|
||||||
|
project = Project(
|
||||||
|
id="test", name="test",
|
||||||
|
design_dir="/workspace/arch-design-agent-skill-dashboard/design",
|
||||||
|
code_dir=None, created_at=datetime(2026, 1, 1),
|
||||||
|
)
|
||||||
|
return svc.scan(project)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_project():
|
||||||
|
return Project(
|
||||||
|
id="test", name="test",
|
||||||
|
design_dir="/workspace/arch-design-agent-skill-dashboard/design",
|
||||||
|
code_dir=None, created_at=datetime(2026, 1, 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_evaluate_no_code_dir(impl_service, test_project, scan_result):
|
||||||
|
progress = impl_service.evaluate(test_project, scan_result)
|
||||||
|
assert len(progress) > 0
|
||||||
|
assert all(p.percentage == 0.0 for p in progress)
|
||||||
|
assert all(p.source == "auto" for p in progress)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_progress_before_evaluate(impl_service):
|
||||||
|
assert impl_service.get_progress("nonexistent") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_progress_after_evaluate(impl_service, test_project, scan_result):
|
||||||
|
impl_service.evaluate(test_project, scan_result)
|
||||||
|
cached = impl_service.get_progress("test")
|
||||||
|
assert cached is not None
|
||||||
|
assert len(cached) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_manual_progress(impl_service, test_project, scan_result):
|
||||||
|
impl_service.evaluate(test_project, scan_result)
|
||||||
|
impl_service.set_manual_progress("test", "MOD-PROJECT", 75.0)
|
||||||
|
cached = impl_service.get_progress("test")
|
||||||
|
mod_progress = [p for p in cached if p.module_id == "MOD-PROJECT"]
|
||||||
|
assert len(mod_progress) == 1
|
||||||
|
assert mod_progress[0].percentage == 75.0
|
||||||
|
assert mod_progress[0].source == "manual"
|
||||||
Loading…
Reference in New Issue
Block a user