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);
|
background: var(--danger-bg);
|
||||||
color: var(--danger);
|
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; }}
|
.stack {{ display: grid; gap: 12px; }}
|
||||||
.muted {{ color: var(--muted); }}
|
.muted {{ color: var(--muted); }}
|
||||||
.pill {{
|
.pill {{
|
||||||
@@ -356,6 +366,148 @@ def discovery_gap_report_page(
|
|||||||
return page("Capability Gap Report", body)
|
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")
|
@router.get("/ui/search")
|
||||||
def search_page(
|
def search_page(
|
||||||
q: str = "",
|
q: str = "",
|
||||||
@@ -929,6 +1081,7 @@ def analysis_run_detail(
|
|||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
repository = service.get_repository(repository_id)
|
repository = service.get_repository(repository_id)
|
||||||
candidate_graph = service.candidate_graph(repository_id, analysis_run_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)
|
facts = service.list_observed_facts(repository_id, analysis_run_id)
|
||||||
chunks = service.list_content_chunks(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)
|
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))}
|
{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>
|
<a class="button secondary" href="/ui/repos/{repository_id}">Repository</a>
|
||||||
</div>
|
</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">
|
<div class="grid">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<h2 style="margin-right:auto">Candidate Graph</h2>
|
<h2 style="margin-right:auto">Candidate Graph</h2>
|
||||||
{render_graph_counts(
|
{render_graph_counts(
|
||||||
asdict(candidate_graph),
|
candidate_graph_data,
|
||||||
facts_count=len(facts),
|
facts_count=len(facts),
|
||||||
base_href=(
|
base_href=(
|
||||||
f"/ui/repos/{repository_id}/elements?scope=all"
|
f"/ui/repos/{repository_id}/elements?scope=all"
|
||||||
@@ -971,7 +1133,7 @@ def analysis_run_detail(
|
|||||||
<button type="submit">Approve</button>
|
<button type="submit">Approve</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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>
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
@@ -1048,6 +1210,7 @@ def repository_element_listing(
|
|||||||
class_filter: str = Query(""),
|
class_filter: str = Query(""),
|
||||||
entry_filter: str = Query(""),
|
entry_filter: str = Query(""),
|
||||||
candidate_status_filter: str = Query("active"),
|
candidate_status_filter: str = Query("active"),
|
||||||
|
support_orientation_filter: str = Query(""),
|
||||||
analysis_run_id: int | None = Query(default=None),
|
analysis_run_id: int | None = Query(default=None),
|
||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
@@ -1097,6 +1260,7 @@ def repository_element_listing(
|
|||||||
q,
|
q,
|
||||||
class_filter,
|
class_filter,
|
||||||
candidate_status_filter="all",
|
candidate_status_filter="all",
|
||||||
|
support_orientation_filter=support_orientation_filter,
|
||||||
)
|
)
|
||||||
rows = render_element_rows(
|
rows = render_element_rows(
|
||||||
filtered,
|
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>
|
<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_entry_filter(entry_filter) if scope != "facts" else ""}
|
||||||
{render_candidate_status_filter(candidate_status_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>
|
</div>
|
||||||
{render_class_datalist(entry_scoped_elements)}
|
{render_class_datalist(entry_scoped_elements)}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
@@ -2204,17 +2369,21 @@ def filter_element_rows(
|
|||||||
class_filter: str,
|
class_filter: str,
|
||||||
entry_filter: str = "",
|
entry_filter: str = "",
|
||||||
candidate_status_filter: str = "",
|
candidate_status_filter: str = "",
|
||||||
|
support_orientation_filter: str = "",
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
query = query.strip().lower()
|
query = query.strip().lower()
|
||||||
class_filter = class_filter.strip().lower()
|
class_filter = class_filter.strip().lower()
|
||||||
entry_filter = entry_filter.strip().lower()
|
entry_filter = entry_filter.strip().lower()
|
||||||
candidate_status_filter = candidate_status_filter.strip().lower()
|
candidate_status_filter = candidate_status_filter.strip().lower()
|
||||||
|
support_orientation_filter = support_orientation_filter.strip().lower()
|
||||||
filtered = []
|
filtered = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if entry_filter and row.get("entry_state", "").lower() != entry_filter:
|
if entry_filter and row.get("entry_state", "").lower() != entry_filter:
|
||||||
continue
|
continue
|
||||||
if not candidate_status_matches(row, candidate_status_filter):
|
if not candidate_status_matches(row, candidate_status_filter):
|
||||||
continue
|
continue
|
||||||
|
if support_orientation_filter and support_orientation_label(row) != support_orientation_filter:
|
||||||
|
continue
|
||||||
row_class = str(row["primary_class"]).lower()
|
row_class = str(row["primary_class"]).lower()
|
||||||
if class_filter and class_filter not in row_class:
|
if class_filter and class_filter not in row_class:
|
||||||
continue
|
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:
|
def render_element_edit_fields(row: dict) -> str:
|
||||||
item_kind = row["item_kind"]
|
item_kind = row["item_kind"]
|
||||||
name = escape(str(row["name"]))
|
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)
|
run_detail = client.get(run_path)
|
||||||
assert run_detail.status_code == 200
|
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 "Candidate Graph" in run_detail.text
|
||||||
assert "1 abilities" in run_detail.text
|
assert "1 abilities" in run_detail.text
|
||||||
assert "2 capabilities" 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()
|
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):
|
def test_ui_register_and_explore_lands_on_analysis_result(tmp_path):
|
||||||
source = tmp_path / "explore-repo"
|
source = tmp_path / "explore-repo"
|
||||||
source.mkdir()
|
source.mkdir()
|
||||||
@@ -1715,6 +1775,21 @@ def test_ui_manual_registry_entry_loop(tmp_path):
|
|||||||
)
|
)
|
||||||
assert evidence_response.status_code == 303
|
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)
|
detail_response = client.get(repository_path)
|
||||||
assert "Manual Ability" in detail_response.text
|
assert "Manual Ability" in detail_response.text
|
||||||
assert "Manual Capability" 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 "supports capability" in detail_response.text
|
||||||
assert "references source" in detail_response.text
|
assert "references source" in detail_response.text
|
||||||
assert "downward support" 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 "ID " in detail_response.text
|
||||||
assert "Save Ability" 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 f"references feature #{feature_id}" in detail_response.text
|
||||||
assert "downward support" 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(
|
delete_feature_response = client.post(
|
||||||
f"{repository_path}/features/{feature_id}/delete",
|
f"{repository_path}/features/{feature_id}/delete",
|
||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ type: workplan
|
|||||||
title: "Repository Ability Registry — Automatic Repository Exploration"
|
title: "Repository Ability Registry — Automatic Repository Exploration"
|
||||||
domain: capabilities
|
domain: capabilities
|
||||||
repo: repo-registry
|
repo: repo-registry
|
||||||
status: active
|
status: done
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: foerster-capabilities
|
topic_slug: foerster-capabilities
|
||||||
created: "2026-04-26"
|
created: "2026-04-26"
|
||||||
@@ -120,7 +120,7 @@ review.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: RREG-WP-0003-T05
|
id: RREG-WP-0003-T05
|
||||||
status: todo
|
status: done
|
||||||
priority: low
|
priority: low
|
||||||
state_hub_task_id: "b812a7fb-19ef-418a-83a2-15bf26fd3f4a"
|
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
|
Acceptance: trying the product on repo-registry itself feels understandable and
|
||||||
useful even when a scan finds gaps or weak evidence.
|
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
|
## P1: Expectation Gap Feedback Loop
|
||||||
|
|
||||||
```task
|
```task
|
||||||
@@ -206,7 +212,7 @@ nested capabilities/features/evidence.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: RREG-WP-0003-T10
|
id: RREG-WP-0003-T10
|
||||||
status: in_progress
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "0d3fa9e0-bb3e-4bf2-bf8d-4681c5b7bdf5"
|
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
|
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
|
support is marked for review, and upward support is marked for review because it
|
||||||
usually indicates an abstraction or organization problem.
|
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