2026-04-25 23:04:15 +02:00
from __future__ import annotations
from dataclasses import asdict
from html import escape
2026-04-26 16:13:53 +02:00
from fastapi import APIRouter , Depends , Form , HTTPException , Query
from fastapi . responses import HTMLResponse , PlainTextResponse , RedirectResponse
2026-04-25 23:04:15 +02:00
from repo_registry . core . service import RegistryService
2026-04-26 02:36:27 +02:00
from repo_registry . storage . sqlite import NotFoundError
2026-04-25 23:04:15 +02:00
from repo_registry . web_api . app import get_service
router = APIRouter ( include_in_schema = False )
def page ( title : str , body : str ) - > HTMLResponse :
return HTMLResponse (
f """
< ! doctype html >
< html lang = " en " >
< head >
< meta charset = " utf-8 " >
< meta name = " viewport " content = " width=device-width, initial-scale=1 " >
< title > { escape ( title ) } · Repository Ability Registry < / title >
< style >
: root { {
color - scheme : light ;
- - bg : #f7f8fa;
- - panel : #ffffff;
- - text : #1f2933;
- - muted : #637381;
- - line : #d9dee7;
- - accent : #0f766e;
- - accent - dark : #115e59;
- - warn : #9a3412;
} }
* { { box - sizing : border - box ; } }
body { {
margin : 0 ;
background : var ( - - bg ) ;
color : var ( - - text ) ;
font : 14 px / 1.45 system - ui , - apple - system , BlinkMacSystemFont , " Segoe UI " , sans - serif ;
} }
header { {
background : #17212b;
color : #fff;
padding : 14 px 28 px ;
display : flex ;
align - items : center ;
justify - content : space - between ;
} }
header a { { color : #e6fffb; text-decoration: none; }}
main { { max - width : 1180 px ; margin : 0 auto ; padding : 24 px ; } }
h1 { { font - size : 24 px ; margin : 0 0 18 px ; } }
h2 { { font - size : 18 px ; margin : 28 px 0 10 px ; } }
h3 { { font - size : 15 px ; margin : 16 px 0 6 px ; } }
p { { margin : 0 0 10 px ; } }
a { { color : var ( - - accent - dark ) ; } }
. grid { { display : grid ; grid - template - columns : 1 fr 1 fr ; gap : 18 px ; align - items : start ; } }
. panel { {
background : var ( - - panel ) ;
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
padding : 16 px ;
} }
. stack { { display : grid ; gap : 12 px ; } }
. muted { { color : var ( - - muted ) ; } }
. pill { {
display : inline - flex ;
align - items : center ;
min - height : 24 px ;
padding : 2 px 8 px ;
border : 1 px solid var ( - - line ) ;
border - radius : 999 px ;
color : var ( - - muted ) ;
background : #fbfcfd;
font - size : 12 px ;
} }
table { { width : 100 % ; border - collapse : collapse ; background : var ( - - panel ) ; } }
th , td { { text - align : left ; border - bottom : 1 px solid var ( - - line ) ; padding : 10 px 8 px ; vertical - align : top ; } }
th { { color : var ( - - muted ) ; font - weight : 600 ; font - size : 12 px ; text - transform : uppercase ; } }
input , textarea { {
width : 100 % ;
border : 1 px solid var ( - - line ) ;
border - radius : 6 px ;
padding : 9 px 10 px ;
font : inherit ;
} }
label { { display : grid ; gap : 5 px ; color : var ( - - muted ) ; font - size : 12 px ; font - weight : 600 ; } }
button , . button { {
display : inline - flex ;
align - items : center ;
justify - content : center ;
min - height : 34 px ;
padding : 7 px 12 px ;
border : 1 px solid var ( - - accent ) ;
border - radius : 6 px ;
background : var ( - - accent ) ;
color : white ;
font : inherit ;
font - weight : 650 ;
text - decoration : none ;
cursor : pointer ;
} }
. button . secondary , button . secondary { {
color : var ( - - accent - dark ) ;
background : white ;
} }
. tree ul { { margin : 8 px 0 0 20 px ; padding : 0 ; } }
. tree li { { margin : 6 px 0 ; } }
. source { { color : var ( - - muted ) ; font - family : ui - monospace , SFMono - Regular , Consolas , monospace ; font - size : 12 px ; } }
. actions { { display : flex ; gap : 8 px ; flex - wrap : wrap ; align - items : center ; } }
@media ( max - width : 780 px ) { {
header { { padding : 12 px 16 px ; } }
main { { padding : 16 px ; } }
. grid { { grid - template - columns : 1 fr ; } }
table , tbody , tr , td { { display : block ; width : 100 % ; } }
thead { { display : none ; } }
td { { border - bottom : 0 ; padding : 6 px 0 ; } }
tr { { border - bottom : 1 px solid var ( - - line ) ; padding : 10 px 0 ; display : block ; } }
} }
< / style >
< / head >
< body >
< header >
< a href = " /ui " > < strong > Repository Ability Registry < / strong > < / a >
2026-04-25 23:50:58 +02:00
< nav class = " actions " >
< a href = " /ui/search " > Search < / a >
2026-04-26 16:13:53 +02:00
< a href = " /ui/discovery " > Discovery < / a >
2026-04-25 23:50:58 +02:00
< a href = " /docs " > API Docs < / a >
< / nav >
2026-04-25 23:04:15 +02:00
< / header >
< main > { body } < / main >
< / body >
< / html >
"""
)
@router.get ( " /ui " )
def repository_index ( service : RegistryService = Depends ( get_service ) ) - > HTMLResponse :
repositories = service . list_repositories ( )
rows = " \n " . join (
f """
< tr >
< td > < a href = " /ui/repos/ {repo.id} " > { escape ( repo . name ) } < / a > < / td >
< td > < span class = " pill " > { escape ( repo . status ) } < / span > < / td >
< td class = " source " > { escape ( repo . branch ) } < / td >
< td class = " source " > { escape ( repo . url ) } < / td >
< / tr >
"""
for repo in repositories
)
body = f """
< h1 > Repositories < / h1 >
< div class = " grid " >
< section class = " panel " >
< h2 > Register Repository < / h2 >
< form class = " stack " method = " post " action = " /ui/repos " >
< label > Git URL or local path < input name = " url " required > < / label >
< label > Branch < input name = " branch " value = " main " > < / label >
< button type = " submit " > Register < / button >
< / form >
< / section >
< section class = " panel " >
2026-04-26 16:13:53 +02:00
< div class = " actions " >
< h2 style = " margin-right:auto " > Registry < / h2 >
< a class = " button secondary " href = " /ui/discovery " > Discovery < / a >
< / div >
2026-04-25 23:04:15 +02:00
< table >
< thead > < tr > < th > Name < / th > < th > Status < / th > < th > Branch < / th > < th > Source < / th > < / tr > < / thead >
< tbody > { rows or ' <tr><td colspan= " 4 " class= " muted " >No repositories yet.</td></tr> ' } < / tbody >
< / table >
< / section >
< / div >
"""
return page ( " Repositories " , body )
2026-04-26 16:13:53 +02:00
@router.get ( " /ui/discovery " )
def discovery_page ( service : RegistryService = Depends ( get_service ) ) - > HTMLResponse :
repositories = service . list_repositories ( )
return page (
" Discovery " ,
f """
< div class = " actions " >
< h1 style = " margin-right:auto " > Discovery < / h1 >
< a class = " button secondary " href = " /ui " > Repositories < / a >
< / div >
< div class = " grid " >
< section class = " panel " >
< h2 > Compare Repositories < / h2 >
< form class = " stack " method = " get " action = " /ui/discovery/compare " >
{ render_repository_checkbox_list ( service , repositories ) }
< button type = " submit " > Compare < / button >
< / form >
< / section >
< section class = " panel " >
< h2 > Capability Gap Report < / h2 >
< form class = " stack " method = " post " action = " /ui/discovery/gaps " >
< label > Desired ability < input name = " desired_ability " required > < / label >
< label > Desired capabilities < textarea name = " desired_capabilities " rows = " 7 " placeholder = " One capability per line or comma-separated " required > < / textarea > < / label >
{ render_repository_checkbox_list ( service , repositories ) }
< button type = " submit " > Run Gap Report < / button >
< / form >
< / section >
< / div >
""" ,
)
@router.get ( " /ui/discovery/compare " )
def discovery_compare_page (
repository_ids : list [ int ] = Query ( default = [ ] ) ,
service : RegistryService = Depends ( get_service ) ,
) - > HTMLResponse :
if len ( repository_ids ) < 2 :
return page (
" Repository Comparison " ,
"""
< div class = " actions " >
< h1 style = " margin-right:auto " > Repository Comparison < / h1 >
< a class = " button secondary " href = " /ui/discovery " > Discovery < / a >
< / div >
< section class = " panel " >
< p class = " muted " > Select at least two repositories with approved profiles . < / p >
< / section >
""" ,
)
try :
comparison = service . compare_repositories ( repository_ids )
except NotFoundError as exc :
raise HTTPException ( status_code = 404 , detail = str ( exc ) ) from exc
body = f """
< div class = " actions " >
< h1 style = " margin-right:auto " > Repository Comparison < / h1 >
< a class = " button secondary " href = " /ui/discovery " > Discovery < / a >
< / div >
< section class = " panel " >
< h2 > Compared Repositories < / h2 >
{ render_compared_repositories ( comparison [ " repositories " ] ) }
< / section >
< section class = " panel " style = " margin-top:18px " >
< h2 > Shared Abilities < / h2 >
{ render_compared_abilities ( comparison [ " abilities " ] ) }
< / section >
< section class = " panel " style = " margin-top:18px " >
< h2 > Unique Capabilities < / h2 >
{ render_unique_capabilities ( comparison [ " unique_capabilities " ] ) }
< / section >
"""
return page ( " Repository Comparison " , body )
@router.post ( " /ui/discovery/gaps " )
def discovery_gap_report_page (
desired_ability : str = Form ( . . . ) ,
desired_capabilities : str = Form ( . . . ) ,
repository_ids : list [ int ] = Form ( default = [ ] ) ,
service : RegistryService = Depends ( get_service ) ,
) - > HTMLResponse :
capabilities = split_capability_lines ( desired_capabilities )
try :
report = service . detect_capability_gaps (
desired_ability = desired_ability ,
desired_capabilities = capabilities ,
repository_ids = repository_ids or None ,
)
except NotFoundError as exc :
raise HTTPException ( status_code = 404 , detail = str ( exc ) ) from exc
body = f """
< div class = " actions " >
< h1 style = " margin-right:auto " > Capability Gap Report < / h1 >
< a class = " button secondary " href = " /ui/discovery " > Discovery < / a >
< / div >
< section class = " panel " >
< h2 > { escape ( report [ " desired_ability " ] ) } < / h2 >
{ render_gap_report ( report ) }
< / section >
"""
return page ( " Capability Gap Report " , body )
2026-04-25 23:50:58 +02:00
@router.get ( " /ui/search " )
def search_page (
q : str = " " ,
2026-04-26 00:04:19 +02:00
status : str = " " ,
language : str = " " ,
framework : str = " " ,
2026-04-26 00:08:55 +02:00
ability : str = " " ,
capability : str = " " ,
2026-04-25 23:50:58 +02:00
service : RegistryService = Depends ( get_service ) ,
) - > HTMLResponse :
2026-04-26 00:04:19 +02:00
results = (
service . search (
q ,
status = status or None ,
language = language or None ,
framework = framework or None ,
2026-04-26 00:08:55 +02:00
ability = ability or None ,
capability = capability or None ,
2026-04-26 00:04:19 +02:00
)
if q . strip ( )
else [ ]
)
2026-04-25 23:50:58 +02:00
rows = " \n " . join (
f """
< tr >
2026-04-26 00:08:55 +02:00
< td > < a href = " { search_result_href(asdict(result))} " > { escape ( result . repository_name ) } < / a > < / td >
2026-04-25 23:50:58 +02:00
< td > < span class = " pill " > { escape ( result . match_type ) } < / span > < / td >
2026-04-25 23:59:38 +02:00
< td >
< strong > { escape ( result . match_name ) } < / strong >
{ render_search_context ( asdict ( result ) ) }
< / td >
< td > { escape ( result . matched_field ) } < / td >
2026-04-26 08:45:00 +02:00
< td > { result . confidence : .2 f } < span class = " pill " > { escape ( result . confidence_label ) } < / span > < / td >
2026-04-25 23:50:58 +02:00
< / tr >
"""
for result in results
)
empty = (
' <tr><td colspan= " 4 " class= " muted " >No matches.</td></tr> '
if q . strip ( )
else ' <tr><td colspan= " 4 " class= " muted " >Enter a need, capability, or repository name.</td></tr> '
)
body = f """
< div class = " actions " >
< h1 style = " margin-right:auto " > Search < / h1 >
< a class = " button secondary " href = " /ui " > Repositories < / a >
< / div >
< section class = " panel " >
2026-04-26 00:04:19 +02:00
< form class = " stack " method = " get " action = " /ui/search " >
2026-04-25 23:50:58 +02:00
< input name = " q " value = " { escape(q)} " placeholder = " Search approved registry entries " >
2026-04-26 00:04:19 +02:00
< div class = " grid " >
< label > Status < input name = " status " value = " { escape(status)} " placeholder = " indexed " > < / label >
< label > Language < input name = " language " value = " { escape(language)} " placeholder = " Python " > < / label >
< label > Framework < input name = " framework " value = " { escape(framework)} " placeholder = " FastAPI " > < / label >
2026-04-26 00:08:55 +02:00
< label > Ability < input name = " ability " value = " { escape(ability)} " placeholder = " Email Routing " > < / label >
< label > Capability < input name = " capability " value = " { escape(capability)} " placeholder = " Classification " > < / label >
2026-04-26 00:04:19 +02:00
< / div >
< div class = " actions " >
< button type = " submit " > Search < / button >
< a class = " button secondary " href = " /ui/search " > Clear < / a >
< / div >
2026-04-25 23:50:58 +02:00
< / form >
< / section >
< section class = " panel " style = " margin-top:18px " >
< table >
2026-04-25 23:59:38 +02:00
< thead > < tr > < th > Repository < / th > < th > Match < / th > < th > Name < / th > < th > Field < / th > < th > Confidence < / th > < / tr > < / thead >
2026-04-25 23:50:58 +02:00
< tbody > { rows or empty } < / tbody >
< / table >
< / section >
"""
return page ( " Search " , body )
2026-04-25 23:04:15 +02:00
@router.post ( " /ui/repos " )
def create_repository_from_form (
url : str = Form ( . . . ) ,
branch : str = Form ( " main " ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
repository = service . register_repository (
url = url ,
branch = branch or " main " ,
)
return RedirectResponse ( f " /ui/repos/ { repository . id } " , status_code = 303 )
@router.get ( " /ui/repos/ {repository_id} " )
def repository_detail (
repository_id : int ,
service : RegistryService = Depends ( get_service ) ,
) - > HTMLResponse :
2026-04-26 02:36:27 +02:00
try :
repository = service . get_repository ( repository_id )
except NotFoundError as exc :
raise HTTPException ( status_code = 404 , detail = str ( exc ) ) from exc
2026-04-25 23:04:15 +02:00
runs = service . list_analysis_runs ( repository_id )
ability_map = service . ability_map ( repository_id )
2026-04-26 00:24:02 +02:00
decisions = service . list_review_decisions ( repository_id )
2026-04-26 00:04:19 +02:00
facts = service . list_observed_facts ( repository_id )
languages = sorted ( { fact . name for fact in facts if fact . kind == " language " } )
frameworks = sorted ( { fact . name for fact in facts if fact . kind == " framework " } )
2026-04-25 23:04:15 +02:00
run_rows = " \n " . join (
f """
< tr >
< td > < a href = " /ui/repos/ {repository_id} /analysis-runs/ {run.id} " > #{run.id}</a></td>
< td > < span class = " pill " > { escape ( run . status ) } < / span > < / td >
< td class = " source " > { escape ( run . started_at ) } < / td >
2026-04-26 15:54:37 +02:00
< td > { render_run_compare_link ( repository_id , run . id , runs ) } < / td >
2026-04-25 23:04:15 +02:00
< td > { escape ( run . error_message or ' ' ) } < / td >
< / tr >
"""
for run in runs
)
body = f """
< div class = " actions " >
< h1 style = " margin-right:auto " > { escape ( repository . name ) } < / h1 >
2026-04-26 16:13:53 +02:00
< a class = " button secondary " href = " /ui/repos/ {repository_id} /export " > Export < / a >
2026-04-25 23:04:15 +02:00
< a class = " button secondary " href = " /ui " > Back < / a >
< / div >
< p class = " muted " > { escape ( repository . description or ' ' ) } < / p >
< p > < span class = " pill " > { escape ( repository . status ) } < / span > < span class = " source " > { escape ( repository . url ) } < / span > < / p >
2026-04-26 00:04:19 +02:00
{ render_repository_facts ( languages , frameworks ) }
2026-04-25 23:04:15 +02:00
< div class = " grid " >
< section class = " panel " >
2026-04-26 02:32:41 +02:00
< h2 > Repository Metadata < / h2 >
< form class = " stack " method = " post " action = " /ui/repos/ {repository_id} /edit " >
< label > Name < input name = " name " value = " { escape(repository.name)} " required > < / label >
< label > Description < textarea name = " description " rows = " 2 " > { escape ( repository . description or ' ' ) } < / textarea > < / label >
< label > Branch < input name = " branch " value = " { escape(repository.branch)} " required > < / label >
< button class = " secondary " type = " submit " > Save Repository < / button >
< / form >
2026-04-25 23:04:15 +02:00
< h2 > Run Analysis < / h2 >
< form class = " stack " method = " post " action = " /ui/repos/ {repository_id} /analysis-runs " >
< label > Override source path < input name = " source_path " placeholder = " Optional local path " > < / label >
< button type = " submit " > Analyze < / button >
< / form >
< h2 > Analysis Runs < / h2 >
< table >
2026-04-26 15:54:37 +02:00
< thead > < tr > < th > Run < / th > < th > Status < / th > < th > Started < / th > < th > Compare < / th > < th > Error < / th > < / tr > < / thead >
< tbody > { run_rows or ' <tr><td colspan= " 5 " class= " muted " >No runs yet.</td></tr> ' } < / tbody >
2026-04-25 23:04:15 +02:00
< / table >
< / section >
< section class = " panel " >
< h2 > Approved Ability Map < / h2 >
2026-04-26 02:30:18 +02:00
{ render_ability_map ( asdict ( ability_map ) , repository_id ) }
2026-04-25 23:04:15 +02:00
< / section >
< / div >
2026-04-26 02:26:34 +02:00
< section class = " panel " style = " margin-top:18px " >
< h2 > Manual Registry Entry < / h2 >
< div class = " grid " >
< form class = " stack " method = " post " action = " /ui/repos/ {repository_id} /abilities " >
< h3 > Add Ability < / h3 >
< label > Name < input name = " name " required > < / label >
< label > Description < textarea name = " description " rows = " 2 " > < / textarea > < / 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 >
< form class = " stack " method = " post " action = " /ui/repos/ {repository_id} /capabilities " >
< h3 > Add Capability < / h3 >
< label > Ability ID < input name = " ability_id " type = " number " min = " 1 " required > < / label >
< label > Name < input name = " name " required > < / label >
< 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 > 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 >
< form class = " stack " method = " post " action = " /ui/repos/ {repository_id} /features " >
< h3 > Add Feature < / h3 >
< 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 > 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 >
< / form >
< form class = " stack " method = " post " action = " /ui/repos/ {repository_id} /evidence " >
< h3 > Add Evidence < / h3 >
< label > Capability ID < input name = " capability_id " type = " number " min = " 1 " required > < / label >
< label > Type < input name = " type " required > < / label >
< label > Reference < input name = " reference " required > < / label >
< label > Strength < input name = " strength " value = " medium " required > < / label >
< button type = " submit " > Add Evidence < / button >
< / form >
< / div >
< / section >
2026-04-26 00:24:02 +02:00
< section class = " panel " style = " margin-top:18px " >
< h2 > Review Decisions < / h2 >
{ render_review_decisions ( decisions ) }
< / section >
2026-04-26 02:36:27 +02:00
< section class = " panel " style = " margin-top:18px " >
< h2 > Delete Repository < / h2 >
< form class = " stack " method = " post " action = " /ui/repos/ {repository_id} /delete " >
< label > Confirm repository name < input name = " confirm_name " placeholder = " { escape(repository.name)} " required > < / label >
< button class = " secondary " type = " submit " > Delete Repository < / button >
< / form >
< / section >
2026-04-25 23:04:15 +02:00
"""
return page ( repository . name , body )
2026-04-26 16:13:53 +02:00
@router.get ( " /ui/repos/ {repository_id} /export " )
def export_repository_from_ui (
repository_id : int ,
service : RegistryService = Depends ( get_service ) ,
) - > PlainTextResponse :
try :
content = service . export_registry_entry ( repository_id )
except NotFoundError as exc :
raise HTTPException ( status_code = 404 , detail = str ( exc ) ) from exc
return PlainTextResponse ( content , media_type = " application/x-yaml " )
2026-04-26 02:32:41 +02:00
@router.post ( " /ui/repos/ {repository_id} /edit " )
def edit_repository_from_form (
repository_id : int ,
name : str = Form ( . . . ) ,
description : str = Form ( " " ) ,
branch : str = Form ( " main " ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . update_repository (
repository_id ,
name = name ,
description = description ,
branch = branch or " main " ,
)
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
2026-04-26 02:36:27 +02:00
@router.post ( " /ui/repos/ {repository_id} /delete " )
def delete_repository_from_form (
repository_id : int ,
confirm_name : str = Form ( . . . ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
repository = service . get_repository ( repository_id )
if confirm_name != repository . name :
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
service . delete_repository ( repository_id )
return RedirectResponse ( " /ui " , status_code = 303 )
2026-04-26 02:26:34 +02:00
@router.post ( " /ui/repos/ {repository_id} /abilities " )
def create_ability_from_form (
repository_id : int ,
name : str = Form ( . . . ) ,
description : str = Form ( " " ) ,
confidence : float = Form ( 1.0 ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . add_ability (
repository_id ,
name = name ,
description = description ,
confidence = confidence ,
)
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
@router.post ( " /ui/repos/ {repository_id} /capabilities " )
def create_capability_from_form (
repository_id : int ,
ability_id : int = Form ( . . . ) ,
name : str = Form ( . . . ) ,
description : str = Form ( " " ) ,
inputs : str = Form ( " " ) ,
outputs : str = Form ( " " ) ,
confidence : float = Form ( 1.0 ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . add_capability (
repository_id ,
ability_id ,
name = name ,
description = description ,
inputs = split_csv ( inputs ) ,
outputs = split_csv ( outputs ) ,
confidence = confidence ,
)
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
@router.post ( " /ui/repos/ {repository_id} /features " )
def create_feature_from_form (
repository_id : int ,
capability_id : int = Form ( . . . ) ,
name : str = Form ( . . . ) ,
type : str = Form ( . . . ) ,
location : str = Form ( " " ) ,
confidence : float = Form ( 1.0 ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . add_feature (
repository_id ,
capability_id ,
name = name ,
type = type ,
location = location ,
confidence = confidence ,
)
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
@router.post ( " /ui/repos/ {repository_id} /evidence " )
def create_evidence_from_form (
repository_id : int ,
capability_id : int = Form ( . . . ) ,
type : str = Form ( . . . ) ,
reference : str = Form ( . . . ) ,
strength : str = Form ( " medium " ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . add_evidence (
repository_id ,
capability_id ,
type = type ,
reference = reference ,
strength = strength ,
)
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
2026-04-26 02:30:18 +02:00
@router.post ( " /ui/repos/ {repository_id} /abilities/ {ability_id} /edit " )
def edit_ability_from_form (
repository_id : int ,
ability_id : int ,
name : str = Form ( . . . ) ,
description : str = Form ( " " ) ,
confidence : float = Form ( 1.0 ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . update_ability (
repository_id ,
ability_id ,
name = name ,
description = description ,
confidence = confidence ,
)
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
@router.post ( " /ui/repos/ {repository_id} /abilities/ {ability_id} /delete " )
def delete_ability_from_form (
repository_id : int ,
ability_id : int ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . delete_ability ( repository_id , ability_id )
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
@router.post ( " /ui/repos/ {repository_id} /capabilities/ {capability_id} /edit " )
def edit_capability_from_form (
repository_id : int ,
capability_id : int ,
name : str = Form ( . . . ) ,
description : str = Form ( " " ) ,
inputs : str = Form ( " " ) ,
outputs : str = Form ( " " ) ,
confidence : float = Form ( 1.0 ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . update_capability (
repository_id ,
capability_id ,
name = name ,
description = description ,
inputs = split_csv ( inputs ) ,
outputs = split_csv ( outputs ) ,
confidence = confidence ,
)
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
@router.post ( " /ui/repos/ {repository_id} /capabilities/ {capability_id} /delete " )
def delete_capability_from_form (
repository_id : int ,
capability_id : int ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . delete_capability ( repository_id , capability_id )
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
@router.post ( " /ui/repos/ {repository_id} /features/ {feature_id} /edit " )
def edit_feature_from_form (
repository_id : int ,
feature_id : int ,
name : str = Form ( . . . ) ,
type : str = Form ( . . . ) ,
location : str = Form ( " " ) ,
confidence : float = Form ( 1.0 ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . update_feature (
repository_id ,
feature_id ,
name = name ,
type = type ,
location = location ,
confidence = confidence ,
)
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
@router.post ( " /ui/repos/ {repository_id} /features/ {feature_id} /delete " )
def delete_feature_from_form (
repository_id : int ,
feature_id : int ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . delete_feature ( repository_id , feature_id )
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
@router.post ( " /ui/repos/ {repository_id} /evidence/ {evidence_id} /edit " )
def edit_evidence_from_form (
repository_id : int ,
evidence_id : int ,
type : str = Form ( . . . ) ,
reference : str = Form ( . . . ) ,
strength : str = Form ( " medium " ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . update_evidence (
repository_id ,
evidence_id ,
type = type ,
reference = reference ,
strength = strength ,
)
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
@router.post ( " /ui/repos/ {repository_id} /evidence/ {evidence_id} /delete " )
def delete_evidence_from_form (
repository_id : int ,
evidence_id : int ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . delete_evidence ( repository_id , evidence_id )
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
2026-04-25 23:04:15 +02:00
@router.post ( " /ui/repos/ {repository_id} /analysis-runs " )
def create_analysis_run_from_form (
repository_id : int ,
source_path : str = Form ( " " ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
summary = service . analyze_repository (
repository_id ,
source_path = source_path or None ,
)
return RedirectResponse (
f " /ui/repos/ { repository_id } /analysis-runs/ { summary . analysis_run . id } " ,
status_code = 303 ,
)
@router.get ( " /ui/repos/ {repository_id} /analysis-runs/ {analysis_run_id} " )
def analysis_run_detail (
repository_id : int ,
analysis_run_id : int ,
service : RegistryService = Depends ( get_service ) ,
) - > HTMLResponse :
repository = service . get_repository ( repository_id )
candidate_graph = service . candidate_graph ( repository_id , analysis_run_id )
facts = service . list_observed_facts ( repository_id , analysis_run_id )
2026-04-26 02:47:10 +02:00
chunks = service . list_content_chunks ( repository_id , analysis_run_id )
2026-04-26 00:24:02 +02:00
decisions = service . list_review_decisions ( repository_id , analysis_run_id )
2026-04-25 23:04:15 +02:00
fact_rows = " \n " . join (
f """
< tr >
< td > { escape ( fact . kind ) } < / td >
< td > { escape ( fact . name ) } < / td >
< td class = " source " > { escape ( fact . path ) } < / td >
< td class = " source " > { escape ( fact . value ) } < / td >
< / tr >
"""
for fact in facts
)
body = f """
< div class = " actions " >
< h1 style = " margin-right:auto " > { escape ( repository . name ) } · Run #{analysis_run_id}</h1>
2026-04-26 15:54:37 +02:00
{ render_run_detail_compare_link ( repository_id , analysis_run_id , service . list_analysis_runs ( repository_id ) ) }
2026-04-25 23:04:15 +02:00
< a class = " button secondary " href = " /ui/repos/ {repository_id} " > Repository < / a >
< / div >
< div class = " grid " >
< section class = " panel " >
< div class = " actions " >
< h2 style = " margin-right:auto " > Candidate Graph < / h2 >
< form method = " post " action = " /ui/repos/ {repository_id} /analysis-runs/ {analysis_run_id} /candidate-graph/approve " >
< button type = " submit " > Approve < / button >
< / form >
< / div >
2026-04-25 23:27:28 +02:00
{ render_candidate_graph ( asdict ( candidate_graph ) , repository_id , analysis_run_id ) }
2026-04-25 23:04:15 +02:00
< / section >
< section class = " panel " >
< h2 > Observed Facts < / h2 >
< table >
< thead > < tr > < th > Kind < / th > < th > Name < / th > < th > Path < / th > < th > Value < / th > < / tr > < / thead >
< tbody > { fact_rows or ' <tr><td colspan= " 4 " class= " muted " >No observed facts.</td></tr> ' } < / tbody >
< / table >
2026-04-26 00:24:02 +02:00
< h2 > Review Decisions < / h2 >
{ render_review_decisions ( decisions ) }
2026-04-25 23:04:15 +02:00
< / section >
< / div >
2026-04-26 02:47:10 +02:00
< section class = " panel " style = " margin-top:18px " >
< h2 > Content Chunks < / h2 >
{ render_content_chunks ( chunks ) }
< / section >
2026-04-25 23:04:15 +02:00
"""
return page ( f " { repository . name } Run { analysis_run_id } " , body )
2026-04-26 15:54:37 +02:00
@router.get ( " /ui/repos/ {repository_id} /analysis-runs/ {base_analysis_run_id} /diff/ {target_analysis_run_id} " )
def analysis_run_diff_detail (
repository_id : int ,
base_analysis_run_id : int ,
target_analysis_run_id : int ,
service : RegistryService = Depends ( get_service ) ,
) - > HTMLResponse :
try :
diff = service . diff_analysis_runs (
repository_id ,
base_analysis_run_id ,
target_analysis_run_id ,
)
except NotFoundError as exc :
raise HTTPException ( status_code = 404 , detail = str ( exc ) ) from exc
body = f """
< div class = " actions " >
< h1 style = " margin-right:auto " > { escape ( diff . repository . name ) } · Change Review < / h1 >
< a class = " button secondary " href = " /ui/repos/ {repository_id} /analysis-runs/ {target_analysis_run_id} " > Target Run < / a >
< a class = " button secondary " href = " /ui/repos/ {repository_id} " > Repository < / a >
< / div >
< p class = " muted " > Comparing run #{base_analysis_run_id} to run #{target_analysis_run_id}.</p>
< section class = " panel " >
< div class = " actions " >
< h2 style = " margin-right:auto " > Approve Target Changes < / h2 >
< form class = " actions " method = " post " action = " /ui/repos/ {repository_id} /analysis-runs/ {target_analysis_run_id} /changes/approve " >
< input name = " notes " value = " Approved target run changes from web UI " aria - label = " Approval notes " >
< button type = " submit " > Approve Changes < / button >
< / form >
< / div >
< / section >
< div class = " grid " style = " margin-top:18px " >
< section class = " panel " >
< h2 > Approved Registry Impact < / h2 >
{ render_diff_section ( asdict ( diff . approved_entries ) ) }
< / section >
< section class = " panel " >
< h2 > Candidate Claims < / h2 >
{ render_diff_section ( asdict ( diff . candidates ) ) }
< / section >
< section class = " panel " >
< h2 > Observed Facts < / h2 >
{ render_diff_section ( asdict ( diff . facts ) ) }
< / section >
< section class = " panel " >
< h2 > Content Chunks < / h2 >
{ render_diff_section ( asdict ( diff . chunks ) ) }
< / section >
< / div >
"""
return page ( f " { diff . repository . name } Change Review " , body )
2026-04-25 23:04:15 +02:00
@router.post ( " /ui/repos/ {repository_id} /analysis-runs/ {analysis_run_id} /candidate-graph/approve " )
def approve_candidate_graph_from_form (
repository_id : int ,
analysis_run_id : int ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . approve_candidate_graph (
repository_id ,
analysis_run_id ,
notes = " Approved from web UI " ,
)
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
2026-04-26 15:54:37 +02:00
@router.post ( " /ui/repos/ {repository_id} /analysis-runs/ {analysis_run_id} /changes/approve " )
def approve_analysis_run_changes_from_form (
repository_id : int ,
analysis_run_id : int ,
notes : str = Form ( " Approved target run changes from web UI " ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . approve_analysis_run_changes (
repository_id ,
analysis_run_id ,
notes = notes ,
)
return RedirectResponse ( f " /ui/repos/ { repository_id } " , status_code = 303 )
2026-04-25 23:27:28 +02:00
@router.post (
" /ui/repos/ {repository_id} /analysis-runs/ {analysis_run_id} "
" /candidate-abilities/ {candidate_ability_id} /reject "
)
def reject_candidate_ability_from_form (
repository_id : int ,
analysis_run_id : int ,
candidate_ability_id : int ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . reject_candidate_ability (
repository_id ,
analysis_run_id ,
candidate_ability_id ,
notes = " Rejected from web UI " ,
)
return RedirectResponse (
f " /ui/repos/ { repository_id } /analysis-runs/ { analysis_run_id } " ,
status_code = 303 ,
)
2026-04-25 23:39:29 +02:00
@router.post (
" /ui/repos/ {repository_id} /analysis-runs/ {analysis_run_id} "
" /candidate-capabilities/ {candidate_capability_id} /reject "
)
def reject_candidate_capability_from_form (
repository_id : int ,
analysis_run_id : int ,
candidate_capability_id : int ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . reject_candidate_capability (
repository_id ,
analysis_run_id ,
candidate_capability_id ,
notes = " Rejected 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} /reject "
)
def reject_candidate_feature_from_form (
repository_id : int ,
analysis_run_id : int ,
candidate_feature_id : int ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . reject_candidate_feature (
repository_id ,
analysis_run_id ,
candidate_feature_id ,
notes = " Rejected 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-evidence/ {candidate_evidence_id} /reject "
)
def reject_candidate_evidence_from_form (
repository_id : int ,
analysis_run_id : int ,
candidate_evidence_id : int ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . reject_candidate_evidence (
repository_id ,
analysis_run_id ,
candidate_evidence_id ,
notes = " Rejected from web UI " ,
)
return RedirectResponse (
f " /ui/repos/ { repository_id } /analysis-runs/ { analysis_run_id } " ,
status_code = 303 ,
)
2026-04-25 23:32:18 +02:00
@router.post (
" /ui/repos/ {repository_id} /analysis-runs/ {analysis_run_id} "
" /candidate-abilities/ {candidate_ability_id} /edit "
)
def edit_candidate_ability_from_form (
repository_id : int ,
analysis_run_id : int ,
candidate_ability_id : int ,
name : str = Form ( . . . ) ,
description : str = Form ( " " ) ,
confidence : float = Form ( . . . ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . edit_candidate_ability (
repository_id ,
analysis_run_id ,
candidate_ability_id ,
name = name ,
description = description ,
confidence = confidence ,
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-capabilities/ {candidate_capability_id} /edit "
)
def edit_candidate_capability_from_form (
repository_id : int ,
analysis_run_id : int ,
candidate_capability_id : int ,
name : str = Form ( . . . ) ,
description : str = Form ( " " ) ,
confidence : float = Form ( . . . ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . edit_candidate_capability (
repository_id ,
analysis_run_id ,
candidate_capability_id ,
name = name ,
description = description ,
confidence = confidence ,
notes = " Edited from web UI " ,
)
return RedirectResponse (
f " /ui/repos/ { repository_id } /analysis-runs/ { analysis_run_id } " ,
status_code = 303 ,
)
2026-04-25 23:44:25 +02:00
@router.post (
" /ui/repos/ {repository_id} /analysis-runs/ {analysis_run_id} "
" /candidate-capabilities/ {candidate_capability_id} /relink "
)
def relink_candidate_capability_from_form (
repository_id : int ,
analysis_run_id : int ,
candidate_capability_id : int ,
target_ability_id : int = Form ( . . . ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . relink_candidate_capability (
repository_id ,
analysis_run_id ,
candidate_capability_id ,
target_ability_id = target_ability_id ,
notes = " Relinked 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} /relink "
)
def relink_candidate_feature_from_form (
repository_id : int ,
analysis_run_id : int ,
candidate_feature_id : int ,
target_capability_id : int = Form ( . . . ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . relink_candidate_feature (
repository_id ,
analysis_run_id ,
candidate_feature_id ,
target_capability_id = target_capability_id ,
notes = " Relinked 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-evidence/ {candidate_evidence_id} /relink "
)
def relink_candidate_evidence_from_form (
repository_id : int ,
analysis_run_id : int ,
candidate_evidence_id : int ,
target_capability_id : int = Form ( . . . ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . relink_candidate_evidence (
repository_id ,
analysis_run_id ,
candidate_evidence_id ,
target_capability_id = target_capability_id ,
notes = " Relinked from web UI " ,
)
return RedirectResponse (
f " /ui/repos/ { repository_id } /analysis-runs/ { analysis_run_id } " ,
status_code = 303 ,
)
2026-04-25 23:50:58 +02:00
@router.post (
" /ui/repos/ {repository_id} /analysis-runs/ {analysis_run_id} "
" /candidate-abilities/ {source_ability_id} /merge "
)
def merge_candidate_ability_from_form (
repository_id : int ,
analysis_run_id : int ,
source_ability_id : int ,
target_ability_id : int = Form ( . . . ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . merge_candidate_ability (
repository_id ,
analysis_run_id ,
source_ability_id ,
target_ability_id = target_ability_id ,
notes = " Merged 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-capabilities/ {source_capability_id} /merge "
)
def merge_candidate_capability_from_form (
repository_id : int ,
analysis_run_id : int ,
source_capability_id : int ,
target_capability_id : int = Form ( . . . ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . merge_candidate_capability (
repository_id ,
analysis_run_id ,
source_capability_id ,
target_capability_id = target_capability_id ,
notes = " Merged 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/ {source_feature_id} /merge "
)
def merge_candidate_feature_from_form (
repository_id : int ,
analysis_run_id : int ,
source_feature_id : int ,
target_feature_id : int = Form ( . . . ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . merge_candidate_feature (
repository_id ,
analysis_run_id ,
source_feature_id ,
target_feature_id = target_feature_id ,
notes = " Merged 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-evidence/ {source_evidence_id} /merge "
)
def merge_candidate_evidence_from_form (
repository_id : int ,
analysis_run_id : int ,
source_evidence_id : int ,
target_evidence_id : int = Form ( . . . ) ,
service : RegistryService = Depends ( get_service ) ,
) - > RedirectResponse :
service . merge_candidate_evidence (
repository_id ,
analysis_run_id ,
source_evidence_id ,
target_evidence_id = target_evidence_id ,
notes = " Merged from web UI " ,
)
return RedirectResponse (
f " /ui/repos/ { repository_id } /analysis-runs/ { analysis_run_id } " ,
status_code = 303 ,
)
2026-04-26 15:54:37 +02:00
def render_run_compare_link ( repository_id : int , run_id : int , runs : list ) - > str :
base_run = next ( ( run for run in runs if run . id < run_id ) , None )
if base_run is None :
return ' <span class= " muted " >Baseline</span> '
href = f " /ui/repos/ { repository_id } /analysis-runs/ { base_run . id } /diff/ { run_id } "
return f ' <a href= " { href } " >Compare to # { base_run . id } </a> '
def render_run_detail_compare_link (
repository_id : int ,
analysis_run_id : int ,
runs : list ,
) - > str :
base_run = next ( ( run for run in runs if run . id < analysis_run_id ) , None )
if base_run is None :
return " "
href = f " /ui/repos/ { repository_id } /analysis-runs/ { base_run . id } /diff/ { analysis_run_id } "
return f ' <a class= " button secondary " href= " { href } " >Compare to # { base_run . id } </a> '
def render_diff_section ( section : dict ) - > str :
groups = [
( " added " , " Added " ) ,
( " removed " , " Removed " ) ,
( " changed " , " Changed " ) ,
( " weakened " , " Weakened " ) ,
]
rendered = " " . join (
render_diff_group ( section . get ( key , [ ] ) , title )
for key , title in groups
)
return rendered or ' <p class= " muted " >No differences.</p> '
def render_diff_group ( items : list [ dict ] , title : str ) - > str :
if not items :
return " "
rows = " \n " . join (
f """
< tr >
< td > < span class = " pill " > { escape ( item [ ' item_type ' ] ) } < / span > < / td >
< td class = " source " > { escape ( item [ ' key ' ] ) } < / td >
< td > { render_diff_payload ( item . get ( ' base ' ) ) } < / td >
< td > { render_diff_payload ( item . get ( ' target ' ) ) } < / td >
< / tr >
"""
for item in items
)
return f """
< h3 > { escape ( title ) } < / h3 >
< table >
< thead > < tr > < th > Type < / th > < th > Key < / th > < th > Base < / th > < th > Target < / th > < / tr > < / thead >
< tbody > { rows } < / tbody >
< / table >
"""
def render_diff_payload ( payload : dict | None ) - > str :
if payload is None :
return ' <span class= " muted " >None</span> '
preferred = [
" name " ,
" description " ,
" confidence " ,
" strength " ,
" path " ,
" location " ,
" reference " ,
" value " ,
" text " ,
]
parts = [ ]
for key in preferred :
if key not in payload or payload [ key ] in ( None , " " , [ ] ) :
continue
value = str ( payload [ key ] )
if key == " text " and len ( value ) > 180 :
value = value [ : 180 ] + " ... "
parts . append (
f ' <div><span class= " muted " > { escape ( key ) } </span>: { escape ( value ) } </div> '
)
return " " . join ( parts ) or ' <span class= " muted " >No display fields.</span> '
2026-04-26 16:13:53 +02:00
def render_repository_checkbox_list (
service : RegistryService ,
repositories : list ,
) - > str :
if not repositories :
return ' <p class= " muted " >No repositories registered.</p> '
rows = [ ]
for repository in repositories :
ability_map = service . ability_map ( repository . id )
approved_count = len ( ability_map . abilities )
disabled = " disabled " if approved_count == 0 else " "
status = (
f " { approved_count } approved "
f " { ' ability ' if approved_count == 1 else ' abilities ' } "
if approved_count
else " No approved profile "
)
rows . append (
f """
< label >
< span >
< input style = " width:auto " type = " checkbox " name = " repository_ids " value = " {repository.id} " { disabled } >
{ escape ( repository . name ) }
< / span >
< span class = " muted " > { escape ( status ) } < / span >
< / label >
"""
)
return f ' <div class= " stack " > { " " . join ( rows ) } </div> '
def render_compared_repositories ( repositories : list [ dict ] ) - > str :
if not repositories :
return ' <p class= " muted " >No repositories selected.</p> '
rows = " \n " . join (
f """
< tr >
< td > < a href = " /ui/repos/ {repository['id']} " > { escape ( repository [ ' name ' ] ) } < / a > < / td >
< td > < span class = " pill " > { escape ( repository [ ' status ' ] ) } < / span > < / td >
< td class = " source " > { escape ( repository [ ' branch ' ] ) } < / td >
< / tr >
"""
for repository in repositories
)
return f """
< table >
< thead > < tr > < th > Name < / th > < th > Status < / th > < th > Branch < / th > < / tr > < / thead >
< tbody > { rows } < / tbody >
< / table >
"""
def render_compared_abilities ( abilities : list [ dict ] ) - > str :
if not abilities :
return ' <p class= " muted " >No shared approved abilities.</p> '
rows = [ ]
for ability in abilities :
repositories = " " . join (
f """
< li >
< strong > { escape ( repository [ ' repository_name ' ] ) } < / strong >
< span class = " pill " > { repository [ ' confidence ' ] : .2 f } { escape ( repository [ ' confidence_label ' ] ) } < / span >
{ render_compared_capability_pills ( repository [ ' capabilities ' ] ) }
< / li >
"""
for repository in ability [ " repositories " ]
)
rows . append (
f """
< li >
< strong > { escape ( ability [ ' name ' ] ) } < / strong >
< ul > { repositories } < / ul >
< / li >
"""
)
return f ' <div class= " tree " ><ul> { " " . join ( rows ) } </ul></div> '
def render_compared_capability_pills ( capabilities : list [ dict ] ) - > str :
if not capabilities :
return " "
return " " . join (
f ' <span class= " pill " > { escape ( capability [ " name " ] ) } · { capability [ " evidence_count " ] } evidence</span> '
for capability in capabilities
)
def render_unique_capabilities ( capabilities : list [ dict ] ) - > str :
if not capabilities :
return ' <p class= " muted " >No unique approved capabilities.</p> '
rows = " \n " . join (
f """
< tr >
< td > < a href = " /ui/repos/ {capability['repository_id']} " > { escape ( capability [ ' repository_name ' ] ) } < / a > < / td >
< td > { escape ( capability [ ' ability_name ' ] ) } < / td >
< td > < strong > { escape ( capability [ ' capability_name ' ] ) } < / strong > < / td >
< / tr >
"""
for capability in capabilities
)
return f """
< table >
< thead > < tr > < th > Repository < / th > < th > Ability < / th > < th > Capability < / th > < / tr > < / thead >
< tbody > { rows } < / tbody >
< / table >
"""
def render_gap_report ( report : dict ) - > str :
return f """
< div class = " grid " >
< section >
< h3 > Matched < / h3 >
{ render_gap_matches ( report [ " matched_capabilities " ] ) }
< / section >
< section >
< h3 > Missing < / h3 >
{ render_gap_list ( report [ " missing_capabilities " ] , " No missing capabilities. " ) }
< / section >
< section >
< h3 > Weak Evidence < / h3 >
{ render_weak_evidence ( report [ " weakly_evidenced_capabilities " ] ) }
< / section >
< section >
< h3 > Duplicates < / h3 >
{ render_duplicate_capabilities ( report [ " duplicate_capabilities " ] ) }
< / section >
< / div >
"""
def render_gap_matches ( matches : list [ dict ] ) - > str :
if not matches :
return ' <p class= " muted " >No desired capabilities matched.</p> '
rows = " \n " . join (
f """
< tr >
< td > < strong > { escape ( match [ ' capability ' ] ) } < / strong > < / td >
< td > { escape ( ' , ' . join ( match [ ' repositories ' ] ) ) } < / td >
< / tr >
"""
for match in matches
)
return f " <table><tbody> { rows } </tbody></table> "
def render_gap_list ( values : list [ str ] , empty : str ) - > str :
if not values :
return f ' <p class= " muted " > { escape ( empty ) } </p> '
return " <ul> " + " " . join ( f " <li> { escape ( value ) } </li> " for value in values ) + " </ul> "
def render_weak_evidence ( items : list [ dict ] ) - > str :
if not items :
return ' <p class= " muted " >No weak evidence flags.</p> '
rows = " \n " . join (
f """
< tr >
< td > { escape ( item [ ' capability ' ] ) } < / td >
< td > { escape ( item [ ' repository_name ' ] ) } < / td >
< td > { escape ( str ( item [ ' strongest_evidence ' ] or ' none ' ) ) } < / td >
< td > { item [ ' confidence ' ] : .2 f } < span class = " pill " > { escape ( item [ ' confidence_label ' ] ) } < / span > < / td >
< / tr >
"""
for item in items
)
return f """
< table >
< thead > < tr > < th > Capability < / th > < th > Repository < / th > < th > Evidence < / th > < th > Confidence < / th > < / tr > < / thead >
< tbody > { rows } < / tbody >
< / table >
"""
def render_duplicate_capabilities ( items : list [ dict ] ) - > str :
if not items :
return ' <p class= " muted " >No duplicate capabilities.</p> '
rows = " \n " . join (
f """
< tr >
< td > { escape ( item [ ' capability ' ] ) } < / td >
< td > { escape ( ' , ' . join ( item [ ' repositories ' ] ) ) } < / td >
< / tr >
"""
for item in items
)
return f " <table><tbody> { rows } </tbody></table> "
def split_capability_lines ( value : str ) - > list [ str ] :
normalized = value . replace ( " , " , " \n " )
return [ line . strip ( ) for line in normalized . splitlines ( ) if line . strip ( ) ]
2026-04-25 23:27:28 +02:00
def render_candidate_graph ( graph : dict , repository_id : int , analysis_run_id : int ) - > str :
2026-04-25 23:04:15 +02:00
abilities = graph . get ( " abilities " , [ ] )
if not abilities :
return ' <p class= " muted " >No candidates generated.</p> '
items = [ ]
for ability in abilities :
2026-04-25 23:32:18 +02:00
capabilities = " " . join (
render_candidate_capability ( capability , repository_id , analysis_run_id )
for capability in ability [ " capabilities " ]
)
2026-04-25 23:04:15 +02:00
items . append (
f """
< li >
< strong > { escape ( ability [ ' name ' ] ) } < / strong >
2026-04-25 23:50:58 +02:00
< span class = " pill " > ID { ability [ ' id ' ] } < / span >
2026-04-25 23:04:15 +02:00
< span class = " pill " > { escape ( ability [ ' status ' ] ) } < / span >
2026-04-26 08:45:00 +02:00
< span class = " pill " > { ability [ ' confidence ' ] : .2 f } { escape ( ability [ ' confidence_label ' ] ) } < / span >
2026-04-25 23:27:28 +02:00
{ render_candidate_ability_actions ( ability , repository_id , analysis_run_id ) }
2026-04-25 23:04:15 +02:00
< p class = " muted " > { escape ( ability [ ' description ' ] ) } < / p >
2026-04-25 23:32:18 +02:00
{ render_candidate_edit_form ( ' candidate-abilities ' , ability , repository_id , analysis_run_id ) }
2026-04-25 23:50:58 +02:00
{ render_candidate_merge_form ( ' candidate-abilities ' , ability , repository_id , analysis_run_id , ' target_ability_id ' , ' Merge into ability ID ' ) }
2026-04-25 23:04:15 +02:00
{ render_sources ( ability [ ' source_refs ' ] ) }
< ul > { capabilities } < / ul >
< / li >
"""
)
return f ' <div class= " tree " ><ul> { " " . join ( items ) } </ul></div> '
2026-04-26 00:04:19 +02:00
def render_repository_facts ( languages : list [ str ] , frameworks : list [ str ] ) - > str :
if not languages and not frameworks :
return " "
language_pills = " " . join (
f ' <span class= " pill " >Language: { escape ( language ) } </span> '
for language in languages
)
framework_pills = " " . join (
f ' <span class= " pill " >Framework: { escape ( framework ) } </span> '
for framework in frameworks
)
return f ' <p class= " actions " > { language_pills } { framework_pills } </p> '
2026-04-26 02:26:34 +02:00
def split_csv ( value : str ) - > list [ str ] :
return [ item . strip ( ) for item in value . split ( " , " ) if item . strip ( ) ]
2026-04-26 00:24:02 +02:00
def render_review_decisions ( decisions : list ) - > str :
if not decisions :
return ' <p class= " muted " >No review decisions yet.</p> '
rows = " \n " . join (
f """
< tr >
< td > { escape ( decision . action ) } < / td >
< td class = " source " > { escape ( decision . created_at ) } < / td >
< td > { escape ( decision . notes ) } < / td >
< / tr >
"""
for decision in decisions
)
return f """
< table >
< thead > < tr > < th > Action < / th > < th > Created < / th > < th > Notes < / th > < / tr > < / thead >
< tbody > { rows } < / tbody >
< / table >
"""
2026-04-26 02:47:10 +02:00
def render_content_chunks ( chunks : list ) - > str :
if not chunks :
return ' <p class= " muted " >No content chunks extracted.</p> '
rows = " \n " . join (
f """
< tr >
< td > < span class = " pill " > { escape ( chunk . kind ) } < / span > < / td >
< td class = " source " > { escape ( chunk . path ) } : { chunk . start_line } - { chunk . end_line } < / td >
< td > < pre > { escape ( chunk . text [ : 500 ] ) } < / pre > < / td >
< / tr >
"""
for chunk in chunks
)
return f """
< table >
< thead > < tr > < th > Kind < / th > < th > Source < / th > < th > Text < / th > < / tr > < / thead >
< tbody > { rows } < / tbody >
< / table >
"""
2026-04-25 23:27:28 +02:00
def render_candidate_ability_actions (
ability : dict ,
repository_id : int ,
analysis_run_id : int ,
) - > str :
if ability [ " status " ] != " candidate " :
return " "
action = (
f " /ui/repos/ { repository_id } /analysis-runs/ { analysis_run_id } "
f " /candidate-abilities/ { ability [ ' id ' ] } /reject "
)
return f """
< form style = " display:inline " method = " post " action = " {action} " >
< button class = " secondary " type = " submit " > Reject < / button >
< / form >
"""
2026-04-25 23:32:18 +02:00
def render_candidate_edit_form (
collection : str ,
candidate : dict ,
repository_id : int ,
analysis_run_id : int ,
) - > str :
if candidate [ " status " ] != " candidate " :
return " "
action = (
f " /ui/repos/ { repository_id } /analysis-runs/ { analysis_run_id } "
f " / { collection } / { candidate [ ' id ' ] } /edit "
)
confidence = f " { candidate [ ' confidence ' ] : .2f } "
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 >
< 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 >
"""
def render_candidate_capability (
capability : dict ,
repository_id : int ,
analysis_run_id : int ,
) - > str :
2026-04-25 23:04:15 +02:00
features = " " . join (
2026-04-25 23:39:29 +02:00
render_candidate_feature ( feature , repository_id , analysis_run_id )
2026-04-25 23:04:15 +02:00
for feature in capability [ " features " ]
)
evidence = " " . join (
2026-04-25 23:39:29 +02:00
render_candidate_evidence ( item , repository_id , analysis_run_id )
2026-04-25 23:04:15 +02:00
for item in capability [ " evidence " ]
)
return f """
< li >
< strong > { escape ( capability [ ' name ' ] ) } < / strong >
2026-04-25 23:50:58 +02:00
< span class = " pill " > ID { capability [ ' id ' ] } < / span >
2026-04-25 23:04:15 +02:00
< span class = " pill " > { escape ( capability [ ' status ' ] ) } < / span >
2026-04-26 08:45:00 +02:00
< span class = " pill " > { capability [ ' confidence ' ] : .2 f } { escape ( capability [ ' confidence_label ' ] ) } < / span >
2026-04-25 23:39:29 +02:00
{ render_candidate_reject_form ( ' candidate-capabilities ' , capability , repository_id , analysis_run_id ) }
2026-04-25 23:04:15 +02:00
< p class = " muted " > { escape ( capability [ ' description ' ] ) } < / p >
2026-04-25 23:32:18 +02:00
{ render_candidate_edit_form ( ' candidate-capabilities ' , capability , repository_id , analysis_run_id ) }
2026-04-25 23:44:25 +02:00
{ render_candidate_relink_form ( ' candidate-capabilities ' , capability , repository_id , analysis_run_id , ' target_ability_id ' , ' Target ability ID ' ) }
2026-04-25 23:50:58 +02:00
{ render_candidate_merge_form ( ' candidate-capabilities ' , capability , repository_id , analysis_run_id , ' target_capability_id ' , ' Merge into capability ID ' ) }
2026-04-25 23:04:15 +02:00
{ render_sources ( capability [ ' source_refs ' ] ) }
< h3 > Features < / h3 >
< ul > { features or ' <li class= " muted " >No feature candidates.</li> ' } < / ul >
< h3 > Evidence < / h3 >
< ul > { evidence or ' <li class= " muted " >No evidence candidates.</li> ' } < / ul >
< / li >
"""
2026-04-25 23:39:29 +02:00
def render_candidate_feature (
feature : dict ,
repository_id : int ,
analysis_run_id : int ,
) - > str :
return f """
< li >
{ escape ( feature [ " name " ] ) }
2026-04-25 23:50:58 +02:00
< span class = " pill " > ID { feature [ " id " ] } < / span >
2026-04-25 23:39:29 +02:00
< span class = " pill " > { escape ( feature [ " status " ] ) } < / span >
< span class = " pill " > { escape ( feature [ " type " ] ) } < / span >
< span class = " source " > { escape ( feature [ " location " ] ) } < / span >
{ render_candidate_reject_form ( ' candidate-features ' , feature , repository_id , analysis_run_id ) }
2026-04-25 23:44:25 +02:00
{ render_candidate_relink_form ( ' candidate-features ' , feature , repository_id , analysis_run_id , ' target_capability_id ' , ' Target capability ID ' ) }
2026-04-25 23:50:58 +02:00
{ render_candidate_merge_form ( ' candidate-features ' , feature , repository_id , analysis_run_id , ' target_feature_id ' , ' Merge into feature ID ' ) }
2026-04-25 23:39:29 +02:00
< / li >
"""
def render_candidate_evidence (
evidence : dict ,
repository_id : int ,
analysis_run_id : int ,
) - > str :
return f """
< li >
{ escape ( evidence [ " type " ] ) }
2026-04-25 23:50:58 +02:00
< span class = " pill " > ID { evidence [ " id " ] } < / span >
2026-04-25 23:39:29 +02:00
< span class = " pill " > { escape ( evidence [ " status " ] ) } < / span >
< span class = " pill " > { escape ( evidence [ " strength " ] ) } < / span >
< span class = " source " > { escape ( evidence [ " reference " ] ) } < / span >
{ render_candidate_reject_form ( ' candidate-evidence ' , evidence , repository_id , analysis_run_id ) }
2026-04-25 23:44:25 +02:00
{ render_candidate_relink_form ( ' candidate-evidence ' , evidence , repository_id , analysis_run_id , ' target_capability_id ' , ' Target capability ID ' ) }
2026-04-25 23:50:58 +02:00
{ render_candidate_merge_form ( ' candidate-evidence ' , evidence , repository_id , analysis_run_id , ' target_evidence_id ' , ' Merge into evidence ID ' ) }
2026-04-25 23:39:29 +02:00
< / li >
"""
def render_candidate_reject_form (
collection : str ,
candidate : dict ,
repository_id : int ,
analysis_run_id : int ,
) - > str :
if candidate [ " status " ] != " candidate " :
return " "
action = (
f " /ui/repos/ { repository_id } /analysis-runs/ { analysis_run_id } "
f " / { collection } / { candidate [ ' id ' ] } /reject "
)
return f """
< form style = " display:inline " method = " post " action = " {action} " >
< button class = " secondary " type = " submit " > Reject < / button >
< / form >
"""
2026-04-25 23:44:25 +02:00
def render_candidate_relink_form (
collection : str ,
candidate : dict ,
repository_id : int ,
analysis_run_id : int ,
field_name : str ,
label : str ,
) - > str :
if candidate [ " status " ] != " candidate " :
return " "
action = (
f " /ui/repos/ { repository_id } /analysis-runs/ { analysis_run_id } "
f " / { collection } / { candidate [ ' id ' ] } /relink "
)
return f """
< form style = " display:inline-grid; grid-template-columns: 120px auto; gap: 6px; align-items: end; " method = " post " action = " {action} " >
< label > { label } < input name = " {field_name} " type = " number " min = " 1 " required > < / label >
< button class = " secondary " type = " submit " > Relink < / button >
< / form >
"""
2026-04-25 23:50:58 +02:00
def render_candidate_merge_form (
collection : str ,
candidate : dict ,
repository_id : int ,
analysis_run_id : int ,
field_name : str ,
label : str ,
) - > str :
if candidate [ " status " ] != " candidate " :
return " "
action = (
f " /ui/repos/ { repository_id } /analysis-runs/ { analysis_run_id } "
f " / { collection } / { candidate [ ' id ' ] } /merge "
)
return f """
< form style = " display:inline-grid; grid-template-columns: 140px auto; gap: 6px; align-items: end; " method = " post " action = " {action} " >
< label > { label } < input name = " {field_name} " type = " number " min = " 1 " required > < / label >
< button class = " secondary " type = " submit " > Merge < / button >
< / form >
"""
2026-04-26 02:30:18 +02:00
def render_ability_map ( ability_map : dict , repository_id : int ) - > str :
2026-04-25 23:04:15 +02:00
abilities = ability_map . get ( " abilities " , [ ] )
if not abilities :
return ' <p class= " muted " >No approved entries yet.</p> '
items = [ ]
for ability in abilities :
capabilities = [ ]
for capability in ability [ " capabilities " ] :
features = " " . join (
2026-04-26 02:30:18 +02:00
render_approved_feature ( feature , repository_id )
2026-04-25 23:04:15 +02:00
for feature in capability [ " features " ]
)
evidence = " " . join (
2026-04-26 02:30:18 +02:00
render_approved_evidence ( item , repository_id )
2026-04-25 23:04:15 +02:00
for item in capability [ " evidence " ]
)
capabilities . append (
f """
2026-04-26 00:08:55 +02:00
< li id = " capability- {capability['id']} " >
2026-04-25 23:04:15 +02:00
< strong > { escape ( capability [ ' name ' ] ) } < / strong >
2026-04-26 02:26:34 +02:00
< span class = " pill " > ID { capability [ ' id ' ] } < / span >
2026-04-26 08:45:00 +02:00
< span class = " pill " > { capability [ ' confidence ' ] : .2 f } { escape ( capability [ ' confidence_label ' ] ) } < / span >
2026-04-25 23:04:15 +02:00
< p class = " muted " > { escape ( capability [ ' description ' ] ) } < / p >
2026-04-26 02:30:18 +02:00
{ render_approved_capability_forms ( capability , repository_id ) }
2026-04-25 23:04:15 +02:00
< ul > { features } { evidence } < / ul >
< / li >
"""
)
items . append (
f """
2026-04-26 00:08:55 +02:00
< li id = " ability- {ability['id']} " >
2026-04-25 23:04:15 +02:00
< strong > { escape ( ability [ ' name ' ] ) } < / strong >
2026-04-26 02:26:34 +02:00
< span class = " pill " > ID { ability [ ' id ' ] } < / span >
2026-04-26 08:45:00 +02:00
< span class = " pill " > { ability [ ' confidence ' ] : .2 f } { escape ( ability [ ' confidence_label ' ] ) } < / span >
2026-04-25 23:04:15 +02:00
< p class = " muted " > { escape ( ability [ ' description ' ] ) } < / p >
2026-04-26 02:30:18 +02:00
{ render_approved_ability_forms ( ability , repository_id ) }
2026-04-25 23:04:15 +02:00
< ul > { ' ' . join ( capabilities ) } < / ul >
< / li >
"""
)
return f ' <div class= " tree " ><ul> { " " . join ( items ) } </ul></div> '
2026-04-26 02:30:18 +02:00
def render_approved_ability_forms ( ability : dict , repository_id : int ) - > str :
return f """
< form class = " stack " method = " post " action = " /ui/repos/ {repository_id} /abilities/ {ability['id']} /edit " >
< label > Name < input name = " name " value = " { escape(ability[ ' name ' ])} " required > < / label >
< label > Description < textarea name = " description " rows = " 2 " > { escape ( ability [ ' description ' ] ) } < / textarea > < / label >
< label > Confidence < input name = " confidence " type = " number " min = " 0 " max = " 1 " step = " 0.01 " value = " {ability['confidence']:.2f} " required > < / label >
< div class = " actions " >
< button class = " secondary " type = " submit " > Save Ability < / button >
< / div >
< / form >
< form method = " post " action = " /ui/repos/ {repository_id} /abilities/ {ability['id']} /delete " >
< button class = " secondary " type = " submit " > Delete Ability < / button >
< / form >
"""
def render_approved_capability_forms ( capability : dict , repository_id : int ) - > str :
inputs = " , " . join ( capability [ " inputs " ] )
outputs = " , " . join ( capability [ " outputs " ] )
return f """
< form class = " stack " method = " post " action = " /ui/repos/ {repository_id} /capabilities/ {capability['id']} /edit " >
< label > Name < input name = " name " value = " { escape(capability[ ' name ' ])} " required > < / label >
< label > Description < textarea name = " description " rows = " 2 " > { escape ( capability [ ' description ' ] ) } < / textarea > < / label >
< label > Inputs < input name = " inputs " value = " { escape(inputs)} " > < / label >
< label > Outputs < input name = " outputs " value = " { escape(outputs)} " > < / label >
< label > Confidence < input name = " confidence " type = " number " min = " 0 " max = " 1 " step = " 0.01 " value = " {capability['confidence']:.2f} " required > < / label >
< div class = " actions " >
< button class = " secondary " type = " submit " > Save Capability < / button >
< / div >
< / form >
< form method = " post " action = " /ui/repos/ {repository_id} /capabilities/ {capability['id']} /delete " >
< button class = " secondary " type = " submit " > Delete Capability < / button >
< / form >
"""
def render_approved_feature ( feature : dict , repository_id : int ) - > str :
2026-04-26 00:13:45 +02:00
return f """
< li >
{ escape ( feature [ " name " ] ) }
< span class = " pill " > { escape ( feature [ " type " ] ) } < / span >
2026-04-26 08:45:00 +02:00
< span class = " pill " > { feature [ " confidence " ] : .2 f } { escape ( feature [ " confidence_label " ] ) } < / span >
2026-04-26 00:13:45 +02:00
< span class = " source " > { escape ( feature [ " location " ] ) } < / span >
{ render_sources ( feature . get ( " source_refs " , [ ] ) ) }
2026-04-26 02:30:18 +02:00
< 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 > Type < input name = " type " value = " { escape(feature[ ' type ' ])} " required > < / label >
< label > Location < input name = " location " value = " { escape(feature[ ' location ' ])} " > < / label >
< label > Confidence < input name = " confidence " type = " number " min = " 0 " max = " 1 " step = " 0.01 " value = " {feature['confidence']:.2f} " required > < / label >
< button class = " secondary " type = " submit " > Save Feature < / button >
< / form >
< form method = " post " action = " /ui/repos/ {repository_id} /features/ {feature['id']} /delete " >
< button class = " secondary " type = " submit " > Delete Feature < / button >
< / form >
2026-04-26 00:13:45 +02:00
< / li >
"""
2026-04-26 02:30:18 +02:00
def render_approved_evidence ( evidence : dict , repository_id : int ) - > str :
2026-04-26 00:13:45 +02:00
return f """
< li >
{ escape ( evidence [ " type " ] ) }
< span class = " pill " > { escape ( evidence [ " strength " ] ) } < / span >
< span class = " source " > { escape ( evidence [ " reference " ] ) } < / span >
{ render_sources ( evidence . get ( " source_refs " , [ ] ) ) }
2026-04-26 02:30:18 +02:00
< form class = " stack " method = " post " action = " /ui/repos/ {repository_id} /evidence/ {evidence['id']} /edit " >
< label > Type < input name = " type " value = " { escape(evidence[ ' type ' ])} " required > < / label >
< label > Reference < input name = " reference " value = " { escape(evidence[ ' reference ' ])} " required > < / label >
< label > Strength < input name = " strength " value = " { escape(evidence[ ' strength ' ])} " required > < / label >
< button class = " secondary " type = " submit " > Save Evidence < / button >
< / form >
< form method = " post " action = " /ui/repos/ {repository_id} /evidence/ {evidence['id']} /delete " >
< button class = " secondary " type = " submit " > Delete Evidence < / button >
< / form >
2026-04-26 00:13:45 +02:00
< / li >
"""
2026-04-26 00:08:55 +02:00
def search_result_href ( result : dict ) - > str :
href = f " /ui/repos/ { result [ ' repository_id ' ] } "
if result . get ( " capability_id " ) :
return f " { href } #capability- { result [ ' capability_id ' ] } "
if result . get ( " ability_id " ) :
return f " { href } #ability- { result [ ' ability_id ' ] } "
return href
2026-04-25 23:04:15 +02:00
def render_sources ( source_refs : list [ dict ] ) - > str :
if not source_refs :
return " "
sources = " , " . join (
2026-04-26 00:13:45 +02:00
f ' <span class= " source " > { escape ( ref [ " kind " ] ) } : { escape ( source_ref_label ( ref ) ) } </span> '
2026-04-25 23:04:15 +02:00
for ref in source_refs [ : 5 ]
)
if len ( source_refs ) > 5 :
sources + = f ' <span class= " muted " >+ { len ( source_refs ) - 5 } more</span> '
return f " <p> { sources } </p> "
2026-04-25 23:59:38 +02:00
2026-04-26 00:13:45 +02:00
def source_ref_label ( ref : dict ) - > str :
label = ref [ " path " ] or ref [ " name " ]
if ref . get ( " line " ) :
label = f " { label } : { ref [ ' line ' ] } "
return label
2026-04-25 23:59:38 +02:00
def render_search_context ( result : dict ) - > str :
details = [ ]
if result . get ( " ability_name " ) :
details . append ( f " Ability: { escape ( result [ ' ability_name ' ] ) } " )
if result . get ( " capability_name " ) :
details . append ( f " Capability: { escape ( result [ ' capability_name ' ] ) } " )
if result . get ( " evidence_level " ) :
details . append ( f " Evidence: { escape ( result [ ' evidence_level ' ] ) } " )
if result . get ( " source_reference " ) :
details . append ( f " Source: { escape ( result [ ' source_reference ' ] ) } " )
if result . get ( " match_description " ) :
details . append ( escape ( result [ " match_description " ] ) )
if not details :
return " "
return f ' <p class= " muted " > { " · " . join ( details ) } </p> '