generated from coulomb/repo-seed
Run diagnostics panel
This commit is contained in:
@@ -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"]))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user