confidence labels

This commit is contained in:
2026-04-26 08:45:00 +02:00
parent 2403accd06
commit 70feabe965
6 changed files with 194 additions and 61 deletions

View File

@@ -82,6 +82,7 @@ curl http://127.0.0.1:8000/repos/1/analysis-runs/1/candidate-graph
```
Candidate entries are source-linked review seeds. They are not canonical registry truth until a review workflow approves them.
Candidate, approved, and search responses include numeric confidence values plus `low`, `medium`, or `high` confidence labels for quick triage.
Approve a candidate graph into the canonical registry:

View File

@@ -4,6 +4,14 @@ from dataclasses import dataclass, field
from typing import Any
def confidence_label(confidence: float) -> str:
if confidence >= 0.8:
return "high"
if confidence >= 0.5:
return "medium"
return "low"
@dataclass(frozen=True)
class Repository:
id: int
@@ -107,6 +115,7 @@ class CandidateFeature:
confidence: float
status: str
source_refs: list[SourceReference]
confidence_label: str = ""
@dataclass(frozen=True)
@@ -119,6 +128,7 @@ class CandidateCapability:
confidence: float
status: str
source_refs: list[SourceReference]
confidence_label: str = ""
features: list[CandidateFeature] = field(default_factory=list)
evidence: list[CandidateEvidence] = field(default_factory=list)
@@ -131,6 +141,7 @@ class CandidateAbility:
confidence: float
status: str
source_refs: list[SourceReference]
confidence_label: str = ""
capabilities: list[CandidateCapability] = field(default_factory=list)
@@ -157,6 +168,7 @@ class Feature:
type: str
location: str
confidence: float
confidence_label: str = ""
source_refs: list[SourceReference] = field(default_factory=list)
@@ -168,6 +180,7 @@ class Capability:
inputs: list[str]
outputs: list[str]
confidence: float
confidence_label: str = ""
features: list[Feature] = field(default_factory=list)
evidence: list[Evidence] = field(default_factory=list)
@@ -178,6 +191,7 @@ class Ability:
name: str
description: str
confidence: float
confidence_label: str = ""
capabilities: list[Capability] = field(default_factory=list)
@@ -194,6 +208,7 @@ class SearchResult:
match_type: str
match_name: str
confidence: float
confidence_label: str = ""
match_description: str = ""
matched_field: str = ""
ability_id: int | None = None
@@ -212,6 +227,7 @@ class AbilitySummary:
name: str
description: str
confidence: float
confidence_label: str = ""
@dataclass(frozen=True)
@@ -224,3 +240,4 @@ class CapabilitySummary:
name: str
description: str
confidence: float
confidence_label: str = ""

View File

@@ -25,6 +25,7 @@ from repo_registry.core.models import (
ReviewDecision,
SearchResult,
SourceReference,
confidence_label,
)
from repo_registry.content_indexing.extractor import ContentChunkCandidate
from repo_registry.candidate_graph.generator import CandidateAbilityDraft
@@ -401,6 +402,7 @@ class RegistryStore:
confidence=row["confidence"],
status=row["status"],
source_refs=self._source_refs_from_json(row["source_refs"]),
confidence_label=confidence_label(row["confidence"]),
)
)
@@ -429,6 +431,7 @@ class RegistryStore:
confidence=row["confidence"],
status=row["status"],
source_refs=self._source_refs_from_json(row["source_refs"]),
confidence_label=confidence_label(row["confidence"]),
features=features_by_capability.get(row["id"], []),
evidence=evidence_by_capability.get(row["id"], []),
)
@@ -442,6 +445,7 @@ class RegistryStore:
confidence=row["confidence"],
status=row["status"],
source_refs=self._source_refs_from_json(row["source_refs"]),
confidence_label=confidence_label(row["confidence"]),
capabilities=capabilities_by_ability.get(row["id"], []),
)
for row in ability_rows
@@ -1119,6 +1123,7 @@ class RegistryStore:
name=row["name"],
description=row["description"],
confidence=row["confidence"],
confidence_label=confidence_label(row["confidence"]),
)
for row in rows
]
@@ -1146,6 +1151,7 @@ class RegistryStore:
name=row["name"],
description=row["description"],
confidence=row["confidence"],
confidence_label=confidence_label(row["confidence"]),
)
for row in rows
]
@@ -1555,6 +1561,7 @@ class RegistryStore:
type=row["type"],
location=row["location"],
confidence=row["confidence"],
confidence_label=confidence_label(row["confidence"]),
source_refs=self._source_refs_from_json(row["source_refs"]),
)
)
@@ -1581,6 +1588,7 @@ class RegistryStore:
inputs=json.loads(row["inputs"]),
outputs=json.loads(row["outputs"]),
confidence=row["confidence"],
confidence_label=confidence_label(row["confidence"]),
features=features_by_capability.get(row["id"], []),
evidence=evidence_by_capability.get(row["id"], []),
)
@@ -1592,6 +1600,7 @@ class RegistryStore:
name=row["name"],
description=row["description"],
confidence=row["confidence"],
confidence_label=confidence_label(row["confidence"]),
capabilities=capabilities_by_ability.get(row["id"], []),
)
for row in ability_rows
@@ -1706,9 +1715,10 @@ class RegistryStore:
repository_name=row["repository_name"],
match_type="repository",
match_name=row["repository_name"],
confidence=1.0,
confidence_label=confidence_label(1.0),
match_description=row["description"] or "",
matched_field=matched_field,
confidence=1.0,
)
)
for row in ability_rows:
@@ -1721,9 +1731,10 @@ class RegistryStore:
repository_name=row["repository_name"],
match_type="ability",
match_name=row["ability_name"],
confidence=row["confidence"],
confidence_label=confidence_label(row["confidence"]),
match_description=row["ability_description"],
matched_field=matched_field,
confidence=row["confidence"],
ability_id=row["ability_id"],
ability_name=row["ability_name"],
)
@@ -1740,9 +1751,10 @@ class RegistryStore:
repository_name=row["repository_name"],
match_type="capability",
match_name=row["capability_name"],
confidence=row["confidence"],
confidence_label=confidence_label(row["confidence"]),
match_description=row["capability_description"],
matched_field=matched_field,
confidence=row["confidence"],
ability_id=row["ability_id"],
ability_name=row["ability_name"],
capability_id=row["capability_id"],
@@ -1764,9 +1776,10 @@ class RegistryStore:
repository_name=row["repository_name"],
match_type="feature",
match_name=row["feature_name"],
confidence=row["confidence"],
confidence_label=confidence_label(row["confidence"]),
match_description=row["feature_type"],
matched_field=matched_field,
confidence=row["confidence"],
ability_id=row["ability_id"],
ability_name=row["ability_name"],
capability_id=row["capability_id"],
@@ -1789,9 +1802,12 @@ class RegistryStore:
repository_name=row["repository_name"],
match_type="evidence",
match_name=row["reference"],
confidence=self._evidence_confidence(row["strength"]),
confidence_label=confidence_label(
self._evidence_confidence(row["strength"])
),
match_description=row["evidence_type"],
matched_field=matched_field,
confidence=self._evidence_confidence(row["strength"]),
ability_id=row["ability_id"],
ability_name=row["ability_name"],
capability_id=row["capability_id"],

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from dataclasses import asdict
from pathlib import Path
from fastapi import Depends, FastAPI, HTTPException
from fastapi import Depends, FastAPI, HTTPException, Query
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -324,7 +324,26 @@ class CandidateEvidenceMerge(BaseModel):
}
app = FastAPI(title="Repository Ability Registry", version="0.1.0")
API_DESCRIPTION = (
"Register repositories, analyze their observable implementation facts, "
"curate reviewable ability graphs, and search approved repository abilities."
)
OPENAPI_TAGS = [
{"name": "health", "description": "Service health checks."},
{"name": "repositories", "description": "Repository registration and metadata."},
{"name": "analysis", "description": "Repository scans and extracted review inputs."},
{"name": "review", "description": "Candidate graph approval and correction workflow."},
{"name": "registry", "description": "Approved ability maps and manual registry CRUD."},
{"name": "search", "description": "Agent-facing discovery endpoints."},
]
app = FastAPI(
title="Repository Ability Registry",
version="0.1.0",
description=API_DESCRIPTION,
openapi_tags=OPENAPI_TAGS,
)
from repo_registry.web_ui.views import router as ui_router
@@ -332,12 +351,12 @@ from repo_registry.web_ui.views import router as ui_router
app.include_router(ui_router)
@app.get("/health")
@app.get("/health", tags=["health"])
def health() -> dict[str, str]:
return {"status": "ok"}
@app.post("/repos", status_code=201)
@app.post("/repos", status_code=201, tags=["repositories"])
def create_repository(
payload: RepositoryCreate,
service: RegistryService = Depends(get_service),
@@ -349,14 +368,14 @@ def create_repository(
return asdict(repository)
@app.get("/repos")
@app.get("/repos", tags=["repositories"])
def list_repositories(
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:
return [asdict(repository) for repository in service.list_repositories()]
@app.get("/repos/{repository_id}")
@app.get("/repos/{repository_id}", tags=["repositories"])
def get_repository(
repository_id: int,
service: RegistryService = Depends(get_service),
@@ -367,7 +386,7 @@ def get_repository(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.patch("/repos/{repository_id}")
@app.patch("/repos/{repository_id}", tags=["repositories"])
def update_repository(
repository_id: int,
payload: RepositoryUpdate,
@@ -384,7 +403,7 @@ def update_repository(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.delete("/repos/{repository_id}", status_code=204)
@app.delete("/repos/{repository_id}", status_code=204, tags=["repositories"])
def delete_repository(
repository_id: int,
service: RegistryService = Depends(get_service),
@@ -395,7 +414,7 @@ def delete_repository(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post("/repos/{repository_id}/analysis-runs", status_code=201)
@app.post("/repos/{repository_id}/analysis-runs", status_code=201, tags=["analysis"])
def create_analysis_run(
repository_id: int,
payload: AnalysisRunCreate,
@@ -411,7 +430,7 @@ def create_analysis_run(
return asdict(summary)
@app.get("/repos/{repository_id}/analysis-runs")
@app.get("/repos/{repository_id}/analysis-runs", tags=["analysis"])
def list_analysis_runs(
repository_id: int,
service: RegistryService = Depends(get_service),
@@ -422,7 +441,7 @@ def list_analysis_runs(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.get("/repos/{repository_id}/analysis-runs/{analysis_run_id}")
@app.get("/repos/{repository_id}/analysis-runs/{analysis_run_id}", tags=["analysis"])
def get_analysis_run(
repository_id: int,
analysis_run_id: int,
@@ -434,7 +453,7 @@ def get_analysis_run(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.get("/repos/{repository_id}/review-decisions")
@app.get("/repos/{repository_id}/review-decisions", tags=["review"])
def list_repository_review_decisions(
repository_id: int,
service: RegistryService = Depends(get_service),
@@ -448,7 +467,10 @@ def list_repository_review_decisions(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.get("/repos/{repository_id}/analysis-runs/{analysis_run_id}/review-decisions")
@app.get(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/review-decisions",
tags=["review"],
)
def list_analysis_run_review_decisions(
repository_id: int,
analysis_run_id: int,
@@ -466,7 +488,7 @@ def list_analysis_run_review_decisions(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.get("/repos/{repository_id}/observed-facts")
@app.get("/repos/{repository_id}/observed-facts", tags=["analysis"])
def list_observed_facts(
repository_id: int,
analysis_run_id: int | None = None,
@@ -481,7 +503,7 @@ def list_observed_facts(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.get("/repos/{repository_id}/content-chunks")
@app.get("/repos/{repository_id}/content-chunks", tags=["analysis"])
def list_content_chunks(
repository_id: int,
analysis_run_id: int | None = None,
@@ -496,7 +518,10 @@ def list_content_chunks(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.get("/repos/{repository_id}/analysis-runs/{analysis_run_id}/content-chunks")
@app.get(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/content-chunks",
tags=["analysis"],
)
def list_analysis_run_content_chunks(
repository_id: int,
analysis_run_id: int,
@@ -511,7 +536,10 @@ def list_analysis_run_content_chunks(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.get("/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph")
@app.get(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph",
tags=["review"],
)
def get_candidate_graph(
repository_id: int,
analysis_run_id: int,
@@ -523,7 +551,10 @@ def get_candidate_graph(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post("/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph/approve")
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph/approve",
tags=["review"],
)
def approve_candidate_graph(
repository_id: int,
analysis_run_id: int,
@@ -544,7 +575,8 @@ def approve_candidate_graph(
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
"/candidate-abilities/{candidate_ability_id}/reject"
"/candidate-abilities/{candidate_ability_id}/reject",
tags=["review"],
)
def reject_candidate_ability(
repository_id: int,
@@ -568,7 +600,8 @@ def reject_candidate_ability(
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
"/candidate-capabilities/{candidate_capability_id}/reject"
"/candidate-capabilities/{candidate_capability_id}/reject",
tags=["review"],
)
def reject_candidate_capability(
repository_id: int,
@@ -592,7 +625,8 @@ def reject_candidate_capability(
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
"/candidate-features/{candidate_feature_id}/reject"
"/candidate-features/{candidate_feature_id}/reject",
tags=["review"],
)
def reject_candidate_feature(
repository_id: int,
@@ -616,7 +650,8 @@ def reject_candidate_feature(
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
"/candidate-evidence/{candidate_evidence_id}/reject"
"/candidate-evidence/{candidate_evidence_id}/reject",
tags=["review"],
)
def reject_candidate_evidence(
repository_id: int,
@@ -640,7 +675,8 @@ def reject_candidate_evidence(
@app.patch(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
"/candidate-abilities/{candidate_ability_id}"
"/candidate-abilities/{candidate_ability_id}",
tags=["review"],
)
def edit_candidate_ability(
repository_id: int,
@@ -664,7 +700,8 @@ def edit_candidate_ability(
@app.patch(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
"/candidate-capabilities/{candidate_capability_id}"
"/candidate-capabilities/{candidate_capability_id}",
tags=["review"],
)
def edit_candidate_capability(
repository_id: int,
@@ -688,7 +725,8 @@ def edit_candidate_capability(
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
"/candidate-capabilities/{candidate_capability_id}/relink"
"/candidate-capabilities/{candidate_capability_id}/relink",
tags=["review"],
)
def relink_candidate_capability(
repository_id: int,
@@ -712,7 +750,8 @@ def relink_candidate_capability(
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
"/candidate-features/{candidate_feature_id}/relink"
"/candidate-features/{candidate_feature_id}/relink",
tags=["review"],
)
def relink_candidate_feature(
repository_id: int,
@@ -736,7 +775,8 @@ def relink_candidate_feature(
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
"/candidate-evidence/{candidate_evidence_id}/relink"
"/candidate-evidence/{candidate_evidence_id}/relink",
tags=["review"],
)
def relink_candidate_evidence(
repository_id: int,
@@ -760,7 +800,8 @@ def relink_candidate_evidence(
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
"/candidate-abilities/{source_ability_id}/merge"
"/candidate-abilities/{source_ability_id}/merge",
tags=["review"],
)
def merge_candidate_ability(
repository_id: int,
@@ -784,7 +825,8 @@ def merge_candidate_ability(
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
"/candidate-capabilities/{source_capability_id}/merge"
"/candidate-capabilities/{source_capability_id}/merge",
tags=["review"],
)
def merge_candidate_capability(
repository_id: int,
@@ -808,7 +850,8 @@ def merge_candidate_capability(
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
"/candidate-features/{source_feature_id}/merge"
"/candidate-features/{source_feature_id}/merge",
tags=["review"],
)
def merge_candidate_feature(
repository_id: int,
@@ -832,7 +875,8 @@ def merge_candidate_feature(
@app.post(
"/repos/{repository_id}/analysis-runs/{analysis_run_id}"
"/candidate-evidence/{source_evidence_id}/merge"
"/candidate-evidence/{source_evidence_id}/merge",
tags=["review"],
)
def merge_candidate_evidence(
repository_id: int,
@@ -854,7 +898,7 @@ def merge_candidate_evidence(
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.post("/repos/{repository_id}/abilities", status_code=201)
@app.post("/repos/{repository_id}/abilities", status_code=201, tags=["registry"])
def create_ability(
repository_id: int,
payload: AbilityCreate,
@@ -867,7 +911,7 @@ def create_ability(
return {"id": ability_id}
@app.patch("/repos/{repository_id}/abilities/{ability_id}")
@app.patch("/repos/{repository_id}/abilities/{ability_id}", tags=["registry"])
def update_ability(
repository_id: int,
ability_id: int,
@@ -886,7 +930,7 @@ def update_ability(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.delete("/repos/{repository_id}/abilities/{ability_id}")
@app.delete("/repos/{repository_id}/abilities/{ability_id}", tags=["registry"])
def delete_ability(
repository_id: int,
ability_id: int,
@@ -898,7 +942,7 @@ def delete_ability(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post("/repos/{repository_id}/capabilities", status_code=201)
@app.post("/repos/{repository_id}/capabilities", status_code=201, tags=["registry"])
def create_capability(
repository_id: int,
payload: CapabilityCreate,
@@ -911,7 +955,7 @@ def create_capability(
return {"id": capability_id}
@app.patch("/repos/{repository_id}/capabilities/{capability_id}")
@app.patch("/repos/{repository_id}/capabilities/{capability_id}", tags=["registry"])
def update_capability(
repository_id: int,
capability_id: int,
@@ -930,7 +974,7 @@ def update_capability(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.delete("/repos/{repository_id}/capabilities/{capability_id}")
@app.delete("/repos/{repository_id}/capabilities/{capability_id}", tags=["registry"])
def delete_capability(
repository_id: int,
capability_id: int,
@@ -942,7 +986,7 @@ def delete_capability(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post("/repos/{repository_id}/features", status_code=201)
@app.post("/repos/{repository_id}/features", status_code=201, tags=["registry"])
def create_feature(
repository_id: int,
payload: FeatureCreate,
@@ -955,7 +999,7 @@ def create_feature(
return {"id": feature_id}
@app.patch("/repos/{repository_id}/features/{feature_id}")
@app.patch("/repos/{repository_id}/features/{feature_id}", tags=["registry"])
def update_feature(
repository_id: int,
feature_id: int,
@@ -974,7 +1018,7 @@ def update_feature(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.delete("/repos/{repository_id}/features/{feature_id}")
@app.delete("/repos/{repository_id}/features/{feature_id}", tags=["registry"])
def delete_feature(
repository_id: int,
feature_id: int,
@@ -986,7 +1030,7 @@ def delete_feature(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post("/repos/{repository_id}/evidence", status_code=201)
@app.post("/repos/{repository_id}/evidence", status_code=201, tags=["registry"])
def create_evidence(
repository_id: int,
payload: EvidenceCreate,
@@ -999,7 +1043,7 @@ def create_evidence(
return {"id": evidence_id}
@app.patch("/repos/{repository_id}/evidence/{evidence_id}")
@app.patch("/repos/{repository_id}/evidence/{evidence_id}", tags=["registry"])
def update_evidence(
repository_id: int,
evidence_id: int,
@@ -1018,7 +1062,7 @@ def update_evidence(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.delete("/repos/{repository_id}/evidence/{evidence_id}")
@app.delete("/repos/{repository_id}/evidence/{evidence_id}", tags=["registry"])
def delete_evidence(
repository_id: int,
evidence_id: int,
@@ -1030,7 +1074,7 @@ def delete_evidence(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.get("/repos/{repository_id}/ability-map")
@app.get("/repos/{repository_id}/ability-map", tags=["registry"])
def get_ability_map(
repository_id: int,
service: RegistryService = Depends(get_service),
@@ -1041,14 +1085,33 @@ def get_ability_map(
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.get("/search")
@app.get("/search", tags=["search"])
def search(
q: str,
status: str | None = None,
language: str | None = None,
framework: str | None = None,
ability: str | None = None,
capability: str | None = None,
q: str = Query(
...,
description="Natural-language or keyword query over approved registry entries.",
examples=["classify email", "FastAPI health endpoint"],
),
status: str | None = Query(
default=None,
description="Filter repositories by registry status, such as indexed.",
),
language: str | None = Query(
default=None,
description="Filter by observed programming language.",
),
framework: str | None = Query(
default=None,
description="Filter by observed framework hint.",
),
ability: str | None = Query(
default=None,
description="Filter to results under an approved ability name.",
),
capability: str | None = Query(
default=None,
description="Filter to results under an approved capability name.",
),
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:
return [
@@ -1064,14 +1127,14 @@ def search(
]
@app.get("/abilities")
@app.get("/abilities", tags=["search"])
def list_abilities(
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:
return [asdict(ability) for ability in service.list_abilities()]
@app.get("/capabilities")
@app.get("/capabilities", tags=["search"])
def list_capabilities(
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:

View File

@@ -206,7 +206,7 @@ def search_page(
{render_search_context(asdict(result))}
</td>
<td>{escape(result.matched_field)}</td>
<td>{result.confidence:.2f}</td>
<td>{result.confidence:.2f} <span class="pill">{escape(result.confidence_label)}</span></td>
</tr>
"""
for result in results
@@ -1018,7 +1018,7 @@ def render_candidate_graph(graph: dict, repository_id: int, analysis_run_id: int
<strong>{escape(ability['name'])}</strong>
<span class="pill">ID {ability['id']}</span>
<span class="pill">{escape(ability['status'])}</span>
<span class="pill">{ability['confidence']:.2f}</span>
<span class="pill">{ability['confidence']:.2f} {escape(ability['confidence_label'])}</span>
{render_candidate_ability_actions(ability, repository_id, analysis_run_id)}
<p class="muted">{escape(ability['description'])}</p>
{render_candidate_edit_form('candidate-abilities', ability, repository_id, analysis_run_id)}
@@ -1150,7 +1150,7 @@ def render_candidate_capability(
<strong>{escape(capability['name'])}</strong>
<span class="pill">ID {capability['id']}</span>
<span class="pill">{escape(capability['status'])}</span>
<span class="pill">{capability['confidence']:.2f}</span>
<span class="pill">{capability['confidence']:.2f} {escape(capability['confidence_label'])}</span>
{render_candidate_reject_form('candidate-capabilities', capability, repository_id, analysis_run_id)}
<p class="muted">{escape(capability['description'])}</p>
{render_candidate_edit_form('candidate-capabilities', capability, repository_id, analysis_run_id)}
@@ -1287,6 +1287,7 @@ def render_ability_map(ability_map: dict, repository_id: int) -> str:
<li id="capability-{capability['id']}">
<strong>{escape(capability['name'])}</strong>
<span class="pill">ID {capability['id']}</span>
<span class="pill">{capability['confidence']:.2f} {escape(capability['confidence_label'])}</span>
<p class="muted">{escape(capability['description'])}</p>
{render_approved_capability_forms(capability, repository_id)}
<ul>{features}{evidence}</ul>
@@ -1298,6 +1299,7 @@ def render_ability_map(ability_map: dict, repository_id: int) -> str:
<li id="ability-{ability['id']}">
<strong>{escape(ability['name'])}</strong>
<span class="pill">ID {ability['id']}</span>
<span class="pill">{ability['confidence']:.2f} {escape(ability['confidence_label'])}</span>
<p class="muted">{escape(ability['description'])}</p>
{render_approved_ability_forms(ability, repository_id)}
<ul>{''.join(capabilities)}</ul>
@@ -1348,6 +1350,7 @@ def render_approved_feature(feature: dict, repository_id: int) -> str:
<li>
{escape(feature["name"])}
<span class="pill">{escape(feature["type"])}</span>
<span class="pill">{feature["confidence"]:.2f} {escape(feature["confidence_label"])}</span>
<span class="source">{escape(feature["location"])}</span>
{render_sources(feature.get("source_refs", []))}
<form class="stack" method="post" action="/ui/repos/{repository_id}/features/{feature['id']}/edit">

View File

@@ -4,6 +4,28 @@ from repo_registry.web_api import app as app_module
from repo_registry.web_api.app import Settings, app, get_service, get_settings
def test_openapi_groups_agent_facing_endpoints():
client = TestClient(app)
response = client.get("/openapi.json")
assert response.status_code == 200
schema = response.json()
assert {tag["name"] for tag in schema["tags"]} >= {
"repositories",
"analysis",
"review",
"registry",
"search",
}
search_operation = schema["paths"]["/search"]["get"]
assert search_operation["tags"] == ["search"]
assert {
parameter["name"]: parameter["description"]
for parameter in search_operation["parameters"]
}["q"].startswith("Natural-language")
def test_api_manual_registry_loop(tmp_path):
def override_settings():
return Settings(
@@ -323,6 +345,11 @@ def test_api_analysis_run_loop(tmp_path):
ability_map = approve_response.json()
assert ability_map["repository"]["status"] == "indexed"
assert ability_map["abilities"][0]["name"] == "Frontend Delivery"
assert ability_map["abilities"][0]["confidence_label"] in {
"low",
"medium",
"high",
}
assert ability_map["abilities"][0]["capabilities"][0]["name"] == (
"Describe Frontend Stack"
)
@@ -331,6 +358,7 @@ def test_api_analysis_run_loop(tmp_path):
assert search_response.status_code == 200
assert search_response.json()
assert "matched_field" in search_response.json()[0]
assert "confidence_label" in search_response.json()[0]
filtered_search_response = client.get(
"/search",
@@ -348,6 +376,11 @@ def test_api_analysis_run_loop(tmp_path):
assert abilities_response.status_code == 200
assert abilities_response.json()[0]["name"] == "Frontend Delivery"
assert abilities_response.json()[0]["repository_name"] == "Frontend"
assert abilities_response.json()[0]["confidence_label"] in {
"low",
"medium",
"high",
}
capabilities_response = client.get("/capabilities")
assert capabilities_response.status_code == 200