Candidate Graph
- {render_graph_counts(asdict(candidate_graph), facts_count=len(facts))}
+ {render_graph_counts(
+ asdict(candidate_graph),
+ facts_count=len(facts),
+ base_href=(
+ f"/ui/repos/{repository_id}/elements?scope=candidate"
+ f"&analysis_run_id={analysis_run_id}"
+ ),
+ facts_href=(
+ f"/ui/repos/{repository_id}/elements?scope=facts"
+ f"&analysis_run_id={analysis_run_id}&type=facts"
+ ),
+ )}
@@ -883,7 +905,15 @@ def analysis_run_detail(
Observed Facts
- {render_count_pills(facts=len(facts))}
+ {render_count_pills(
+ hrefs={
+ "facts": (
+ f"/ui/repos/{repository_id}/elements?scope=facts"
+ f"&analysis_run_id={analysis_run_id}&type=facts"
+ )
+ },
+ facts=len(facts),
+ )}
| Kind | Name | Path | Value |
@@ -901,6 +931,52 @@ def analysis_run_detail(
return page(f"{repository.name} Run {analysis_run_id}", body)
+@router.get("/ui/repos/{repository_id}/elements")
+def repository_element_listing(
+ repository_id: int,
+ scope: str = Query("approved"),
+ type: str = Query("abilities"),
+ analysis_run_id: int | None = Query(default=None),
+ service: RegistryService = Depends(get_service),
+) -> HTMLResponse:
+ repository = service.get_repository(repository_id)
+ title = element_listing_title(repository.name, scope, type)
+ if scope == "approved":
+ graph = asdict(service.ability_map(repository_id))
+ rows = render_graph_element_rows(graph, type)
+ elif scope == "candidate":
+ if analysis_run_id is None:
+ runs = service.list_analysis_runs(repository_id)
+ latest = latest_completed_candidate_graph(service, repository_id, runs)
+ if latest is None:
+ rows = '| No completed candidate graph. |
'
+ else:
+ analysis_run_id, candidate_graph = latest
+ rows = render_graph_element_rows(asdict(candidate_graph), type)
+ else:
+ candidate_graph = service.candidate_graph(repository_id, analysis_run_id)
+ rows = render_graph_element_rows(asdict(candidate_graph), type)
+ elif scope == "facts":
+ facts = service.list_observed_facts(repository_id, analysis_run_id)
+ rows = render_fact_element_rows(facts)
+ else:
+ rows = '| Unknown listing scope. |
'
+
+ body = f"""
+
+
+
+ | Type | Name | Parent | Source |
+ {rows}
+
+
+ """
+ return page(title, body)
+
+
@router.get("/ui/repos/{repository_id}/analysis-runs/{base_analysis_run_id}/diff/{target_analysis_run_id}")
def analysis_run_diff_detail(
repository_id: int,
@@ -1573,7 +1649,52 @@ def split_capability_lines(value: str) -> list[str]:
return [line.strip() for line in normalized.splitlines() if line.strip()]
-def render_graph_counts(graph: dict, facts_count: int | None = None) -> str:
+def latest_completed_candidate_graph(
+ service: RegistryService,
+ repository_id: int,
+ runs: list,
+):
+ for run in sorted(runs, key=lambda item: item.id, reverse=True):
+ if run.status != "completed":
+ continue
+ try:
+ return run.id, service.candidate_graph(repository_id, run.id)
+ except NotFoundError:
+ continue
+ return None
+
+
+def render_latest_candidate_counts(
+ repository_id: int,
+ latest_candidate,
+ service: RegistryService,
+) -> str:
+ if latest_candidate is None:
+ return 'No completed candidate graph yet.
'
+ analysis_run_id, candidate_graph = latest_candidate
+ facts_count = len(service.list_observed_facts(repository_id, analysis_run_id))
+ return render_graph_counts(
+ asdict(candidate_graph),
+ facts_count=facts_count,
+ label_prefix="candidate",
+ base_href=(
+ f"/ui/repos/{repository_id}/elements?scope=candidate"
+ f"&analysis_run_id={analysis_run_id}"
+ ),
+ facts_href=(
+ f"/ui/repos/{repository_id}/elements?scope=facts"
+ f"&analysis_run_id={analysis_run_id}&type=facts"
+ ),
+ )
+
+
+def render_graph_counts(
+ graph: dict,
+ facts_count: int | None = None,
+ label_prefix: str = "",
+ base_href: str | None = None,
+ facts_href: str | None = None,
+) -> str:
abilities = graph.get("abilities", [])
capabilities = [
capability
@@ -1592,20 +1713,121 @@ def render_graph_counts(graph: dict, facts_count: int | None = None) -> str:
}
if facts_count is not None:
counts["facts"] = facts_count
- return render_count_pills(**counts)
+ hrefs = (
+ {name: f"{base_href}&type={name}" for name in counts if name != "facts"}
+ if base_href
+ else {}
+ )
+ if facts_href:
+ hrefs["facts"] = facts_href
+ return render_count_pills(label_prefix=label_prefix, hrefs=hrefs, **counts)
-def render_count_pills(**counts: int) -> str:
+def render_count_pills(
+ label_prefix: str = "",
+ hrefs: dict[str, str] | None = None,
+ **counts: int,
+) -> str:
labels = {
"abilities": "abilities",
"capabilities": "capabilities",
"features": "features",
"facts": "facts",
}
- return "".join(
- f'{count} {labels[name]}'
- for name, count in counts.items()
- )
+ prefix = f"{label_prefix} " if label_prefix else ""
+ hrefs = hrefs or {}
+ items = []
+ for name, count in counts.items():
+ label = f"{count} {prefix}{labels[name]}"
+ if name in hrefs:
+ items.append(f'{label}')
+ else:
+ items.append(f'{label}')
+ return "".join(items)
+
+
+def render_graph_element_rows(graph: dict, item_type: str) -> str:
+ rows: list[str] = []
+ for ability in graph.get("abilities", []):
+ if item_type == "abilities":
+ rows.append(
+ render_element_row("ability", ability["name"], "", ability.get("source_refs", []))
+ )
+ for capability in ability.get("capabilities", []):
+ if item_type == "capabilities":
+ rows.append(
+ render_element_row(
+ "capability",
+ capability["name"],
+ ability["name"],
+ capability.get("source_refs", []),
+ )
+ )
+ for feature in capability.get("features", []):
+ if item_type == "features":
+ rows.append(
+ render_element_row(
+ "feature",
+ feature["name"],
+ capability["name"],
+ feature.get("source_refs", []),
+ )
+ )
+ return "\n".join(rows) or '| No matching elements. |
'
+
+
+def element_listing_title(repository_name: str, scope: str, item_type: str) -> str:
+ type_labels = {
+ "abilities": "Abilities",
+ "capabilities": "Capabilities",
+ "features": "Features",
+ "facts": "Facts",
+ }
+ if scope == "facts":
+ return f"{repository_name} · Observed Facts"
+ scope_labels = {
+ "approved": "Approved",
+ "candidate": "Candidate",
+ }
+ scope_label = scope_labels.get(scope, scope.title())
+ type_label = type_labels.get(item_type, item_type.title())
+ return f"{repository_name} · {scope_label} {type_label}"
+
+
+def render_fact_element_rows(facts: list) -> str:
+ rows = [
+ render_element_row(
+ fact.kind,
+ fact.name,
+ fact.path,
+ [
+ {
+ "path": fact.path,
+ "kind": fact.kind,
+ "name": fact.name,
+ "line": fact.metadata.get("line"),
+ }
+ ],
+ )
+ for fact in facts
+ ]
+ return "\n".join(rows) or '| No matching facts. |
'
+
+
+def render_element_row(
+ item_type: str,
+ name: str,
+ parent: str,
+ source_refs: list[dict],
+) -> str:
+ return f"""
+
+ | {escape(item_type)} |
+ {escape(name)} |
+ {escape(parent)} |
+ {render_sources(source_refs)} |
+
+ """
def render_candidate_graph(graph: dict, repository_id: int, analysis_run_id: int) -> str:
diff --git a/tests/test_web_api.py b/tests/test_web_api.py
index 21a6d78..a322fe2 100644
--- a/tests/test_web_api.py
+++ b/tests/test_web_api.py
@@ -1100,6 +1100,7 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
)
assert create_response.status_code == 303
repository_path = create_response.headers["location"]
+ repository_id = int(repository_path.rsplit("/", 1)[-1])
detail_response = client.get(repository_path)
assert detail_response.status_code == 200
@@ -1159,12 +1160,61 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
assert "1 abilities" in approved_detail.text
assert "2 capabilities" in approved_detail.text
assert "2 features" in approved_detail.text
+ assert "Latest Candidate Graph" in approved_detail.text
+ assert "1 candidate abilities" in approved_detail.text
+ assert "2 candidate capabilities" in approved_detail.text
+ assert "2 candidate features" in approved_detail.text
+ assert "7 candidate facts" in approved_detail.text
+ assert (
+ f"/ui/repos/{repository_id}/elements?scope=approved&type=abilities"
+ in approved_detail.text
+ )
+ assert (
+ f"/ui/repos/{repository_id}/elements?scope=candidate&analysis_run_id={first_run_id}&type=features"
+ in approved_detail.text
+ )
+ assert (
+ f"/ui/repos/{repository_id}/elements?scope=facts&analysis_run_id={first_run_id}&type=facts"
+ in approved_detail.text
+ )
assert "Review UI Repo Edited Repository Usefulness" in approved_detail.text
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
+ approved_listing = client.get(
+ f"/ui/repos/{repository_id}/elements",
+ params={"scope": "approved", "type": "capabilities"},
+ )
+ assert approved_listing.status_code == 200
+ assert "Approved Capabilities" in approved_listing.text
+ assert "Expose Repository Interface" in approved_listing.text
+
+ candidate_listing = client.get(
+ f"/ui/repos/{repository_id}/elements",
+ params={
+ "scope": "candidate",
+ "analysis_run_id": first_run_id,
+ "type": "features",
+ },
+ )
+ assert candidate_listing.status_code == 200
+ assert "Candidate Features" in candidate_listing.text
+ assert "GET /status" in candidate_listing.text
+
+ fact_listing = client.get(
+ f"/ui/repos/{repository_id}/elements",
+ params={
+ "scope": "facts",
+ "analysis_run_id": first_run_id,
+ "type": "facts",
+ },
+ )
+ assert fact_listing.status_code == 200
+ assert "Observed Facts" in fact_listing.text
+ assert "python route decorator" in fact_listing.text
+
(source / "app.py").write_text(
"from fastapi import FastAPI\n"
"app = FastAPI()\n"
diff --git a/workplans/RREG-WP-0003-automatic-repository-exploration.md b/workplans/RREG-WP-0003-automatic-repository-exploration.md
index 4ded3cf..2173bd1 100644
--- a/workplans/RREG-WP-0003-automatic-repository-exploration.md
+++ b/workplans/RREG-WP-0003-automatic-repository-exploration.md
@@ -8,7 +8,7 @@ status: active
owner: codex
topic_slug: foerster-capabilities
created: "2026-04-26"
-updated: "2026-04-26"
+updated: "2026-04-28"
state_hub_workstream_id: "c121d462-f2e4-45d3-9d2d-9c04a3556953"
---
@@ -66,7 +66,7 @@ facts as drilldown evidence, not mirror individual scanner facts.
```task
id: RREG-WP-0003-T06
-status: todo
+status: done
priority: high
state_hub_task_id: "51890aff-511d-4635-85c4-fe4db0b7dd01"
```
@@ -85,7 +85,7 @@ against feature granularity collapsing into one feature per observed fact.
```task
id: RREG-WP-0003-T03
-status: todo
+status: in_progress
priority: medium
state_hub_task_id: "5c4b5bb1-390c-4782-bb70-104b0006fe67"
```