diff --git a/README.md b/README.md index 9f65347..f2431cd 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/src/repo_registry/core/models.py b/src/repo_registry/core/models.py index 7143b5f..1037a0d 100644 --- a/src/repo_registry/core/models.py +++ b/src/repo_registry/core/models.py @@ -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 = "" diff --git a/src/repo_registry/storage/sqlite.py b/src/repo_registry/storage/sqlite.py index dbe041a..51eae19 100644 --- a/src/repo_registry/storage/sqlite.py +++ b/src/repo_registry/storage/sqlite.py @@ -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"], diff --git a/src/repo_registry/web_api/app.py b/src/repo_registry/web_api/app.py index 695fe83..b46ff46 100644 --- a/src/repo_registry/web_api/app.py +++ b/src/repo_registry/web_api/app.py @@ -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]]: diff --git a/src/repo_registry/web_ui/views.py b/src/repo_registry/web_ui/views.py index 770a83c..f4788eb 100644 --- a/src/repo_registry/web_ui/views.py +++ b/src/repo_registry/web_ui/views.py @@ -206,7 +206,7 @@ def search_page( {render_search_context(asdict(result))}
{escape(ability['description'])}
{render_candidate_edit_form('candidate-abilities', ability, repository_id, analysis_run_id)} @@ -1150,7 +1150,7 @@ def render_candidate_capability( {escape(capability['name'])} ID {capability['id']} {escape(capability['status'])} - {capability['confidence']:.2f} + {capability['confidence']:.2f} {escape(capability['confidence_label'])} {render_candidate_reject_form('candidate-capabilities', capability, repository_id, analysis_run_id)}{escape(capability['description'])}
{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:{escape(capability['description'])}
{render_approved_capability_forms(capability, repository_id)}{escape(ability['description'])}
{render_approved_ability_forms(ability, repository_id)}