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"]))
|
||||
|
||||
Reference in New Issue
Block a user