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 capability in capabilities
|
||||||
for feature in capability.get("features", [])
|
for feature in capability.get("features", [])
|
||||||
]
|
]
|
||||||
|
supports = [
|
||||||
|
evidence
|
||||||
|
for capability in capabilities
|
||||||
|
for evidence in capability.get("evidence", [])
|
||||||
|
]
|
||||||
counts: dict[str, int] = {
|
counts: dict[str, int] = {
|
||||||
|
"scopes": 1 if graph.get("scope") else 0,
|
||||||
"abilities": len(abilities),
|
"abilities": len(abilities),
|
||||||
"capabilities": len(capabilities),
|
"capabilities": len(capabilities),
|
||||||
"features": len(features),
|
"features": len(features),
|
||||||
|
"supports": len(supports),
|
||||||
}
|
}
|
||||||
if facts_count is not None:
|
if facts_count is not None:
|
||||||
counts["facts"] = facts_count
|
counts["facts"] = facts_count
|
||||||
@@ -1983,9 +1990,11 @@ def render_count_pills(
|
|||||||
**counts: int,
|
**counts: int,
|
||||||
) -> str:
|
) -> str:
|
||||||
labels = {
|
labels = {
|
||||||
|
"scopes": "scope",
|
||||||
"abilities": "abilities",
|
"abilities": "abilities",
|
||||||
"capabilities": "capabilities",
|
"capabilities": "capabilities",
|
||||||
"features": "features",
|
"features": "features",
|
||||||
|
"supports": "supports",
|
||||||
"facts": "facts",
|
"facts": "facts",
|
||||||
}
|
}
|
||||||
prefix = f"{label_prefix} " if label_prefix else ""
|
prefix = f"{label_prefix} " if label_prefix else ""
|
||||||
@@ -2025,6 +2034,21 @@ def graph_element_rows(
|
|||||||
entry_state: str = "",
|
entry_state: str = "",
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
rows: 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", []):
|
for ability in graph.get("abilities", []):
|
||||||
if item_type == "abilities":
|
if item_type == "abilities":
|
||||||
rows.append(
|
rows.append(
|
||||||
@@ -2075,6 +2099,27 @@ def graph_element_rows(
|
|||||||
entry_state=entry_state,
|
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
|
return rows
|
||||||
|
|
||||||
|
|
||||||
@@ -2083,6 +2128,8 @@ def element_listing_title(repository_name: str, scope: str, item_type: str) -> s
|
|||||||
"abilities": "Abilities",
|
"abilities": "Abilities",
|
||||||
"capabilities": "Capabilities",
|
"capabilities": "Capabilities",
|
||||||
"features": "Features",
|
"features": "Features",
|
||||||
|
"scopes": "Scopes",
|
||||||
|
"supports": "Supports",
|
||||||
"facts": "Facts",
|
"facts": "Facts",
|
||||||
}
|
}
|
||||||
if scope == "facts":
|
if scope == "facts":
|
||||||
@@ -2159,6 +2206,13 @@ def filter_element_rows(
|
|||||||
str(row["parent"]),
|
str(row["parent"]),
|
||||||
str(row.get("entry_state", "")),
|
str(row.get("entry_state", "")),
|
||||||
str(row.get("status", "")),
|
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"]),
|
source_refs_text(row["source_refs"]),
|
||||||
]
|
]
|
||||||
).lower()
|
).lower()
|
||||||
@@ -2203,12 +2257,26 @@ def render_element_row(
|
|||||||
<td><span class="pill">{escape(str(row["primary_class"]))}</span></td>
|
<td><span class="pill">{escape(str(row["primary_class"]))}</span></td>
|
||||||
<td>{escape(str(row["name"]))}</td>
|
<td>{escape(str(row["name"]))}</td>
|
||||||
<td>{escape(str(row["parent"]))}</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>
|
<td>{render_element_actions(row, repository_id, analysis_run_id)}</td>
|
||||||
</tr>
|
</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(
|
def render_element_actions(
|
||||||
row: dict,
|
row: dict,
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
@@ -2220,6 +2288,17 @@ def render_element_actions(
|
|||||||
return ""
|
return ""
|
||||||
if row.get("entry_state") == "approved":
|
if row.get("entry_state") == "approved":
|
||||||
return render_approved_element_actions(row, repository_id)
|
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 (
|
if (
|
||||||
row.get("entry_state") == "candidate"
|
row.get("entry_state") == "candidate"
|
||||||
and analysis_run_id is not None
|
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:
|
def render_approved_element_actions(row: dict, repository_id: int) -> str:
|
||||||
item_id = row["item_id"]
|
item_id = row["item_id"]
|
||||||
item_kind = row["item_kind"]
|
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"
|
edit_action = f"/ui/repos/{repository_id}/{item_kind}/{item_id}/edit"
|
||||||
delete_action = f"/ui/repos/{repository_id}/{item_kind}/{item_id}/delete"
|
delete_action = f"/ui/repos/{repository_id}/{item_kind}/{item_id}/delete"
|
||||||
hidden_fields = render_element_hidden_fields(row)
|
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(
|
def render_candidate_element_actions(
|
||||||
row: dict,
|
row: dict,
|
||||||
repository_id: int,
|
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:
|
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"]))
|
||||||
|
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":
|
if item_kind == "features":
|
||||||
feature_type = escape(str(row["primary_class"]))
|
feature_type = escape(str(row["primary_class"]))
|
||||||
location = escape(str(row.get("location", "")))
|
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>Type <input name="type" value="{feature_type}" required></label>
|
||||||
<label>Location <input name="location" value="{location}"></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>'
|
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(
|
fields.append(
|
||||||
f'<input type="hidden" name="description" value="{escape(str(row.get("description", "")))}">'
|
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":
|
if item_kind == "capabilities":
|
||||||
inputs = escape(", ".join(row.get("inputs", [])))
|
inputs = escape(", ".join(row.get("inputs", [])))
|
||||||
outputs = escape(", ".join(row.get("outputs", [])))
|
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 "Approved Characteristic Tree" in approved_detail.text
|
||||||
assert "scope" in approved_detail.text
|
assert "scope" in approved_detail.text
|
||||||
assert "Evidence supporting this capability" 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 "1 abilities" in approved_detail.text
|
||||||
assert "2 capabilities" in approved_detail.text
|
assert "2 capabilities" in approved_detail.text
|
||||||
assert "2 features" 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 "Save" in approved_listing.text
|
||||||
assert "Delete" 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(
|
combined_listing = client.get(
|
||||||
f"/ui/repos/{repository_id}/elements",
|
f"/ui/repos/{repository_id}/elements",
|
||||||
params={
|
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
|
`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
|
existing repositories. The ability-map model and API include `scope`, and the UI
|
||||||
allows editing the scope root above approved abilities.
|
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