Improved datamodel and deterministic generation

This commit is contained in:
2026-04-30 01:29:29 +02:00
parent 973d4bbe7c
commit 26e87ab52c
14 changed files with 848 additions and 39 deletions

View File

@@ -719,6 +719,8 @@ def repository_detail(
<h3>Add Ability</h3>
<label>Name <input name="name" required></label>
<label>Description <textarea name="description" rows="2"></textarea></label>
<label>Primary class <input name="primary_class" value="ability" required></label>
<label>Attributes <input name="attributes" placeholder="Comma-separated"></label>
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="1.0" required></label>
<button type="submit">Add Ability</button>
</form>
@@ -729,6 +731,8 @@ def repository_detail(
<label>Description <textarea name="description" rows="2"></textarea></label>
<label>Inputs <input name="inputs" placeholder="Comma-separated"></label>
<label>Outputs <input name="outputs" placeholder="Comma-separated"></label>
<label>Primary class <input name="primary_class" value="capability" required></label>
<label>Attributes <input name="attributes" placeholder="Comma-separated"></label>
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="1.0" required></label>
<button type="submit">Add Capability</button>
</form>
@@ -737,6 +741,8 @@ def repository_detail(
<label>Capability ID <input name="capability_id" type="number" min="1" required></label>
<label>Name <input name="name" required></label>
<label>Type <input name="type" required></label>
<label>Primary class <input name="primary_class" placeholder="Defaults to type"></label>
<label>Attributes <input name="attributes" placeholder="Comma-separated; defaults to type"></label>
<label>Location <input name="location"></label>
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="1.0" required></label>
<button type="submit">Add Feature</button>
@@ -835,6 +841,8 @@ def create_ability_from_form(
name: str = Form(...),
description: str = Form(""),
confidence: float = Form(1.0),
primary_class: str = Form("ability"),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.add_ability(
@@ -842,6 +850,8 @@ def create_ability_from_form(
name=name,
description=description,
confidence=confidence,
primary_class=primary_class or "ability",
attributes=split_csv(attributes),
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@@ -855,6 +865,8 @@ def create_capability_from_form(
inputs: str = Form(""),
outputs: str = Form(""),
confidence: float = Form(1.0),
primary_class: str = Form("capability"),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.add_capability(
@@ -865,6 +877,8 @@ def create_capability_from_form(
inputs=split_csv(inputs),
outputs=split_csv(outputs),
confidence=confidence,
primary_class=primary_class or "capability",
attributes=split_csv(attributes),
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@@ -877,6 +891,8 @@ def create_feature_from_form(
type: str = Form(...),
location: str = Form(""),
confidence: float = Form(1.0),
primary_class: str = Form(""),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.add_feature(
@@ -886,6 +902,8 @@ def create_feature_from_form(
type=type,
location=location,
confidence=confidence,
primary_class=primary_class or type,
attributes=split_csv(attributes) or [type],
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@@ -924,6 +942,8 @@ def edit_ability_from_form(
name: str = Form(...),
description: str = Form(""),
confidence: float = Form(1.0),
primary_class: str = Form("ability"),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.update_ability(
@@ -932,6 +952,8 @@ def edit_ability_from_form(
name=name,
description=description,
confidence=confidence,
primary_class=primary_class or "ability",
attributes=split_csv(attributes),
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@@ -955,6 +977,8 @@ def edit_capability_from_form(
inputs: str = Form(""),
outputs: str = Form(""),
confidence: float = Form(1.0),
primary_class: str = Form("capability"),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.update_capability(
@@ -965,6 +989,8 @@ def edit_capability_from_form(
inputs=split_csv(inputs),
outputs=split_csv(outputs),
confidence=confidence,
primary_class=primary_class or "capability",
attributes=split_csv(attributes),
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@@ -987,6 +1013,8 @@ def edit_feature_from_form(
type: str = Form(...),
location: str = Form(""),
confidence: float = Form(1.0),
primary_class: str = Form(""),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.update_feature(
@@ -996,6 +1024,8 @@ def edit_feature_from_form(
type=type,
location=location,
confidence=confidence,
primary_class=primary_class or type,
attributes=split_csv(attributes) or [type],
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@@ -1208,6 +1238,7 @@ def repository_element_listing(
type: str = Query("abilities"),
q: str = Query(""),
class_filter: str = Query(""),
attribute_filter: str = Query(""),
entry_filter: str = Query(""),
candidate_status_filter: str = Query("active"),
support_orientation_filter: str = Query(""),
@@ -1252,13 +1283,15 @@ def repository_element_listing(
elements,
"",
"",
entry_filter,
candidate_status_filter,
"",
entry_filter=entry_filter,
candidate_status_filter=candidate_status_filter,
)
filtered = filter_element_rows(
entry_scoped_elements,
q,
class_filter,
attribute_filter,
candidate_status_filter="all",
support_orientation_filter=support_orientation_filter,
)
@@ -1283,11 +1316,13 @@ def repository_element_listing(
<div class="grid">
<label>Search <input name="q" value="{escape(q)}" placeholder="Name, parent, source, or class"></label>
<label>Class <input name="class_filter" value="{escape(class_filter)}" list="element-classes" placeholder="Any class"></label>
<label>Attribute <input name="attribute_filter" value="{escape(attribute_filter)}" list="element-attributes" placeholder="Any attribute"></label>
{render_entry_filter(entry_filter) if scope != "facts" else ""}
{render_candidate_status_filter(candidate_status_filter) if scope != "facts" else ""}
{render_support_orientation_filter(support_orientation_filter) if type in {"supports", "evidence"} else ""}
</div>
{render_class_datalist(entry_scoped_elements)}
{render_attribute_datalist(entry_scoped_elements)}
<div class="actions">
<button type="submit">Filter</button>
<a class="button secondary" href="{filter_action}?scope={escape(listing_scope)}&type={escape(type)}{render_analysis_run_query_suffix(analysis_run_id)}">Clear</a>
@@ -1562,6 +1597,8 @@ def edit_candidate_ability_from_form(
name: str = Form(...),
description: str = Form(""),
confidence: float = Form(...),
primary_class: str = Form("ability"),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.edit_candidate_ability(
@@ -1571,6 +1608,8 @@ def edit_candidate_ability_from_form(
name=name,
description=description,
confidence=confidence,
primary_class=primary_class or "ability",
attributes=split_csv(attributes),
notes="Edited from web UI",
)
return RedirectResponse(
@@ -1590,6 +1629,8 @@ def edit_candidate_capability_from_form(
name: str = Form(...),
description: str = Form(""),
confidence: float = Form(...),
primary_class: str = Form("capability"),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.edit_candidate_capability(
@@ -1599,6 +1640,42 @@ def edit_candidate_capability_from_form(
name=name,
description=description,
confidence=confidence,
primary_class=primary_class or "capability",
attributes=split_csv(attributes),
notes="Edited from web UI",
)
return RedirectResponse(
f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}",
status_code=303,
)
@router.post(
"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}"
"/candidate-features/{candidate_feature_id}/edit"
)
def edit_candidate_feature_from_form(
repository_id: int,
analysis_run_id: int,
candidate_feature_id: int,
name: str = Form(...),
type: str = Form(...),
location: str = Form(""),
confidence: float = Form(...),
primary_class: str = Form(""),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.edit_candidate_feature(
repository_id,
analysis_run_id,
candidate_feature_id,
name=name,
type=type,
location=location,
confidence=confidence,
primary_class=primary_class or type,
attributes=split_csv(attributes) or [type],
notes="Edited from web UI",
)
return RedirectResponse(
@@ -2245,6 +2322,7 @@ def graph_element_rows(
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,
)
@@ -2261,6 +2339,7 @@ def graph_element_rows(
item_kind="capabilities",
description=capability.get("description", ""),
confidence=capability.get("confidence", 1.0),
attributes=capability.get("attributes", []),
inputs=capability.get("inputs", []),
outputs=capability.get("outputs", []),
status=capability.get("status", ""),
@@ -2271,12 +2350,14 @@ def graph_element_rows(
if item_type == "features":
rows.append(
element_row(
feature.get("type", "feature"),
feature.get("primary_class", feature.get("type", "feature")),
feature["name"],
capability["name"],
feature.get("source_refs", []),
item_id=feature.get("id"),
item_kind="features",
type=feature.get("type", ""),
attributes=feature.get("attributes", []),
confidence=feature.get("confidence", 1.0),
location=feature.get("location", ""),
status=feature.get("status", ""),
@@ -2367,12 +2448,14 @@ def filter_element_rows(
rows: list[dict],
query: str,
class_filter: str,
attribute_filter: str = "",
entry_filter: str = "",
candidate_status_filter: str = "",
support_orientation_filter: str = "",
) -> list[dict]:
query = query.strip().lower()
class_filter = class_filter.strip().lower()
attribute_filter = attribute_filter.strip().lower()
entry_filter = entry_filter.strip().lower()
candidate_status_filter = candidate_status_filter.strip().lower()
support_orientation_filter = support_orientation_filter.strip().lower()
@@ -2387,9 +2470,15 @@ def filter_element_rows(
row_class = str(row["primary_class"]).lower()
if class_filter and class_filter not in row_class:
continue
row_attributes = [str(item).lower() for item in row.get("attributes", [])]
if attribute_filter and not any(
attribute_filter in item for item in row_attributes
):
continue
haystack = " ".join(
[
str(row["primary_class"]),
" ".join(str(item) for item in row.get("attributes", [])),
str(row["name"]),
str(row["parent"]),
str(row.get("entry_state", "")),
@@ -2443,7 +2532,7 @@ def render_element_row(
return f"""
<tr>
<td>{render_entry_badge(row)}</td>
<td><span class="pill">{escape(str(row["primary_class"]))}</span></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["parent"]))}</td>
<td>{render_element_source_detail(row)}</td>
@@ -2452,6 +2541,20 @@ def render_element_row(
"""
def render_attribute_pills(row: dict) -> str:
attributes = [
str(attribute)
for attribute in row.get("attributes", [])
if str(attribute) and str(attribute) != str(row.get("primary_class", ""))
]
if not attributes:
return ""
return "".join(
f' <span class="pill">{escape(attribute)}</span>'
for attribute in attributes
)
def render_element_source_detail(row: dict) -> str:
if row.get("item_kind") == "evidence":
target = escape(str(row.get("target_kind") or "capability"))
@@ -2647,7 +2750,7 @@ def render_candidate_element_actions(
)
status = row.get("status", "candidate")
edit = ""
if item_kind in {"abilities", "capabilities"}:
if item_kind in {"abilities", "capabilities", "features"}:
edit_action = (
f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}"
f"/{collection}/{item_id}/edit"
@@ -2722,6 +2825,7 @@ def render_support_orientation_filter(support_orientation_filter: str) -> str:
def render_element_edit_fields(row: dict) -> str:
item_kind = row["item_kind"]
name = escape(str(row["name"]))
classification = render_classification_edit_fields(row)
if item_kind == "scope":
return f"""
<label>Name <input name="name" value="{name}" required></label>
@@ -2734,6 +2838,7 @@ def render_element_edit_fields(row: dict) -> str:
return f"""
<label>Name <input name="name" value="{name}" required></label>
<label>Type <input name="type" value="{feature_type}" required></label>
{classification}
<label>Location <input name="location" value="{location}"></label>
"""
if item_kind == "evidence":
@@ -2746,7 +2851,10 @@ def render_element_edit_fields(row: dict) -> str:
<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>
{classification}
"""
def render_element_hidden_fields(row: dict) -> str:
@@ -2774,7 +2882,17 @@ def render_element_hidden_fields(row: dict) -> str:
def render_candidate_edit_fields(row: dict) -> str:
return f'<label>Name <input name="name" value="{escape(str(row["name"]))}" required></label>'
if row.get("item_kind") == "features":
return f"""
<label>Name <input name="name" value="{escape(str(row["name"]))}" required></label>
<label>Type <input name="type" value="{escape(str(row.get("type", row.get("primary_class", ""))))}" required></label>
{render_classification_edit_fields(row)}
<label>Location <input name="location" value="{escape(str(row.get("location", "")))}"></label>
"""
return f"""
<label>Name <input name="name" value="{escape(str(row["name"]))}" required></label>
{render_classification_edit_fields(row)}
"""
def render_candidate_hidden_fields(row: dict) -> str:
@@ -2784,6 +2902,17 @@ def render_candidate_hidden_fields(row: dict) -> str:
)
def render_classification_edit_fields(row: dict) -> str:
return f"""
<label>Primary class <input name="primary_class" value="{escape(str(row.get("primary_class", "")))}" required></label>
<label>Attributes <input name="attributes" value="{escape(attributes_text(row))}" placeholder="Comma-separated"></label>
"""
def attributes_text(row: dict) -> str:
return ", ".join(str(item) for item in row.get("attributes", []) if str(item))
def render_class_datalist(rows: list[dict]) -> str:
classes = sorted({str(row["primary_class"]) for row in rows if row["primary_class"]})
options = "".join(
@@ -2793,6 +2922,22 @@ def render_class_datalist(rows: list[dict]) -> str:
return f'<datalist id="element-classes">{options}</datalist>'
def render_attribute_datalist(rows: list[dict]) -> str:
attributes = sorted(
{
str(attribute)
for row in rows
for attribute in row.get("attributes", [])
if str(attribute)
}
)
options = "".join(
f'<option value="{escape(item)}"></option>'
for item in attributes
)
return f'<datalist id="element-attributes">{options}</datalist>'
def render_optional_hidden(name: str, value: int | None) -> str:
if value is None:
return ""
@@ -2958,10 +3103,17 @@ def render_candidate_edit_form(
f"/{collection}/{candidate['id']}/edit"
)
confidence = f"{candidate['confidence']:.2f}"
extra_fields = ""
if collection == "candidate-features":
extra_fields = f"""
<label>Type <input name="type" value="{escape(candidate['type'])}" required></label>
<label>Location <input name="location" value="{escape(candidate.get('location', ''))}"></label>
"""
return f"""
<form class="stack" method="post" action="{action}">
<label>Name <input name="name" value="{escape(candidate['name'])}" required></label>
<label>Description <textarea name="description" rows="2">{escape(candidate['description'])}</textarea></label>
{extra_fields or f'<label>Description <textarea name="description" rows="2">{escape(candidate.get("description", ""))}</textarea></label>'}
{render_classification_edit_fields(candidate)}
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="{confidence}" required></label>
<button class="secondary" type="submit">Save Edit</button>
</form>
@@ -3012,8 +3164,10 @@ def render_candidate_feature(
<span class="pill">ID {feature["id"]}</span>
<span class="pill">{escape(feature["status"])}</span>
<span class="pill">{escape(feature["type"])}</span>
<span class="pill">{escape(feature.get("primary_class", feature["type"]))}</span>
<span class="source">{escape(feature["location"])}</span>
{render_candidate_reject_form('candidate-features', feature, repository_id, analysis_run_id)}
{render_candidate_edit_form('candidate-features', feature, repository_id, analysis_run_id)}
{render_candidate_relink_form('candidate-features', feature, repository_id, analysis_run_id, 'target_capability_id', 'Target capability ID')}
{render_candidate_merge_form('candidate-features', feature, repository_id, analysis_run_id, 'target_feature_id', 'Merge into feature ID')}
</li>