diff --git a/src/repo_registry/core/models.py b/src/repo_registry/core/models.py
index 299f4e6..99c6cd7 100644
--- a/src/repo_registry/core/models.py
+++ b/src/repo_registry/core/models.py
@@ -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
diff --git a/src/repo_registry/core/service.py b/src/repo_registry/core/service.py
index df70c06..fa794d9 100644
--- a/src/repo_registry/core/service.py
+++ b/src/repo_registry/core/service.py
@@ -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,
diff --git a/src/repo_registry/storage/sqlite.py b/src/repo_registry/storage/sqlite.py
index 465b7d1..5120a5f 100644
--- a/src/repo_registry/storage/sqlite.py
+++ b/src/repo_registry/storage/sqlite.py
@@ -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,
diff --git a/src/repo_registry/web_api/app.py b/src/repo_registry/web_api/app.py
index 57383e6..f49563e 100644
--- a/src/repo_registry/web_api/app.py
+++ b/src/repo_registry/web_api/app.py
@@ -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,
diff --git a/src/repo_registry/web_ui/views.py b/src/repo_registry/web_ui/views.py
index c45757b..84ecb14 100644
--- a/src/repo_registry/web_ui/views.py
+++ b/src/repo_registry/web_ui/views.py
@@ -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))}
+ Review Decisions
+ {render_review_decisions(decisions)}
+
{language_pills}{framework_pills}
' +def render_review_decisions(decisions: list) -> str: + if not decisions: + return 'No review decisions yet.
' + rows = "\n".join( + f""" +| Action | Created | Notes |
|---|