generated from coulomb/repo-seed
Characteristics model ui
This commit is contained in:
@@ -1960,10 +1960,17 @@ def render_graph_counts(
|
||||
for capability in capabilities
|
||||
for feature in capability.get("features", [])
|
||||
]
|
||||
supports = [
|
||||
evidence
|
||||
for capability in capabilities
|
||||
for evidence in capability.get("evidence", [])
|
||||
]
|
||||
counts: dict[str, int] = {
|
||||
"scopes": 1 if graph.get("scope") else 0,
|
||||
"abilities": len(abilities),
|
||||
"capabilities": len(capabilities),
|
||||
"features": len(features),
|
||||
"supports": len(supports),
|
||||
}
|
||||
if facts_count is not None:
|
||||
counts["facts"] = facts_count
|
||||
@@ -1983,9 +1990,11 @@ def render_count_pills(
|
||||
**counts: int,
|
||||
) -> str:
|
||||
labels = {
|
||||
"scopes": "scope",
|
||||
"abilities": "abilities",
|
||||
"capabilities": "capabilities",
|
||||
"features": "features",
|
||||
"supports": "supports",
|
||||
"facts": "facts",
|
||||
}
|
||||
prefix = f"{label_prefix} " if label_prefix else ""
|
||||
@@ -2025,6 +2034,21 @@ def graph_element_rows(
|
||||
entry_state: str = "",
|
||||
) -> list[dict]:
|
||||
rows: list[dict] = []
|
||||
scope = graph.get("scope")
|
||||
if item_type in {"scopes", "scope"} and scope and entry_state == "approved":
|
||||
rows.append(
|
||||
element_row(
|
||||
"scope",
|
||||
scope["name"],
|
||||
"",
|
||||
[],
|
||||
item_id=scope.get("id"),
|
||||
item_kind="scope",
|
||||
description=scope.get("description", ""),
|
||||
confidence=scope.get("confidence", 1.0),
|
||||
entry_state=entry_state,
|
||||
)
|
||||
)
|
||||
for ability in graph.get("abilities", []):
|
||||
if item_type == "abilities":
|
||||
rows.append(
|
||||
@@ -2075,6 +2099,27 @@ def graph_element_rows(
|
||||
entry_state=entry_state,
|
||||
)
|
||||
)
|
||||
for evidence in capability.get("evidence", []):
|
||||
if item_type in {"supports", "evidence"}:
|
||||
rows.append(
|
||||
element_row(
|
||||
evidence.get("strength", "medium"),
|
||||
f"{evidence.get('type', 'support')}: {evidence.get('reference', '')}",
|
||||
capability["name"],
|
||||
evidence.get("source_refs", []),
|
||||
item_id=evidence.get("id"),
|
||||
item_kind="evidence",
|
||||
support_type=evidence.get("type", ""),
|
||||
reference=evidence.get("reference", ""),
|
||||
strength=evidence.get("strength", "medium"),
|
||||
status=evidence.get("status", ""),
|
||||
target_kind=evidence.get("target_kind", "capability"),
|
||||
target_id=evidence.get("target_id"),
|
||||
reference_kind=evidence.get("reference_kind", "source"),
|
||||
reference_id=evidence.get("reference_id"),
|
||||
entry_state=entry_state,
|
||||
)
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
@@ -2083,6 +2128,8 @@ def element_listing_title(repository_name: str, scope: str, item_type: str) -> s
|
||||
"abilities": "Abilities",
|
||||
"capabilities": "Capabilities",
|
||||
"features": "Features",
|
||||
"scopes": "Scopes",
|
||||
"supports": "Supports",
|
||||
"facts": "Facts",
|
||||
}
|
||||
if scope == "facts":
|
||||
@@ -2159,6 +2206,13 @@ def filter_element_rows(
|
||||
str(row["parent"]),
|
||||
str(row.get("entry_state", "")),
|
||||
str(row.get("status", "")),
|
||||
str(row.get("support_type", "")),
|
||||
str(row.get("reference", "")),
|
||||
str(row.get("strength", "")),
|
||||
str(row.get("target_kind", "")),
|
||||
str(row.get("target_id", "")),
|
||||
str(row.get("reference_kind", "")),
|
||||
str(row.get("reference_id", "")),
|
||||
source_refs_text(row["source_refs"]),
|
||||
]
|
||||
).lower()
|
||||
@@ -2203,12 +2257,26 @@ def render_element_row(
|
||||
<td><span class="pill">{escape(str(row["primary_class"]))}</span></td>
|
||||
<td>{escape(str(row["name"]))}</td>
|
||||
<td>{escape(str(row["parent"]))}</td>
|
||||
<td>{render_sources(row["source_refs"])}</td>
|
||||
<td>{render_element_source_detail(row)}</td>
|
||||
<td>{render_element_actions(row, repository_id, analysis_run_id)}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
|
||||
def render_element_source_detail(row: dict) -> str:
|
||||
if row.get("item_kind") == "evidence":
|
||||
target = escape(str(row.get("target_kind") or "capability"))
|
||||
target_id = row.get("target_id")
|
||||
reference_kind = escape(str(row.get("reference_kind") or "source"))
|
||||
reference_id = row.get("reference_id")
|
||||
return (
|
||||
f'<p><span class="pill">supports {target}{f" #{target_id}" if target_id else ""}</span>'
|
||||
f' <span class="pill">references {reference_kind}{f" #{reference_id}" if reference_id else ""}</span></p>'
|
||||
f'{render_sources(row["source_refs"])}'
|
||||
)
|
||||
return render_sources(row["source_refs"])
|
||||
|
||||
|
||||
def render_element_actions(
|
||||
row: dict,
|
||||
repository_id: int,
|
||||
@@ -2220,6 +2288,17 @@ def render_element_actions(
|
||||
return ""
|
||||
if row.get("entry_state") == "approved":
|
||||
return render_approved_element_actions(row, repository_id)
|
||||
if (
|
||||
row.get("entry_state") == "candidate"
|
||||
and row.get("item_kind") == "evidence"
|
||||
and analysis_run_id is not None
|
||||
and row.get("status", "candidate") == "candidate"
|
||||
):
|
||||
return render_candidate_support_element_actions(
|
||||
row,
|
||||
repository_id,
|
||||
analysis_run_id,
|
||||
)
|
||||
if (
|
||||
row.get("entry_state") == "candidate"
|
||||
and analysis_run_id is not None
|
||||
@@ -2264,6 +2343,13 @@ def render_entry_filter(entry_filter: str) -> str:
|
||||
def render_approved_element_actions(row: dict, repository_id: int) -> str:
|
||||
item_id = row["item_id"]
|
||||
item_kind = row["item_kind"]
|
||||
if item_kind == "scope":
|
||||
return f"""
|
||||
<form class="stack" method="post" action="/ui/repos/{repository_id}/scope/edit">
|
||||
{render_element_edit_fields(row)}
|
||||
<button class="secondary" type="submit">Save</button>
|
||||
</form>
|
||||
"""
|
||||
edit_action = f"/ui/repos/{repository_id}/{item_kind}/{item_id}/edit"
|
||||
delete_action = f"/ui/repos/{repository_id}/{item_kind}/{item_id}/delete"
|
||||
hidden_fields = render_element_hidden_fields(row)
|
||||
@@ -2280,6 +2366,39 @@ def render_approved_element_actions(row: dict, repository_id: int) -> str:
|
||||
"""
|
||||
|
||||
|
||||
def render_candidate_support_element_actions(
|
||||
row: dict,
|
||||
repository_id: int,
|
||||
analysis_run_id: int,
|
||||
) -> str:
|
||||
item_id = row["item_id"]
|
||||
reject_action = (
|
||||
f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
||||
f"/candidate-evidence/{item_id}/reject"
|
||||
)
|
||||
relink_action = (
|
||||
f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
||||
f"/candidate-evidence/{item_id}/relink"
|
||||
)
|
||||
merge_action = (
|
||||
f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
||||
f"/candidate-evidence/{item_id}/merge"
|
||||
)
|
||||
return f"""
|
||||
<form method="post" action="{reject_action}">
|
||||
<button class="secondary" type="submit">Remove</button>
|
||||
</form>
|
||||
<form style="display:inline-grid; grid-template-columns: 120px auto; gap: 6px; align-items: end;" method="post" action="{relink_action}">
|
||||
<label>Target capability ID<input name="target_capability_id" type="number" min="1" required></label>
|
||||
<button class="secondary" type="submit">Relink</button>
|
||||
</form>
|
||||
<form style="display:inline-grid; grid-template-columns: 140px auto; gap: 6px; align-items: end;" method="post" action="{merge_action}">
|
||||
<label>Merge into support ID<input name="target_evidence_id" type="number" min="1" required></label>
|
||||
<button class="secondary" type="submit">Merge</button>
|
||||
</form>
|
||||
"""
|
||||
|
||||
|
||||
def render_candidate_element_actions(
|
||||
row: dict,
|
||||
repository_id: int,
|
||||
@@ -2352,6 +2471,12 @@ def render_candidate_status_filter(candidate_status_filter: str) -> str:
|
||||
def render_element_edit_fields(row: dict) -> str:
|
||||
item_kind = row["item_kind"]
|
||||
name = escape(str(row["name"]))
|
||||
if item_kind == "scope":
|
||||
return f"""
|
||||
<label>Name <input name="name" value="{name}" required></label>
|
||||
<label>Description <textarea name="description" rows="2">{escape(str(row.get("description", "")))}</textarea></label>
|
||||
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="{float(row.get("confidence", 1.0)):.2f}" required></label>
|
||||
"""
|
||||
if item_kind == "features":
|
||||
feature_type = escape(str(row["primary_class"]))
|
||||
location = escape(str(row.get("location", "")))
|
||||
@@ -2360,6 +2485,16 @@ def render_element_edit_fields(row: dict) -> str:
|
||||
<label>Type <input name="type" value="{feature_type}" required></label>
|
||||
<label>Location <input name="location" value="{location}"></label>
|
||||
"""
|
||||
if item_kind == "evidence":
|
||||
return f"""
|
||||
<label>Supported characteristic kind <input name="target_kind" value="{escape(str(row.get("target_kind", "capability")))}" required></label>
|
||||
<label>Supported characteristic ID <input name="target_id" type="number" min="1" value="{row.get('target_id') or ''}"></label>
|
||||
<label>Support type <input name="type" value="{escape(str(row.get("support_type", "")))}" required></label>
|
||||
<label>Reference <input name="reference" value="{escape(str(row.get("reference", "")))}" required></label>
|
||||
<label>Reference kind <input name="reference_kind" value="{escape(str(row.get("reference_kind", "source")))}" required></label>
|
||||
<label>Reference ID <input name="reference_id" type="number" min="1" value="{row.get('reference_id') or ''}"></label>
|
||||
<label>Strength <input name="strength" value="{escape(str(row.get("strength", "medium")))}" required></label>
|
||||
"""
|
||||
return f'<label>Name <input name="name" value="{name}" required></label>'
|
||||
|
||||
|
||||
@@ -2371,6 +2506,10 @@ def render_element_hidden_fields(row: dict) -> str:
|
||||
fields.append(
|
||||
f'<input type="hidden" name="description" value="{escape(str(row.get("description", "")))}">'
|
||||
)
|
||||
if item_kind == "scope":
|
||||
fields.append(
|
||||
f'<input type="hidden" name="description" value="{escape(str(row.get("description", "")))}">'
|
||||
)
|
||||
if item_kind == "capabilities":
|
||||
inputs = escape(", ".join(row.get("inputs", [])))
|
||||
outputs = escape(", ".join(row.get("outputs", [])))
|
||||
|
||||
@@ -1217,6 +1217,8 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
|
||||
assert "Approved Characteristic Tree" in approved_detail.text
|
||||
assert "scope" in approved_detail.text
|
||||
assert "Evidence supporting this capability" in approved_detail.text
|
||||
assert "1 scope" in approved_detail.text
|
||||
assert "supports" in approved_detail.text
|
||||
assert "1 abilities" in approved_detail.text
|
||||
assert "2 capabilities" in approved_detail.text
|
||||
assert "2 features" in approved_detail.text
|
||||
@@ -1261,6 +1263,23 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
|
||||
assert "Save" in approved_listing.text
|
||||
assert "Delete" in approved_listing.text
|
||||
|
||||
scope_listing = client.get(
|
||||
f"/ui/repos/{repository_id}/elements",
|
||||
params={"scope": "all", "entry_filter": "approved", "type": "scopes"},
|
||||
)
|
||||
assert scope_listing.status_code == 200
|
||||
assert "Registry Scopes" in scope_listing.text
|
||||
assert "Save" in scope_listing.text
|
||||
|
||||
support_listing = client.get(
|
||||
f"/ui/repos/{repository_id}/elements",
|
||||
params={"scope": "all", "entry_filter": "approved", "type": "supports"},
|
||||
)
|
||||
assert support_listing.status_code == 200
|
||||
assert "Registry Supports" in support_listing.text
|
||||
assert "supports capability" in support_listing.text
|
||||
assert "references source" in support_listing.text
|
||||
|
||||
combined_listing = client.get(
|
||||
f"/ui/repos/{repository_id}/elements",
|
||||
params={
|
||||
|
||||
@@ -236,3 +236,8 @@ Implementation note 2026-04-29: repository scope is now first-class. A
|
||||
`repository_scopes` row is created for new repositories and backfilled lazily for
|
||||
existing repositories. The ability-map model and API include `scope`, and the UI
|
||||
allows editing the scope root above approved abilities.
|
||||
|
||||
Implementation note 2026-04-29: the element browser now includes approved scope
|
||||
and support/evidence rows. Count badges link to scope and support listings, and
|
||||
support rows show both the supported characteristic target and the referenced
|
||||
source/fact/characteristic metadata.
|
||||
|
||||
Reference in New Issue
Block a user