generated from coulomb/repo-seed
ReviewDecision domain model
This commit is contained in:
@@ -36,6 +36,16 @@ class AnalysisRun:
|
||||
scanner_version: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReviewDecision:
|
||||
id: int
|
||||
repository_id: int
|
||||
analysis_run_id: int | None
|
||||
action: str
|
||||
notes: str
|
||||
created_at: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ObservedFact:
|
||||
id: int
|
||||
|
||||
@@ -10,6 +10,7 @@ from repo_registry.core.models import (
|
||||
ObservedFact,
|
||||
Repository,
|
||||
RepositoryAbilityMap,
|
||||
ReviewDecision,
|
||||
ScanSummary,
|
||||
SearchResult,
|
||||
)
|
||||
@@ -112,6 +113,13 @@ class RegistryService:
|
||||
def list_capabilities(self) -> list[CapabilitySummary]:
|
||||
return self.store.list_capabilities()
|
||||
|
||||
def list_review_decisions(
|
||||
self,
|
||||
repository_id: int,
|
||||
analysis_run_id: int | None = None,
|
||||
) -> list[ReviewDecision]:
|
||||
return self.store.list_review_decisions(repository_id, analysis_run_id)
|
||||
|
||||
def list_observed_facts(
|
||||
self,
|
||||
repository_id: int,
|
||||
|
||||
@@ -21,6 +21,7 @@ from repo_registry.core.models import (
|
||||
Repository,
|
||||
RepositoryAbilityMap,
|
||||
RepositorySnapshot,
|
||||
ReviewDecision,
|
||||
SearchResult,
|
||||
SourceReference,
|
||||
)
|
||||
@@ -929,6 +930,40 @@ class RegistryStore:
|
||||
)
|
||||
return int(cursor.lastrowid)
|
||||
|
||||
def list_review_decisions(
|
||||
self,
|
||||
repository_id: int,
|
||||
analysis_run_id: int | None = None,
|
||||
) -> list[ReviewDecision]:
|
||||
self.get_repository(repository_id)
|
||||
params: tuple[int, ...]
|
||||
where = "WHERE repository_id = ?"
|
||||
params = (repository_id,)
|
||||
if analysis_run_id is not None:
|
||||
where += " AND analysis_run_id = ?"
|
||||
params = (repository_id, analysis_run_id)
|
||||
with self.connect() as connection:
|
||||
rows = connection.execute(
|
||||
f"""
|
||||
SELECT id, repository_id, analysis_run_id, action, notes, created_at
|
||||
FROM review_decisions
|
||||
{where}
|
||||
ORDER BY created_at DESC, id DESC
|
||||
""",
|
||||
params,
|
||||
).fetchall()
|
||||
return [
|
||||
ReviewDecision(
|
||||
id=row["id"],
|
||||
repository_id=row["repository_id"],
|
||||
analysis_run_id=row["analysis_run_id"],
|
||||
action=row["action"],
|
||||
notes=row["notes"],
|
||||
created_at=row["created_at"],
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
def fail_analysis_run(
|
||||
self,
|
||||
repository_id: int,
|
||||
|
||||
@@ -344,6 +344,38 @@ def get_analysis_run(
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@app.get("/repos/{repository_id}/review-decisions")
|
||||
def list_repository_review_decisions(
|
||||
repository_id: int,
|
||||
service: RegistryService = Depends(get_service),
|
||||
) -> list[dict[str, object]]:
|
||||
try:
|
||||
return [
|
||||
asdict(decision)
|
||||
for decision in service.list_review_decisions(repository_id)
|
||||
]
|
||||
except NotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@app.get("/repos/{repository_id}/analysis-runs/{analysis_run_id}/review-decisions")
|
||||
def list_analysis_run_review_decisions(
|
||||
repository_id: int,
|
||||
analysis_run_id: int,
|
||||
service: RegistryService = Depends(get_service),
|
||||
) -> list[dict[str, object]]:
|
||||
try:
|
||||
return [
|
||||
asdict(decision)
|
||||
for decision in service.list_review_decisions(
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
)
|
||||
]
|
||||
except NotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@app.get("/repos/{repository_id}/observed-facts")
|
||||
def list_observed_facts(
|
||||
repository_id: int,
|
||||
|
||||
@@ -267,6 +267,7 @@ def repository_detail(
|
||||
repository = service.get_repository(repository_id)
|
||||
runs = service.list_analysis_runs(repository_id)
|
||||
ability_map = service.ability_map(repository_id)
|
||||
decisions = service.list_review_decisions(repository_id)
|
||||
facts = service.list_observed_facts(repository_id)
|
||||
languages = sorted({fact.name for fact in facts if fact.kind == "language"})
|
||||
frameworks = sorted({fact.name for fact in facts if fact.kind == "framework"})
|
||||
@@ -307,6 +308,10 @@ def repository_detail(
|
||||
{render_ability_map(asdict(ability_map))}
|
||||
</section>
|
||||
</div>
|
||||
<section class="panel" style="margin-top:18px">
|
||||
<h2>Review Decisions</h2>
|
||||
{render_review_decisions(decisions)}
|
||||
</section>
|
||||
"""
|
||||
return page(repository.name, body)
|
||||
|
||||
@@ -336,6 +341,7 @@ def analysis_run_detail(
|
||||
repository = service.get_repository(repository_id)
|
||||
candidate_graph = service.candidate_graph(repository_id, analysis_run_id)
|
||||
facts = service.list_observed_facts(repository_id, analysis_run_id)
|
||||
decisions = service.list_review_decisions(repository_id, analysis_run_id)
|
||||
fact_rows = "\n".join(
|
||||
f"""
|
||||
<tr>
|
||||
@@ -368,6 +374,8 @@ def analysis_run_detail(
|
||||
<thead><tr><th>Kind</th><th>Name</th><th>Path</th><th>Value</th></tr></thead>
|
||||
<tbody>{fact_rows or '<tr><td colspan="4" class="muted">No observed facts.</td></tr>'}</tbody>
|
||||
</table>
|
||||
<h2>Review Decisions</h2>
|
||||
{render_review_decisions(decisions)}
|
||||
</section>
|
||||
</div>
|
||||
"""
|
||||
@@ -743,6 +751,27 @@ def render_repository_facts(languages: list[str], frameworks: list[str]) -> str:
|
||||
return f'<p class="actions">{language_pills}{framework_pills}</p>'
|
||||
|
||||
|
||||
def render_review_decisions(decisions: list) -> str:
|
||||
if not decisions:
|
||||
return '<p class="muted">No review decisions yet.</p>'
|
||||
rows = "\n".join(
|
||||
f"""
|
||||
<tr>
|
||||
<td>{escape(decision.action)}</td>
|
||||
<td class="source">{escape(decision.created_at)}</td>
|
||||
<td>{escape(decision.notes)}</td>
|
||||
</tr>
|
||||
"""
|
||||
for decision in decisions
|
||||
)
|
||||
return f"""
|
||||
<table>
|
||||
<thead><tr><th>Action</th><th>Created</th><th>Notes</th></tr></thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</table>
|
||||
"""
|
||||
|
||||
|
||||
def render_candidate_ability_actions(
|
||||
ability: dict,
|
||||
repository_id: int,
|
||||
|
||||
@@ -303,6 +303,9 @@ def test_approve_candidate_graph_publishes_ability_map_once(tmp_path):
|
||||
|
||||
candidate_graph = service.candidate_graph(repository.id, summary.analysis_run.id)
|
||||
assert candidate_graph.abilities[0].status == "approved"
|
||||
decisions = service.list_review_decisions(repository.id, summary.analysis_run.id)
|
||||
assert decisions[0].action == "approve_candidate_graph"
|
||||
assert decisions[0].notes == "Looks good for the first pass."
|
||||
|
||||
|
||||
def test_reject_candidate_ability_excludes_it_from_approval(tmp_path):
|
||||
|
||||
@@ -155,6 +155,17 @@ def test_api_analysis_run_loop(tmp_path):
|
||||
)
|
||||
assert reject_response.status_code == 200
|
||||
assert reject_response.json()["abilities"][0]["status"] == "rejected"
|
||||
decisions_response = client.get(f"/repos/{repository_id}/review-decisions")
|
||||
assert decisions_response.status_code == 200
|
||||
assert decisions_response.json()[0]["action"] == "reject_candidate_ability"
|
||||
run_decisions_response = client.get(
|
||||
f"/repos/{repository_id}/analysis-runs/"
|
||||
f"{run['analysis_run']['id']}/review-decisions"
|
||||
)
|
||||
assert run_decisions_response.status_code == 200
|
||||
assert run_decisions_response.json()[0]["notes"] == (
|
||||
"Reject once to exercise review correction."
|
||||
)
|
||||
|
||||
run_response = client.post(f"/repos/{repository_id}/analysis-runs", json={})
|
||||
assert run_response.status_code == 201
|
||||
@@ -302,6 +313,7 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
|
||||
assert run_detail.status_code == 200
|
||||
assert "Candidate Graph" in run_detail.text
|
||||
assert "ID " in run_detail.text
|
||||
assert "No review decisions yet." in run_detail.text
|
||||
|
||||
approve_response = client.post(
|
||||
f"{run_path}/candidate-graph/approve",
|
||||
@@ -316,6 +328,7 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
|
||||
assert "Language: Python" in approved_detail.text
|
||||
assert "Framework: FastAPI" in approved_detail.text
|
||||
assert "interface:app.py:3" in approved_detail.text
|
||||
assert "approve_candidate_graph" in approved_detail.text
|
||||
|
||||
search_response = client.get("/ui/search", params={"q": "repository"})
|
||||
assert search_response.status_code == 200
|
||||
|
||||
Reference in New Issue
Block a user