generated from coulomb/repo-seed
Removed obsolete TODO.md
This commit is contained in:
76
TODO.md
76
TODO.md
@@ -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=<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.
|
|
||||||
@@ -765,6 +765,16 @@ def repository_detail(
|
|||||||
<h2>Review Decisions</h2>
|
<h2>Review Decisions</h2>
|
||||||
{render_review_decisions(decisions)}
|
{render_review_decisions(decisions)}
|
||||||
</section>
|
</section>
|
||||||
|
<section class="panel" style="margin-top:18px">
|
||||||
|
<h2>Classification Quality Feedback</h2>
|
||||||
|
{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))}
|
||||||
|
</section>
|
||||||
<section class="panel" style="margin-top:18px">
|
<section class="panel" style="margin-top:18px">
|
||||||
<h2>Delete Repository</h2>
|
<h2>Delete Repository</h2>
|
||||||
<form class="stack" method="post" action="/ui/repos/{repository_id}/delete">
|
<form class="stack" method="post" action="/ui/repos/{repository_id}/delete">
|
||||||
@@ -1192,15 +1202,12 @@ def analysis_run_detail(
|
|||||||
</section>
|
</section>
|
||||||
<section class="panel" style="margin-top:18px">
|
<section class="panel" style="margin-top:18px">
|
||||||
<h2>Expectation Gaps</h2>
|
<h2>Expectation Gaps</h2>
|
||||||
<form class="stack" method="post" action="/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}/expectation-gaps">
|
{render_expectation_gap_form(
|
||||||
<div class="grid">
|
action=f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}/expectation-gaps",
|
||||||
<label>Expected type <input name="expected_type" placeholder="capability, feature, fact, classification" required></label>
|
default_type="classification",
|
||||||
<label>Expected name <input name="expected_name" placeholder="Use OpenRouter Models" required></label>
|
default_name="Missing or wrong classification",
|
||||||
<label>Source <input name="source" value="human" required></label>
|
default_notes="",
|
||||||
<label>Notes <input name="notes" placeholder="What made you expect this?"></label>
|
)}
|
||||||
</div>
|
|
||||||
<button type="submit">Record Gap</button>
|
|
||||||
</form>
|
|
||||||
{render_expectation_gaps(expectation_gaps)}
|
{render_expectation_gaps(expectation_gaps)}
|
||||||
</section>
|
</section>
|
||||||
"""
|
"""
|
||||||
@@ -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")
|
@router.get("/ui/repos/{repository_id}/elements")
|
||||||
def repository_element_listing(
|
def repository_element_listing(
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
@@ -1308,6 +1334,13 @@ def repository_element_listing(
|
|||||||
<h1 style="margin-right:auto">{escape(title)}</h1>
|
<h1 style="margin-right:auto">{escape(title)}</h1>
|
||||||
<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_element_breadcrumbs(
|
||||||
|
repository_id=repository_id,
|
||||||
|
scope=scope,
|
||||||
|
item_type=type,
|
||||||
|
entry_filter=entry_filter,
|
||||||
|
analysis_run_id=analysis_run_id,
|
||||||
|
)}
|
||||||
<section class="panel" style="margin-bottom:18px">
|
<section class="panel" style="margin-bottom:18px">
|
||||||
<form class="stack" method="get" action="{filter_action}">
|
<form class="stack" method="get" action="{filter_action}">
|
||||||
<input type="hidden" name="scope" value="{escape(listing_scope)}">
|
<input type="hidden" name="scope" value="{escape(listing_scope)}">
|
||||||
@@ -1331,6 +1364,13 @@ def repository_element_listing(
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
|
{render_element_type_nav(
|
||||||
|
repository_id=repository_id,
|
||||||
|
scope=listing_scope,
|
||||||
|
item_type=type,
|
||||||
|
entry_filter=entry_filter,
|
||||||
|
analysis_run_id=analysis_run_id,
|
||||||
|
)}
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>Entry</th><th>Class</th><th>Name</th><th>Parent</th><th>Source</th><th>Actions</th></tr></thead>
|
<thead><tr><th>Entry</th><th>Class</th><th>Name</th><th>Parent</th><th>Source</th><th>Actions</th></tr></thead>
|
||||||
<tbody>{rows}</tbody>
|
<tbody>{rows}</tbody>
|
||||||
@@ -2318,14 +2358,25 @@ def graph_element_rows(
|
|||||||
ability["name"],
|
ability["name"],
|
||||||
"",
|
"",
|
||||||
ability.get("source_refs", []),
|
ability.get("source_refs", []),
|
||||||
item_id=ability.get("id"),
|
item_id=ability.get("id"),
|
||||||
item_kind="abilities",
|
item_kind="abilities",
|
||||||
description=ability.get("description", ""),
|
description=ability.get("description", ""),
|
||||||
confidence=ability.get("confidence", 1.0),
|
confidence=ability.get("confidence", 1.0),
|
||||||
attributes=ability.get("attributes", []),
|
attributes=ability.get("attributes", []),
|
||||||
status=ability.get("status", ""),
|
child_counts={
|
||||||
entry_state=entry_state,
|
"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", []):
|
for capability in ability.get("capabilities", []):
|
||||||
if item_type == "capabilities":
|
if item_type == "capabilities":
|
||||||
@@ -2340,6 +2391,10 @@ def graph_element_rows(
|
|||||||
description=capability.get("description", ""),
|
description=capability.get("description", ""),
|
||||||
confidence=capability.get("confidence", 1.0),
|
confidence=capability.get("confidence", 1.0),
|
||||||
attributes=capability.get("attributes", []),
|
attributes=capability.get("attributes", []),
|
||||||
|
child_counts={
|
||||||
|
"features": len(capability.get("features", [])),
|
||||||
|
"supports": len(capability.get("evidence", [])),
|
||||||
|
},
|
||||||
inputs=capability.get("inputs", []),
|
inputs=capability.get("inputs", []),
|
||||||
outputs=capability.get("outputs", []),
|
outputs=capability.get("outputs", []),
|
||||||
status=capability.get("status", ""),
|
status=capability.get("status", ""),
|
||||||
@@ -2360,6 +2415,7 @@ def graph_element_rows(
|
|||||||
attributes=feature.get("attributes", []),
|
attributes=feature.get("attributes", []),
|
||||||
confidence=feature.get("confidence", 1.0),
|
confidence=feature.get("confidence", 1.0),
|
||||||
location=feature.get("location", ""),
|
location=feature.get("location", ""),
|
||||||
|
child_counts={"facts": len(feature.get("source_refs", []))},
|
||||||
status=feature.get("status", ""),
|
status=feature.get("status", ""),
|
||||||
entry_state=entry_state,
|
entry_state=entry_state,
|
||||||
)
|
)
|
||||||
@@ -2533,7 +2589,7 @@ def render_element_row(
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{render_entry_badge(row)}</td>
|
<td>{render_entry_badge(row)}</td>
|
||||||
<td><span class="pill">{escape(str(row["primary_class"]))}</span>{render_attribute_pills(row)}</td>
|
<td><span class="pill">{escape(str(row["primary_class"]))}</span>{render_attribute_pills(row)}</td>
|
||||||
<td>{escape(str(row["name"]))}</td>
|
<td>{escape(str(row["name"]))}{render_drilldown_links(row, repository_id, analysis_run_id)}</td>
|
||||||
<td>{escape(str(row["parent"]))}</td>
|
<td>{escape(str(row["parent"]))}</td>
|
||||||
<td>{render_element_source_detail(row)}</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>
|
||||||
@@ -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"""
|
||||||
|
<p class="actions">
|
||||||
|
<a class="pill" href="/ui/repos/{repository_id}">Repository</a>
|
||||||
|
<span class="pill">{escape(scope_label)}</span>
|
||||||
|
<a class="pill" href="{href}">{escape(item_type)}</a>
|
||||||
|
</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
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'<a class="pill{active}" href="/ui/repos/{repository_id}/elements?{params}">{escape(label)}</a>'
|
||||||
|
)
|
||||||
|
return f'<p class="actions">{"".join(links)}</p>'
|
||||||
|
|
||||||
|
|
||||||
|
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'<a class="pill" href="/ui/repos/{repository_id}/elements?{params}">{escape(label_text)}</a>'
|
||||||
|
)
|
||||||
|
return f'<p class="actions">{ "".join(rendered) }</p>'
|
||||||
|
|
||||||
|
|
||||||
def render_attribute_pills(row: dict) -> str:
|
def render_attribute_pills(row: dict) -> str:
|
||||||
attributes = [
|
attributes = [
|
||||||
str(attribute)
|
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"""
|
||||||
|
<form class="stack" method="post" action="{action}">
|
||||||
|
<div class="grid">
|
||||||
|
<label>Expected type
|
||||||
|
<select name="expected_type">
|
||||||
|
{render_gap_type_options(default_type)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>Expected name <input name="expected_name" value="{escape(default_name)}" required></label>
|
||||||
|
<label>Source <input name="source" value="human" required></label>
|
||||||
|
<label>Notes <input name="notes" value="{escape(default_notes)}" placeholder="Expected class, attribute, or organization smell"></label>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="submit">Record Gap</button>
|
||||||
|
<button class="secondary" type="submit" name="expected_type" value="classification-primary">Missing Primary Class</button>
|
||||||
|
<button class="secondary" type="submit" name="expected_type" value="classification-attribute">Wrong Attribute</button>
|
||||||
|
<button class="secondary" type="submit" name="expected_type" value="classification-granularity">Granularity Smell</button>
|
||||||
|
<button class="secondary" type="submit" name="expected_type" value="classification-support">Support Organization Smell</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
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'<option value="{escape(option)}"{" selected" if option == selected else ""}>{escape(option)}</option>'
|
||||||
|
for option in options
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def render_expectation_gaps(gaps: list) -> str:
|
def render_expectation_gaps(gaps: list) -> str:
|
||||||
if not gaps:
|
if not gaps:
|
||||||
return '<p class="muted">No expectation gaps recorded for this run.</p>'
|
return '<p class="muted">No expectation gaps recorded for this run.</p>'
|
||||||
@@ -3279,7 +3486,10 @@ def render_ability_map(ability_map: dict, repository_id: int) -> str:
|
|||||||
<li id="capability-{capability['id']}">
|
<li id="capability-{capability['id']}">
|
||||||
<strong>{escape(capability['name'])}</strong>
|
<strong>{escape(capability['name'])}</strong>
|
||||||
<span class="pill">ID {capability['id']}</span>
|
<span class="pill">ID {capability['id']}</span>
|
||||||
|
<span class="pill">{escape(capability.get('primary_class', 'capability'))}</span>
|
||||||
|
{render_characteristic_attributes(capability)}
|
||||||
<span class="pill">{capability['confidence']:.2f} {escape(capability['confidence_label'])}</span>
|
<span class="pill">{capability['confidence']:.2f} {escape(capability['confidence_label'])}</span>
|
||||||
|
{render_approved_tree_drilldown(repository_id, "capabilities", capability)}
|
||||||
<p class="muted">{escape(capability['description'])}</p>
|
<p class="muted">{escape(capability['description'])}</p>
|
||||||
{render_approved_capability_forms(capability, repository_id)}
|
{render_approved_capability_forms(capability, repository_id)}
|
||||||
<h3>Features</h3>
|
<h3>Features</h3>
|
||||||
@@ -3294,7 +3504,10 @@ def render_ability_map(ability_map: dict, repository_id: int) -> str:
|
|||||||
<li id="ability-{ability['id']}">
|
<li id="ability-{ability['id']}">
|
||||||
<strong>{escape(ability['name'])}</strong>
|
<strong>{escape(ability['name'])}</strong>
|
||||||
<span class="pill">ID {ability['id']}</span>
|
<span class="pill">ID {ability['id']}</span>
|
||||||
|
<span class="pill">{escape(ability.get('primary_class', 'ability'))}</span>
|
||||||
|
{render_characteristic_attributes(ability)}
|
||||||
<span class="pill">{ability['confidence']:.2f} {escape(ability['confidence_label'])}</span>
|
<span class="pill">{ability['confidence']:.2f} {escape(ability['confidence_label'])}</span>
|
||||||
|
{render_approved_tree_drilldown(repository_id, "abilities", ability)}
|
||||||
<p class="muted">{escape(ability['description'])}</p>
|
<p class="muted">{escape(ability['description'])}</p>
|
||||||
{render_approved_ability_forms(ability, repository_id)}
|
{render_approved_ability_forms(ability, repository_id)}
|
||||||
<ul>{''.join(capabilities)}</ul>
|
<ul>{''.join(capabilities)}</ul>
|
||||||
@@ -3371,8 +3584,11 @@ def render_approved_feature(feature: dict, repository_id: int) -> str:
|
|||||||
<li>
|
<li>
|
||||||
{escape(feature["name"])}
|
{escape(feature["name"])}
|
||||||
<span class="pill">{escape(feature["type"])}</span>
|
<span class="pill">{escape(feature["type"])}</span>
|
||||||
|
<span class="pill">{escape(feature.get("primary_class", feature["type"]))}</span>
|
||||||
|
{render_characteristic_attributes(feature)}
|
||||||
<span class="pill">{feature["confidence"]:.2f} {escape(feature["confidence_label"])}</span>
|
<span class="pill">{feature["confidence"]:.2f} {escape(feature["confidence_label"])}</span>
|
||||||
<span class="source">{escape(feature["location"])}</span>
|
<span class="source">{escape(feature["location"])}</span>
|
||||||
|
{render_approved_tree_drilldown(repository_id, "features", feature)}
|
||||||
{render_sources(feature.get("source_refs", []))}
|
{render_sources(feature.get("source_refs", []))}
|
||||||
<form class="stack" method="post" action="/ui/repos/{repository_id}/features/{feature['id']}/edit">
|
<form class="stack" method="post" action="/ui/repos/{repository_id}/features/{feature['id']}/edit">
|
||||||
<label>Name <input name="name" value="{escape(feature['name'])}" required></label>
|
<label>Name <input name="name" value="{escape(feature['name'])}" required></label>
|
||||||
@@ -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' <span class="pill">{escape(attribute)}</span>'
|
||||||
|
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'<a class="pill" href="/ui/repos/{repository_id}/elements?{params}">{count} {escape(target_type)}</a>'
|
||||||
|
)
|
||||||
|
return f'<p class="actions">{"".join(rendered)}</p>'
|
||||||
|
|
||||||
|
|
||||||
def render_approved_evidence(evidence: dict, repository_id: int) -> str:
|
def render_approved_evidence(evidence: dict, repository_id: int) -> str:
|
||||||
target_kind = escape(str(evidence.get("target_kind") or "capability"))
|
target_kind = escape(str(evidence.get("target_kind") or "capability"))
|
||||||
target_id = evidence.get("target_id")
|
target_id = evidence.get("target_id")
|
||||||
|
|||||||
@@ -1873,6 +1873,12 @@ def test_ui_manual_registry_entry_loop(tmp_path):
|
|||||||
assert "Edited Manual Ability" in detail_response.text
|
assert "Edited Manual Ability" in detail_response.text
|
||||||
assert "Edited Manual Capability" in detail_response.text
|
assert "Edited Manual Capability" in detail_response.text
|
||||||
assert "Edited Manual API" 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 "tests/test_manual.py" in detail_response.text
|
||||||
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
|
||||||
@@ -1900,9 +1906,26 @@ def test_ui_manual_registry_entry_loop(tmp_path):
|
|||||||
)
|
)
|
||||||
assert filtered_feature_listing.status_code == 200
|
assert filtered_feature_listing.status_code == 200
|
||||||
assert "Attribute" in filtered_feature_listing.text
|
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 "Edited Manual API" in filtered_feature_listing.text
|
||||||
assert "1 of 1 shown" 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(
|
upward_support_listing = client.get(
|
||||||
f"/ui/repos/{repository_id}/elements",
|
f"/ui/repos/{repository_id}/elements",
|
||||||
params={
|
params={
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ type: workplan
|
|||||||
title: "Repository Ability Registry - Characteristic Classification And Navigation"
|
title: "Repository Ability Registry - Characteristic Classification And Navigation"
|
||||||
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-29"
|
created: "2026-04-29"
|
||||||
@@ -94,7 +94,7 @@ surface/provider attributes such as `api`, `cli`, `http`, `llm-provider`,
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: RREG-WP-0004-T04
|
id: RREG-WP-0004-T04
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "14ede41f-a0cb-4a9a-b4ba-f23b34d7ae33"
|
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
|
from scope to abilities to lower-level support/facts, with counts, filters, and
|
||||||
clear breadcrumbs.
|
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
|
## P2: Classification Quality Feedback
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RREG-WP-0004-T05
|
id: RREG-WP-0004-T05
|
||||||
status: todo
|
status: done
|
||||||
priority: low
|
priority: low
|
||||||
state_hub_task_id: "691f3cb7-c8a2-4f80-a6c2-29cb5a0c7a96"
|
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
|
Acceptance: reviewers can record classification-specific improvement inputs that
|
||||||
feed the scanner coevolution workflow.
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user