diff --git a/src/repo_registry/web_ui/views.py b/src/repo_registry/web_ui/views.py index c554c90..2ee0e00 100644 --- a/src/repo_registry/web_ui/views.py +++ b/src/repo_registry/web_ui/views.py @@ -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( {escape(str(row["primary_class"]))} {escape(str(row["name"]))} {escape(str(row["parent"]))} - {render_sources(row["source_refs"])} + {render_element_source_detail(row)} {render_element_actions(row, repository_id, analysis_run_id)} """ +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'

supports {target}{f" #{target_id}" if target_id else ""}' + f' references {reference_kind}{f" #{reference_id}" if reference_id else ""}

' + 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""" +
+ {render_element_edit_fields(row)} + +
+ """ 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""" +
+ +
+
+ + +
+
+ + +
+ """ + + 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""" + + + + """ 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: """ + if item_kind == "evidence": + return f""" + + + + + + + + """ return f'' @@ -2371,6 +2506,10 @@ def render_element_hidden_fields(row: dict) -> str: fields.append( f'' ) + if item_kind == "scope": + fields.append( + f'' + ) if item_kind == "capabilities": inputs = escape(", ".join(row.get("inputs", []))) outputs = escape(", ".join(row.get("outputs", []))) diff --git a/tests/test_web_api.py b/tests/test_web_api.py index 7263098..3a7b09a 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -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={ diff --git a/workplans/RREG-WP-0003-automatic-repository-exploration.md b/workplans/RREG-WP-0003-automatic-repository-exploration.md index bce4b3b..8376894 100644 --- a/workplans/RREG-WP-0003-automatic-repository-exploration.md +++ b/workplans/RREG-WP-0003-automatic-repository-exploration.md @@ -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.