generated from coulomb/repo-seed
Count badges now with navigation
This commit is contained in:
72
docs/abstraction-strategy.md
Normal file
72
docs/abstraction-strategy.md
Normal 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.
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user