From 39612bde537d8463198954e0c960076e5de369ad Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 30 Apr 2026 14:04:22 +0200 Subject: [PATCH] Removed obsolete TODO.md --- TODO.md | 76 ----- src/repo_registry/web_ui/views.py | 316 +++++++++++++++++- tests/test_web_api.py | 23 ++ ...haracteristic-classification-navigation.md | 19 +- 4 files changed, 337 insertions(+), 97 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 6e39081..0000000 --- a/TODO.md +++ /dev/null @@ -1,76 +0,0 @@ -# TODO — Custodian Integration Notes - -These notes are for the Codex agent working in this repository. They document the -current state of the Custodian State Hub integration and what to be aware of. - ---- - -## Git Push Credentials (C-17 Warning) - -The Custodian consistency checker warns when local commits have not been pushed to -the remote. The onboarding commit (AGENTS.md, SCOPE.md, ADR-001 workplans) is -currently unpushed because Gitea HTTP credentials are not configured in this -environment. - -This does not block normal work, but the checker will warn at each run. - -To resolve, either configure HTTP credentials: - -```bash -git config credential.helper store -git push # prompts once, then stores -``` - -Or switch the remote to SSH (if an SSH key is registered on Gitea): - -```bash -git remote set-url origin git@92.205.130.254:coulomb/repo-registry.git -git push -``` - -Until resolved, the consistency checker will log C-17 and skip file write-backs. -Task statuses you update in workplan files will still be visible locally; they will -sync to the hub DB once the push succeeds. - ---- - -## AGENTS.md - -`AGENTS.md` at the repo root is your primary instruction file for interacting with -the Custodian State Hub. Read it at the start of each session. It documents: - -- State Hub HTTP API endpoints (orient, inbox, progress logging) -- Session protocol (start / during / close) -- Workplan file convention (ADR-001 format) -- How to create new workplans and notify the hub - ---- - -## Active Workplan - -`workplans/RREG-WP-0004-characteristic-classification-navigation.md` is the -current active workplan. Start with T01 (P0: Characteristic Classification Fields) -as the highest-priority item. - ---- - -## Custodian Tooling Improvements (2026-04-26) - -The following improvements were made to the Custodian registration tooling as a -result of onboarding this repository as the first Codex-based repo: - -1. **`register_project.sh --codex` flag** — new flag skips MCP registration check - (Claude Code-specific) and generates `AGENTS.md` from an HTTP-API-based template - instead of `CLAUDE.md` and `.claude/rules/`. Future Codex repos can use: - ```bash - cd ~/the-custodian/state-hub - make register-codex-project DOMAIN= PROJECT_PATH=/path/to/repo - ``` - -2. **`agents-codex.template`** — new parameterised template in - `state-hub/scripts/project_rules/` that produces the HTTP REST session protocol - for Codex agents (this file was used to generate `AGENTS.md` in this repo). - -These changes are committed in `the-custodian`. If the tooling needs further -refinement based on how Codex works with this integration, note it here and the -custodian operator will update the templates. diff --git a/src/repo_registry/web_ui/views.py b/src/repo_registry/web_ui/views.py index ade3674..0411aa8 100644 --- a/src/repo_registry/web_ui/views.py +++ b/src/repo_registry/web_ui/views.py @@ -765,6 +765,16 @@ def repository_detail(

Review Decisions

{render_review_decisions(decisions)} +
+

Classification Quality Feedback

+ {render_expectation_gap_form( + action=f"/ui/repos/{repository_id}/expectation-gaps", + default_type="classification", + default_name="Wrong or missing characteristic classification", + default_notes="", + )} + {render_expectation_gaps(service.list_expectation_gaps(repository_id))} +

Delete Repository

@@ -1192,15 +1202,12 @@ def analysis_run_detail(

Expectation Gaps

- -
- - - - -
- - + {render_expectation_gap_form( + action=f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}/expectation-gaps", + default_type="classification", + default_name="Missing or wrong classification", + default_notes="", + )} {render_expectation_gaps(expectation_gaps)}
""" @@ -1231,6 +1238,25 @@ def create_expectation_gap_from_form( ) +@router.post("/ui/repos/{repository_id}/expectation-gaps") +def create_repository_expectation_gap_from_form( + repository_id: int, + expected_type: str = Form(...), + expected_name: str = Form(...), + source: str = Form("human"), + notes: str = Form(""), + service: RegistryService = Depends(get_service), +) -> RedirectResponse: + service.record_expectation_gap( + repository_id, + expected_type=expected_type, + expected_name=expected_name, + source=source, + notes=notes, + ) + return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303) + + @router.get("/ui/repos/{repository_id}/elements") def repository_element_listing( repository_id: int, @@ -1308,6 +1334,13 @@ def repository_element_listing(

{escape(title)}

Repository + {render_element_breadcrumbs( + repository_id=repository_id, + scope=scope, + item_type=type, + entry_filter=entry_filter, + analysis_run_id=analysis_run_id, + )}
@@ -1331,6 +1364,13 @@ def repository_element_listing(
+ {render_element_type_nav( + repository_id=repository_id, + scope=listing_scope, + item_type=type, + entry_filter=entry_filter, + analysis_run_id=analysis_run_id, + )} {rows} @@ -2318,14 +2358,25 @@ def graph_element_rows( ability["name"], "", ability.get("source_refs", []), - item_id=ability.get("id"), - item_kind="abilities", - description=ability.get("description", ""), - confidence=ability.get("confidence", 1.0), - attributes=ability.get("attributes", []), - status=ability.get("status", ""), - entry_state=entry_state, - ) + item_id=ability.get("id"), + item_kind="abilities", + description=ability.get("description", ""), + confidence=ability.get("confidence", 1.0), + attributes=ability.get("attributes", []), + child_counts={ + "capabilities": len(ability.get("capabilities", [])), + "features": sum( + len(capability.get("features", [])) + for capability in ability.get("capabilities", []) + ), + "supports": sum( + len(capability.get("evidence", [])) + for capability in ability.get("capabilities", []) + ), + }, + status=ability.get("status", ""), + entry_state=entry_state, + ) ) for capability in ability.get("capabilities", []): if item_type == "capabilities": @@ -2340,6 +2391,10 @@ def graph_element_rows( description=capability.get("description", ""), confidence=capability.get("confidence", 1.0), attributes=capability.get("attributes", []), + child_counts={ + "features": len(capability.get("features", [])), + "supports": len(capability.get("evidence", [])), + }, inputs=capability.get("inputs", []), outputs=capability.get("outputs", []), status=capability.get("status", ""), @@ -2360,6 +2415,7 @@ def graph_element_rows( attributes=feature.get("attributes", []), confidence=feature.get("confidence", 1.0), location=feature.get("location", ""), + child_counts={"facts": len(feature.get("source_refs", []))}, status=feature.get("status", ""), entry_state=entry_state, ) @@ -2533,7 +2589,7 @@ def render_element_row( - + @@ -2541,6 +2597,109 @@ def render_element_row( """ +def render_element_breadcrumbs( + *, + repository_id: int, + scope: str, + item_type: str, + entry_filter: str, + analysis_run_id: int | None, +) -> str: + scope_label = "Facts" if scope == "facts" else (entry_filter or "all") + href = ( + f"/ui/repos/{repository_id}/elements?scope={escape(scope)}" + f"&type={escape(item_type)}{render_analysis_run_query_suffix(analysis_run_id)}" + ) + return f""" +

+ Repository + {escape(scope_label)} + {escape(item_type)} +

+ """ + + +def render_element_type_nav( + *, + repository_id: int, + scope: str, + item_type: str, + entry_filter: str, + analysis_run_id: int | None, +) -> str: + items = [ + ("scopes", "Scope"), + ("abilities", "Abilities"), + ("capabilities", "Capabilities"), + ("features", "Features"), + ("supports", "Supports"), + ("facts", "Facts"), + ] + links = [] + for value, label in items: + link_scope = "facts" if value == "facts" else scope + params = ( + f"scope={quote_plus(link_scope)}&type={quote_plus(value)}" + f"{render_analysis_run_query_suffix(analysis_run_id)}" + ) + if value != "facts" and entry_filter: + params += f"&entry_filter={quote_plus(entry_filter)}" + active = " active" if value == item_type else "" + links.append( + f'{escape(label)}' + ) + return f'

{"".join(links)}

' + + +def render_drilldown_links( + row: dict, + repository_id: int, + analysis_run_id: int | None, +) -> str: + item_kind = row.get("item_kind") + entry_state = str(row.get("entry_state") or "approved") + counts = row.get("child_counts", {}) + links: list[tuple[str, str, str]] = [] + if item_kind == "scope": + links.append(("abilities", "abilities", "")) + elif item_kind == "abilities": + links.extend( + [ + ("capabilities", "capabilities", str(row["name"])), + ("features", "features", str(row["name"])), + ("supports", "supports", str(row["name"])), + ] + ) + elif item_kind == "capabilities": + links.extend( + [ + ("features", "features", str(row["name"])), + ("supports", "supports", str(row["name"])), + ] + ) + elif item_kind == "features": + fact_query = str(row.get("location") or row.get("name") or "") + links.append(("facts", "facts", fact_query)) + if not links: + return "" + rendered = [] + for item_type, label, query in links: + count = counts.get(item_type) + label_text = f"{count} {label}" if count is not None else label + scope = "facts" if item_type == "facts" else "all" + params = f"scope={scope}&type={item_type}" + if item_type != "facts": + params += f"&entry_filter={quote_plus(entry_state)}" + if query: + params += f"&q={quote_plus(query)}" + if analysis_run_id is not None: + params += f"&analysis_run_id={analysis_run_id}" + rendered.append( + f'{escape(label_text)}' + ) + return f'

{ "".join(rendered) }

' + + def render_attribute_pills(row: dict) -> str: attributes = [ str(attribute) @@ -3028,6 +3187,54 @@ def render_review_decisions(decisions: list) -> str: """ +def render_expectation_gap_form( + *, + action: str, + default_type: str, + default_name: str, + default_notes: str, +) -> str: + return f""" + +
+ + + + +
+
+ + + + + +
+ + """ + + +def render_gap_type_options(selected: str) -> str: + options = [ + "ability", + "capability", + "feature", + "fact", + "classification", + "classification-primary", + "classification-attribute", + "classification-granularity", + "classification-support", + ] + return "".join( + f'' + for option in options + ) + + def render_expectation_gaps(gaps: list) -> str: if not gaps: return '

No expectation gaps recorded for this run.

' @@ -3279,7 +3486,10 @@ def render_ability_map(ability_map: dict, repository_id: int) -> str:
  • {escape(capability['name'])} ID {capability['id']} + {escape(capability.get('primary_class', 'capability'))} + {render_characteristic_attributes(capability)} {capability['confidence']:.2f} {escape(capability['confidence_label'])} + {render_approved_tree_drilldown(repository_id, "capabilities", capability)}

    {escape(capability['description'])}

    {render_approved_capability_forms(capability, repository_id)}

    Features

    @@ -3294,7 +3504,10 @@ def render_ability_map(ability_map: dict, repository_id: int) -> str:
  • {escape(ability['name'])} ID {ability['id']} + {escape(ability.get('primary_class', 'ability'))} + {render_characteristic_attributes(ability)} {ability['confidence']:.2f} {escape(ability['confidence_label'])} + {render_approved_tree_drilldown(repository_id, "abilities", ability)}

    {escape(ability['description'])}

    {render_approved_ability_forms(ability, repository_id)}
      {''.join(capabilities)}
    @@ -3371,8 +3584,11 @@ def render_approved_feature(feature: dict, repository_id: int) -> str:
  • {escape(feature["name"])} {escape(feature["type"])} + {escape(feature.get("primary_class", feature["type"]))} + {render_characteristic_attributes(feature)} {feature["confidence"]:.2f} {escape(feature["confidence_label"])} {escape(feature["location"])} + {render_approved_tree_drilldown(repository_id, "features", feature)} {render_sources(feature.get("source_refs", []))}
    @@ -3388,6 +3604,70 @@ def render_approved_feature(feature: dict, repository_id: int) -> str: """ +def render_characteristic_attributes(item: dict) -> str: + primary = str(item.get("primary_class", "")) + attributes = [ + str(attribute) + for attribute in item.get("attributes", []) + if str(attribute) and str(attribute) != primary + ] + return "".join( + f' {escape(attribute)}' + for attribute in attributes + ) + + +def render_approved_tree_drilldown( + repository_id: int, + item_kind: str, + item: dict, +) -> str: + name = str(item.get("name", "")) + if item_kind == "abilities": + links = [ + ("capabilities", len(item.get("capabilities", [])), name), + ( + "features", + sum( + len(capability.get("features", [])) + for capability in item.get("capabilities", []) + ), + name, + ), + ( + "supports", + sum( + len(capability.get("evidence", [])) + for capability in item.get("capabilities", []) + ), + name, + ), + ] + elif item_kind == "capabilities": + links = [ + ("features", len(item.get("features", [])), name), + ("supports", len(item.get("evidence", [])), name), + ] + elif item_kind == "features": + links = [("facts", len(item.get("source_refs", [])), item.get("location", name))] + else: + links = [] + if not links: + return "" + rendered = [] + for target_type, count, query in links: + scope = "facts" if target_type == "facts" else "all" + params = f"scope={scope}&type={target_type}" + if target_type != "facts": + params += "&entry_filter=approved" + if query: + params += f"&q={quote_plus(str(query))}" + rendered.append( + f'{count} {escape(target_type)}' + ) + return f'

    {"".join(rendered)}

    ' + + def render_approved_evidence(evidence: dict, repository_id: int) -> str: target_kind = escape(str(evidence.get("target_kind") or "capability")) target_id = evidence.get("target_id") diff --git a/tests/test_web_api.py b/tests/test_web_api.py index d268765..60c3427 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -1873,6 +1873,12 @@ def test_ui_manual_registry_entry_loop(tmp_path): assert "Edited Manual Ability" in detail_response.text assert "Edited Manual Capability" in detail_response.text assert "Edited Manual API" in detail_response.text + assert "Classification Quality Feedback" in detail_response.text + assert "classification-primary" in detail_response.text + assert "1 capabilities" in detail_response.text + assert "1 features" in detail_response.text + assert "2 supports" in detail_response.text + assert "0 facts" in detail_response.text assert "tests/test_manual.py" in detail_response.text assert f"references feature #{feature_id}" in detail_response.text assert "downward support" in detail_response.text @@ -1900,9 +1906,26 @@ def test_ui_manual_registry_entry_loop(tmp_path): ) assert filtered_feature_listing.status_code == 200 assert "Attribute" in filtered_feature_listing.text + assert "Repository" in filtered_feature_listing.text + assert "Facts" in filtered_feature_listing.text assert "Edited Manual API" in filtered_feature_listing.text assert "1 of 1 shown" in filtered_feature_listing.text + classification_gap_response = client.post( + f"{repository_path}/expectation-gaps", + data={ + "expected_type": "classification-support", + "expected_name": "Feature references another feature too broadly", + "source": "human", + "notes": "Same-level support should be reviewed.", + }, + follow_redirects=False, + ) + assert classification_gap_response.status_code == 303 + detail_response = client.get(repository_path) + assert "Feature references another feature too broadly" in detail_response.text + assert "classification-support" in detail_response.text + upward_support_listing = client.get( f"/ui/repos/{repository_id}/elements", params={ diff --git a/workplans/RREG-WP-0004-characteristic-classification-navigation.md b/workplans/RREG-WP-0004-characteristic-classification-navigation.md index a42dfe1..0e75055 100644 --- a/workplans/RREG-WP-0004-characteristic-classification-navigation.md +++ b/workplans/RREG-WP-0004-characteristic-classification-navigation.md @@ -4,7 +4,7 @@ type: workplan title: "Repository Ability Registry - Characteristic Classification And Navigation" domain: capabilities repo: repo-registry -status: active +status: done owner: codex topic_slug: foerster-capabilities created: "2026-04-29" @@ -94,7 +94,7 @@ surface/provider attributes such as `api`, `cli`, `http`, `llm-provider`, ```task id: RREG-WP-0004-T04 -status: todo +status: done priority: medium state_hub_task_id: "14ede41f-a0cb-4a9a-b4ba-f23b34d7ae33" ``` @@ -107,11 +107,18 @@ Acceptance: the repository page and element listings make it natural to move from scope to abilities to lower-level support/facts, with counts, filters, and clear breadcrumbs. +Implementation note 2026-04-30: approved characteristic trees and element +listings now expose explicit drilldown links. Abilities link to capabilities, +features, and supports; capabilities link to features and supports; features +link to observed facts by source path. Element listings include breadcrumbs and +a type navigation row for scope, abilities, capabilities, features, supports, +and facts. + ## P2: Classification Quality Feedback ```task id: RREG-WP-0004-T05 -status: todo +status: done priority: low state_hub_task_id: "691f3cb7-c8a2-4f80-a6c2-29cb5a0c7a96" ``` @@ -122,3 +129,9 @@ same-level/upward support patterns that indicate suboptimal organization. Acceptance: reviewers can record classification-specific improvement inputs that feed the scanner coevolution workflow. + +Implementation note 2026-04-30: classification feedback is captured through +expectation gaps with dedicated types for missing primary classes, wrong +attributes, granularity smells, and support organization smells. Run pages and +repository pages expose the feedback form so reviewers can record optimization +inputs with or without a specific analysis run.
  • EntryClassNameParentSourceActions
    {render_entry_badge(row)} {escape(str(row["primary_class"]))}{render_attribute_pills(row)}{escape(str(row["name"]))}{escape(str(row["name"]))}{render_drilldown_links(row, repository_id, analysis_run_id)} {escape(str(row["parent"]))} {render_element_source_detail(row)} {render_element_actions(row, repository_id, analysis_run_id)}