Count badges now with navigation

This commit is contained in:
2026-04-28 03:58:34 +02:00
parent 6981cb1df4
commit 360537ef05
4 changed files with 357 additions and 13 deletions

View File

@@ -0,0 +1,72 @@
# Abstraction Strategy
The registry has three layers with different trust levels:
1. Observed facts are deterministic scanner output: files, manifests, framework
hints, tests, docs, routes, commands, and source locations.
2. Candidate claims are abstractions proposed from those facts. They are useful
review seeds, not registry truth.
3. Approved entries are curated truth after human review or an explicit trusted
automation mode.
## Granularity
Features should describe a user-visible or operational behavior surface, not mirror
individual scanner facts. A one-to-one pattern such as one route fact becoming one
feature is a smell unless the repository truly exposes only one behavior.
Current deterministic grouping:
- Multiple HTTP route facts become one `HTTP API surface` feature with several
source references.
- Multiple CLI command facts become one `CLI command surface` feature with several
source references.
- Facts remain available as drilldown evidence through `source_refs`.
This gives reviewers orientation at the behavior level while keeping provenance.
## What Deterministic Logic Can Do
Deterministic scanners can reliably identify:
- repository structure and languages
- package manifests and framework hints
- API/CLI entry-point surfaces
- docs, examples, and tests as corroborating evidence
- stable source references for review and approval
Deterministic candidate generation can group these into conservative capabilities
such as interface exposure and repository structure. It should avoid pretending it
understands domain intent when the evidence is thin.
## Where LLM Assistance Helps
LLMs are most useful for naming and explaining intent:
- turning `HTTP API surface` into a domain capability such as repository ingestion,
review workflow, or search
- separating administrative, operational, and product-facing capabilities
- summarizing README and code context into clearer ability descriptions
- suggesting merges or relinks when deterministic names are too generic
LLM output remains candidate material. It should cite source paths and be reviewed
or explicitly auto-approved by a trusted mode before becoming approved registry
truth.
## Trial Repo Observations
`repo-registry` demonstrates the current boundary well: deterministic scanning sees
FastAPI routes, tests, docs, and Python structure, but the meaningful abstractions
are repository ingestion, deterministic analysis, candidate review, discovery, and
State Hub coordination. Those names likely require either review edits or LLM
assistance.
The other trial repos reinforce the same point: fact lists are useful audit trails,
but the primary UI should lead with candidate or approved ability maps and expose
facts as drilldown evidence.
## Regression Guard
`tests/test_candidate_graph.py` includes a guard that multiple interface facts are
grouped into behavioral surface features with multiple source refs. This protects
against falling back to one feature per observed fact.

View File

@@ -464,6 +464,11 @@ def repository_detail(
raise HTTPException(status_code=404, detail=str(exc)) from exc
runs = service.list_analysis_runs(repository_id)
ability_map = service.ability_map(repository_id)
latest_candidate = latest_completed_candidate_graph(
service,
repository_id,
runs,
)
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"})
@@ -517,7 +522,13 @@ def repository_detail(
</section>
<section class="panel">
<h2>Approved Ability Map</h2>
{render_graph_counts(asdict(ability_map), facts_count=None)}
{render_graph_counts(
asdict(ability_map),
facts_count=None,
base_href=f"/ui/repos/{repository_id}/elements?scope=approved",
)}
<h2>Latest Candidate Graph</h2>
{render_latest_candidate_counts(repository_id, latest_candidate, service)}
{render_ability_map(asdict(ability_map), repository_id)}
</section>
</div>
@@ -873,7 +884,18 @@ def analysis_run_detail(
<section class="panel">
<div class="actions">
<h2 style="margin-right:auto">Candidate Graph</h2>
{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"
),
)}
<form method="post" action="/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph/approve">
<button type="submit">Approve</button>
</form>
@@ -883,7 +905,15 @@ def analysis_run_detail(
<section class="panel">
<div class="actions">
<h2 style="margin-right:auto">Observed Facts</h2>
{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),
)}
</div>
<table>
<thead><tr><th>Kind</th><th>Name</th><th>Path</th><th>Value</th></tr></thead>
@@ -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 = '<tr><td class="muted">No completed candidate graph.</td></tr>'
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 = '<tr><td class="muted">Unknown listing scope.</td></tr>'
body = f"""
<div class="actions">
<h1 style="margin-right:auto">{escape(title)}</h1>
<a class="button secondary" href="/ui/repos/{repository_id}">Repository</a>
</div>
<section class="panel">
<table>
<thead><tr><th>Type</th><th>Name</th><th>Parent</th><th>Source</th></tr></thead>
<tbody>{rows}</tbody>
</table>
</section>
"""
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 '<p class="muted">No completed candidate graph yet.</p>'
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'<span class="pill">{count} {labels[name]}</span>'
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'<a class="pill" href="{escape(hrefs[name])}">{label}</a>')
else:
items.append(f'<span class="pill">{label}</span>')
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 '<tr><td colspan="4" class="muted">No matching elements.</td></tr>'
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 '<tr><td colspan="4" class="muted">No matching facts.</td></tr>'
def render_element_row(
item_type: str,
name: str,
parent: str,
source_refs: list[dict],
) -> str:
return f"""
<tr>
<td>{escape(item_type)}</td>
<td>{escape(name)}</td>
<td>{escape(parent)}</td>
<td>{render_sources(source_refs)}</td>
</tr>
"""
def render_candidate_graph(graph: dict, repository_id: int, analysis_run_id: int) -> str:

View File

@@ -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&amp;type=abilities"
in approved_detail.text
)
assert (
f"/ui/repos/{repository_id}/elements?scope=candidate&amp;analysis_run_id={first_run_id}&amp;type=features"
in approved_detail.text
)
assert (
f"/ui/repos/{repository_id}/elements?scope=facts&amp;analysis_run_id={first_run_id}&amp;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"

View File

@@ -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"
```