@@ -1048,6 +1210,7 @@ def repository_element_listing(
class_filter: str = Query(""),
entry_filter: str = Query(""),
candidate_status_filter: str = Query("active"),
+ support_orientation_filter: str = Query(""),
analysis_run_id: int | None = Query(default=None),
service: RegistryService = Depends(get_service),
) -> HTMLResponse:
@@ -1097,6 +1260,7 @@ def repository_element_listing(
q,
class_filter,
candidate_status_filter="all",
+ support_orientation_filter=support_orientation_filter,
)
rows = render_element_rows(
filtered,
@@ -1121,6 +1285,7 @@ def repository_element_listing(
Class
{render_entry_filter(entry_filter) if scope != "facts" else ""}
{render_candidate_status_filter(candidate_status_filter) if scope != "facts" else ""}
+ {render_support_orientation_filter(support_orientation_filter) if type in {"supports", "evidence"} else ""}
{render_class_datalist(entry_scoped_elements)}
@@ -2204,17 +2369,21 @@ def filter_element_rows(
class_filter: str,
entry_filter: str = "",
candidate_status_filter: str = "",
+ support_orientation_filter: str = "",
) -> list[dict]:
query = query.strip().lower()
class_filter = class_filter.strip().lower()
entry_filter = entry_filter.strip().lower()
candidate_status_filter = candidate_status_filter.strip().lower()
+ support_orientation_filter = support_orientation_filter.strip().lower()
filtered = []
for row in rows:
if entry_filter and row.get("entry_state", "").lower() != entry_filter:
continue
if not candidate_status_matches(row, candidate_status_filter):
continue
+ if support_orientation_filter and support_orientation_label(row) != support_orientation_filter:
+ continue
row_class = str(row["primary_class"]).lower()
if class_filter and class_filter not in row_class:
continue
@@ -2529,6 +2698,27 @@ def render_candidate_status_filter(candidate_status_filter: str) -> str:
"""
+def render_support_orientation_filter(support_orientation_filter: str) -> str:
+ options = [
+ ("", "Any orientation"),
+ ("downward support", "Downward support"),
+ ("same-level support review", "Same-level review"),
+ ("upward support review", "Upward review"),
+ ("unclassified support", "Unclassified"),
+ ]
+ rendered_options = "".join(
+ f'{escape(label)} '
+ for value, label in options
+ )
+ return f"""
+ Support orientation
+
+ {rendered_options}
+
+
+ """
+
+
def render_element_edit_fields(row: dict) -> str:
item_kind = row["item_kind"]
name = escape(str(row["name"]))
diff --git a/tests/test_web_api.py b/tests/test_web_api.py
index 2ac55b7..7490acb 100644
--- a/tests/test_web_api.py
+++ b/tests/test_web_api.py
@@ -1167,6 +1167,8 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
run_detail = client.get(run_path)
assert run_detail.status_code == 200
+ assert "Run Diagnostics" in run_detail.text
+ assert "Analysis completed with reviewable results." in run_detail.text
assert "Candidate Graph" in run_detail.text
assert "1 abilities" in run_detail.text
assert "2 capabilities" in run_detail.text
@@ -1538,6 +1540,64 @@ def test_ui_element_listing_hides_rejected_candidates_by_default(tmp_path):
app.dependency_overrides.clear()
+def test_ui_analysis_run_diagnostics_explain_failures_and_empty_results(tmp_path):
+ empty_source = tmp_path / "empty-repo"
+ empty_source.mkdir()
+
+ def override_settings():
+ return Settings(
+ database_path=str(tmp_path / "ui-diagnostics.sqlite3"),
+ checkout_root=str(tmp_path / "ui-diagnostics-checkouts"),
+ )
+
+ app.dependency_overrides[get_settings] = override_settings
+ client = TestClient(app)
+ try:
+ create_response = client.post(
+ "/repos",
+ json={
+ "url": str(empty_source),
+ "name": "Diagnostics Repo",
+ "description": "Used for UI diagnostics.",
+ "branch": "main",
+ },
+ )
+ assert create_response.status_code == 201
+ repository_id = create_response.json()["id"]
+
+ failed_run = client.post(
+ f"/ui/repos/{repository_id}/analysis-runs",
+ data={
+ "source_path": str(tmp_path / "missing-repo"),
+ "use_llm_assistance": "",
+ },
+ follow_redirects=False,
+ )
+ assert failed_run.status_code == 303
+ failed_detail = client.get(failed_run.headers["location"])
+ assert failed_detail.status_code == 200
+ assert "Run Diagnostics" in failed_detail.text
+ assert "Analysis failed." in failed_detail.text
+ assert "Verify the local path or Git URL" in failed_detail.text
+
+ empty_run = client.post(
+ f"/ui/repos/{repository_id}/analysis-runs",
+ data={
+ "source_path": "",
+ "use_llm_assistance": "",
+ },
+ follow_redirects=False,
+ )
+ assert empty_run.status_code == 303
+ empty_detail = client.get(empty_run.headers["location"])
+ assert empty_detail.status_code == 200
+ assert "No observed facts were found." in empty_detail.text
+ assert "0 facts" in empty_detail.text
+ assert "0 abilities" in empty_detail.text
+ finally:
+ app.dependency_overrides.clear()
+
+
def test_ui_register_and_explore_lands_on_analysis_result(tmp_path):
source = tmp_path / "explore-repo"
source.mkdir()
@@ -1715,6 +1775,21 @@ def test_ui_manual_registry_entry_loop(tmp_path):
)
assert evidence_response.status_code == 303
+ upward_support_response = client.post(
+ f"{repository_path}/evidence",
+ data={
+ "capability_id": str(capability_id),
+ "target_kind": "feature",
+ "target_id": "999",
+ "type": "review-smell",
+ "reference": "Manual Capability",
+ "reference_kind": "capability",
+ "strength": "weak",
+ },
+ follow_redirects=False,
+ )
+ assert upward_support_response.status_code == 303
+
detail_response = client.get(repository_path)
assert "Manual Ability" in detail_response.text
assert "Manual Capability" in detail_response.text
@@ -1723,6 +1798,7 @@ def test_ui_manual_registry_entry_loop(tmp_path):
assert "supports capability" in detail_response.text
assert "references source" in detail_response.text
assert "downward support" in detail_response.text
+ assert "upward support review" in detail_response.text
assert "ID " in detail_response.text
assert "Save Ability" in detail_response.text
@@ -1789,6 +1865,21 @@ def test_ui_manual_registry_entry_loop(tmp_path):
assert f"references feature #{feature_id}" in detail_response.text
assert "downward support" in detail_response.text
+ upward_support_listing = client.get(
+ f"/ui/repos/{repository_id}/elements",
+ params={
+ "scope": "all",
+ "entry_filter": "approved",
+ "type": "supports",
+ "support_orientation_filter": "upward support review",
+ },
+ )
+ assert upward_support_listing.status_code == 200
+ assert "Support orientation" in upward_support_listing.text
+ assert "1 of 2 shown" in upward_support_listing.text
+ assert "review-smell" in upward_support_listing.text
+ assert "tests/test_manual.py" not in upward_support_listing.text
+
delete_feature_response = client.post(
f"{repository_path}/features/{feature_id}/delete",
follow_redirects=False,
diff --git a/workplans/RREG-WP-0003-automatic-repository-exploration.md b/workplans/RREG-WP-0003-automatic-repository-exploration.md
index a995749..54580e0 100644
--- a/workplans/RREG-WP-0003-automatic-repository-exploration.md
+++ b/workplans/RREG-WP-0003-automatic-repository-exploration.md
@@ -4,7 +4,7 @@ type: workplan
title: "Repository Ability Registry — Automatic Repository Exploration"
domain: capabilities
repo: repo-registry
-status: active
+status: done
owner: codex
topic_slug: foerster-capabilities
created: "2026-04-26"
@@ -120,7 +120,7 @@ review.
```task
id: RREG-WP-0003-T05
-status: todo
+status: done
priority: low
state_hub_task_id: "b812a7fb-19ef-418a-83a2-15bf26fd3f4a"
```
@@ -132,6 +132,12 @@ that produce only weak candidates.
Acceptance: trying the product on repo-registry itself feels understandable and
useful even when a scan finds gaps or weak evidence.
+Implementation note 2026-04-29: analysis result pages now include a Run
+Diagnostics panel with explicit success, failure, empty-result, no-candidate, and
+weak-candidate states. The panel links directly to fact and candidate element
+lists and gives first-run recovery hints for bad paths, inaccessible sources,
+credential issues, and upstream timeouts.
+
## P1: Expectation Gap Feedback Loop
```task
@@ -206,7 +212,7 @@ nested capabilities/features/evidence.
```task
id: RREG-WP-0003-T10
-status: in_progress
+status: done
priority: medium
state_hub_task_id: "0d3fa9e0-bb3e-4bf2-bf8d-4681c5b7bdf5"
```
@@ -252,3 +258,7 @@ Implementation note 2026-04-29: support rows now show an orientation label based
on target/reference abstraction levels. Downward support is normal, same-level
support is marked for review, and upward support is marked for review because it
usually indicates an abstraction or organization problem.
+
+Implementation note 2026-04-29: support listings now include a support
+orientation filter so reviewers can isolate downward support, same-level review
+items, upward review items, or unclassified support.