Run diagnostics panel

This commit is contained in:
2026-04-29 19:09:32 +02:00
parent 5c9a0f0614
commit d1048a3177
3 changed files with 296 additions and 5 deletions

View File

@@ -78,6 +78,16 @@ def page(title: str, body: str) -> HTMLResponse:
background: var(--danger-bg);
color: var(--danger);
}}
.notice.warn {{
border-color: #fed7aa;
background: #fff7ed;
color: var(--warn);
}}
.notice.success {{
border-color: #a7f3d0;
background: #ecfdf5;
color: #065f46;
}}
.stack {{ display: grid; gap: 12px; }}
.muted {{ color: var(--muted); }}
.pill {{
@@ -356,6 +366,148 @@ def discovery_gap_report_page(
return page("Capability Gap Report", body)
def render_analysis_diagnostics(
*,
repository_id: int,
analysis_run_id: int,
analysis_run_status: str,
error_message: str | None,
candidate_graph: dict,
facts_count: int,
chunk_count: int,
) -> str:
ability_count = len(candidate_graph.get("abilities", []))
capability_count = sum(
len(ability.get("capabilities", []))
for ability in candidate_graph.get("abilities", [])
)
feature_count = sum(
len(capability.get("features", []))
for ability in candidate_graph.get("abilities", [])
for capability in ability.get("capabilities", [])
)
support_count = sum(
len(capability.get("evidence", []))
for ability in candidate_graph.get("abilities", [])
for capability in ability.get("capabilities", [])
)
confidences = [
float(ability.get("confidence") or 0)
for ability in candidate_graph.get("abilities", [])
]
only_weak_candidates = bool(confidences) and max(confidences) < 0.6
notices: list[tuple[str, str, str]] = []
if analysis_run_status == "failed":
error = error_message or "Analysis failed without a detailed error."
notices.append(
(
"error",
"Analysis failed.",
first_run_failure_hint(error),
)
)
elif facts_count == 0:
notices.append(
(
"warn",
"No observed facts were found.",
(
"This usually means the repository is empty, the selected path "
"does not contain source files, or the deterministic scanner does "
"not yet understand the project shape."
),
)
)
elif ability_count == 0:
notices.append(
(
"warn",
"No candidate abilities were produced.",
(
"The scanner found facts, but could not turn them into a useful "
"ability graph yet. Record an expectation gap for the concepts "
"you expected to see so the deterministic scanner can learn."
),
)
)
elif only_weak_candidates:
notices.append(
(
"warn",
"Only weak candidate abilities were produced.",
(
"Review these cautiously, accept only what is clearly supported, "
"and record expectation gaps for missing core concepts."
),
)
)
else:
notices.append(
(
"success",
"Analysis completed with reviewable results.",
(
"Use the candidate graph and element lists to approve, edit, "
"reject, or record expectation gaps."
),
)
)
cards = "\n".join(
f"""
<div class="notice {escape(level)}">
<strong>{escape(title)}</strong>
<p>{escape(message)}</p>
</div>
"""
for level, title, message in notices
)
error_detail = (
f'<p class="source">{escape(error_message)}</p>' if error_message else ""
)
return f"""
<section class="panel" style="margin-bottom:18px">
<div class="actions">
<h2 style="margin-right:auto">Run Diagnostics</h2>
<span class="pill">{escape(analysis_run_status)}</span>
</div>
{cards}
{error_detail}
<div class="actions">
<a class="button secondary" href="/ui/repos/{repository_id}/elements?scope=facts&analysis_run_id={analysis_run_id}&type=facts">{facts_count} facts</a>
<a class="button secondary" href="/ui/repos/{repository_id}/elements?scope=candidate&analysis_run_id={analysis_run_id}&type=abilities">{ability_count} abilities</a>
<a class="button secondary" href="/ui/repos/{repository_id}/elements?scope=candidate&analysis_run_id={analysis_run_id}&type=capabilities">{capability_count} capabilities</a>
<a class="button secondary" href="/ui/repos/{repository_id}/elements?scope=candidate&analysis_run_id={analysis_run_id}&type=features">{feature_count} features</a>
<a class="button secondary" href="/ui/repos/{repository_id}/elements?scope=candidate&analysis_run_id={analysis_run_id}&type=supports">{support_count} supports</a>
<span class="pill">{chunk_count} content chunks</span>
</div>
</section>
"""
def first_run_failure_hint(error_message: str) -> str:
error = error_message.lower()
if any(marker in error for marker in ("authentication", "credential", "password")):
return (
"The source appears to require credentials. Re-run analysis with a "
"username and password or access token."
)
if any(marker in error for marker in ("not found", "no such file", "does not exist")):
return (
"Verify the local path or Git URL, then re-run analysis. If the "
"upstream is unavailable but a checkout exists, use the cached "
"checkout option."
)
if any(marker in error for marker in ("timeout", "timed out", "could not read", "unable to access")):
return (
"The repository could not be reached in time. Check network access, "
"credentials, and whether the upstream service is available."
)
return (
"Review the error detail below, adjust the source or credentials, and "
"re-run analysis from the repository page."
)
@router.get("/ui/search")
def search_page(
q: str = "",
@@ -929,6 +1081,7 @@ def analysis_run_detail(
) -> HTMLResponse:
repository = service.get_repository(repository_id)
candidate_graph = service.candidate_graph(repository_id, analysis_run_id)
candidate_graph_data = asdict(candidate_graph)
facts = service.list_observed_facts(repository_id, analysis_run_id)
chunks = service.list_content_chunks(repository_id, analysis_run_id)
decisions = service.list_review_decisions(repository_id, analysis_run_id)
@@ -950,12 +1103,21 @@ def analysis_run_detail(
{render_run_detail_compare_link(repository_id, analysis_run_id, service.list_analysis_runs(repository_id))}
<a class="button secondary" href="/ui/repos/{repository_id}">Repository</a>
</div>
{render_analysis_diagnostics(
repository_id=repository_id,
analysis_run_id=analysis_run_id,
analysis_run_status=candidate_graph.analysis_run.status,
error_message=candidate_graph.analysis_run.error_message,
candidate_graph=candidate_graph_data,
facts_count=len(facts),
chunk_count=len(chunks),
)}
<div class="grid">
<section class="panel">
<div class="actions">
<h2 style="margin-right:auto">Candidate Graph</h2>
{render_graph_counts(
asdict(candidate_graph),
candidate_graph_data,
facts_count=len(facts),
base_href=(
f"/ui/repos/{repository_id}/elements?scope=all"
@@ -971,7 +1133,7 @@ def analysis_run_detail(
<button type="submit">Approve</button>
</form>
</div>
{render_candidate_graph(asdict(candidate_graph), repository_id, analysis_run_id)}
{render_candidate_graph(candidate_graph_data, repository_id, analysis_run_id)}
</section>
<section class="panel">
<div class="actions">
@@ -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(
<label>Class <input name="class_filter" value="{escape(class_filter)}" list="element-classes" placeholder="Any class"></label>
{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 ""}
</div>
{render_class_datalist(entry_scoped_elements)}
<div class="actions">
@@ -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'<option value="{escape(value)}"{" selected" if support_orientation_filter == value else ""}>{escape(label)}</option>'
for value, label in options
)
return f"""
<label>Support orientation
<select name="support_orientation_filter">
{rendered_options}
</select>
</label>
"""
def render_element_edit_fields(row: dict) -> str:
item_kind = row["item_kind"]
name = escape(str(row["name"]))

View File

@@ -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,

View File

@@ -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.