Observed Facts
@@ -308,7 +308,29 @@ def approve_candidate_graph_from_form(
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
-def render_candidate_graph(graph: dict) -> str:
+@router.post(
+ "/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}"
+ "/candidate-abilities/{candidate_ability_id}/reject"
+)
+def reject_candidate_ability_from_form(
+ repository_id: int,
+ analysis_run_id: int,
+ candidate_ability_id: int,
+ service: RegistryService = Depends(get_service),
+) -> RedirectResponse:
+ service.reject_candidate_ability(
+ repository_id,
+ analysis_run_id,
+ candidate_ability_id,
+ notes="Rejected from web UI",
+ )
+ return RedirectResponse(
+ f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}",
+ status_code=303,
+ )
+
+
+def render_candidate_graph(graph: dict, repository_id: int, analysis_run_id: int) -> str:
abilities = graph.get("abilities", [])
if not abilities:
return 'No candidates generated.
'
@@ -321,6 +343,7 @@ def render_candidate_graph(graph: dict) -> str:
{escape(ability['name'])}
{escape(ability['status'])}
{ability['confidence']:.2f}
+ {render_candidate_ability_actions(ability, repository_id, analysis_run_id)}
{escape(ability['description'])}
{render_sources(ability['source_refs'])}
@@ -330,6 +353,24 @@ def render_candidate_graph(graph: dict) -> str:
return f''
+def render_candidate_ability_actions(
+ ability: dict,
+ repository_id: int,
+ analysis_run_id: int,
+) -> str:
+ if ability["status"] != "candidate":
+ return ""
+ action = (
+ f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}"
+ f"/candidate-abilities/{ability['id']}/reject"
+ )
+ return f"""
+
+ """
+
+
def render_candidate_capability(capability: dict) -> str:
features = "".join(
f'{escape(feature["name"])} {escape(feature["type"])} {escape(feature["location"])}'
diff --git a/tests/test_registry_service.py b/tests/test_registry_service.py
index df59f65..5669f57 100644
--- a/tests/test_registry_service.py
+++ b/tests/test_registry_service.py
@@ -206,6 +206,43 @@ def test_approve_candidate_graph_publishes_ability_map_once(tmp_path):
assert candidate_graph.abilities[0].status == "approved"
+def test_reject_candidate_ability_excludes_it_from_approval(tmp_path):
+ source = tmp_path / "repo"
+ source.mkdir()
+ (source / "README.md").write_text("# Rejectable\n", encoding="utf-8")
+ (source / "app.py").write_text(
+ "from fastapi import FastAPI\n"
+ "app = FastAPI()\n"
+ '@app.get("/health")\n'
+ "def health():\n"
+ " return {}\n",
+ encoding="utf-8",
+ )
+
+ service = make_service(tmp_path)
+ repository = service.register_repository(name="Rejectable", url=str(source))
+ summary = service.analyze_repository(repository.id)
+ graph = service.candidate_graph(repository.id, summary.analysis_run.id)
+ candidate = graph.abilities[0]
+
+ rejected_graph = service.reject_candidate_ability(
+ repository.id,
+ summary.analysis_run.id,
+ candidate.id,
+ notes="Too generic.",
+ )
+ ability_map = service.approve_candidate_graph(
+ repository.id,
+ summary.analysis_run.id,
+ )
+
+ assert service.get_repository(repository.id).status == "reviewing"
+ assert rejected_graph.abilities[0].status == "rejected"
+ assert rejected_graph.abilities[0].capabilities[0].status == "rejected"
+ assert rejected_graph.abilities[0].capabilities[0].features[0].status == "rejected"
+ assert ability_map.abilities == []
+
+
def test_analyze_repository_failure_is_recorded(tmp_path):
service = make_service(tmp_path)
repository = service.register_repository(
diff --git a/tests/test_web_api.py b/tests/test_web_api.py
index 2ff6b44..d37526f 100644
--- a/tests/test_web_api.py
+++ b/tests/test_web_api.py
@@ -138,6 +138,20 @@ def test_api_analysis_run_loop(tmp_path):
assert candidate_graph["abilities"][0]["name"] == (
"Review Frontend Repository Usefulness"
)
+ candidate_ability_id = candidate_graph["abilities"][0]["id"]
+
+ reject_response = client.post(
+ f"/repos/{repository_id}/analysis-runs/"
+ f"{run['analysis_run']['id']}/candidate-abilities/"
+ f"{candidate_ability_id}/reject",
+ json={"notes": "Reject once to exercise review correction."},
+ )
+ assert reject_response.status_code == 200
+ assert reject_response.json()["abilities"][0]["status"] == "rejected"
+
+ run_response = client.post(f"/repos/{repository_id}/analysis-runs", json={})
+ assert run_response.status_code == 201
+ run = run_response.json()
approve_response = client.post(
f"/repos/{repository_id}/analysis-runs/"