search filters and inspection polish

This commit is contained in:
2026-04-26 00:04:19 +02:00
parent b8627c0e1d
commit 8d1e1ff583
6 changed files with 209 additions and 22 deletions

View File

@@ -580,5 +580,17 @@ class RegistryService:
def ability_map(self, repository_id: int) -> RepositoryAbilityMap:
return self.store.get_ability_map(repository_id)
def search(self, query: str) -> list[SearchResult]:
return self.store.search(query)
def search(
self,
query: str,
*,
status: str | None = None,
language: str | None = None,
framework: str | None = None,
) -> list[SearchResult]:
return self.store.search(
query,
status=status,
language=language,
framework=framework,
)

View File

@@ -1275,35 +1275,55 @@ class RegistryStore:
]
return RepositoryAbilityMap(repository=repository, abilities=abilities)
def search(self, query: str) -> list[SearchResult]:
def search(
self,
query: str,
*,
status: str | None = None,
language: str | None = None,
framework: str | None = None,
) -> list[SearchResult]:
term = query.strip()
needle = f"%{term}%"
if not term:
return []
with self.connect() as connection:
repository_ids = self._search_filter_repository_ids(
connection,
status=status,
language=language,
framework=framework,
)
if repository_ids is not None and not repository_ids:
return []
repository_filter, repository_params = self._repository_filter_sql(
repository_ids,
)
repository_rows = connection.execute(
"""
f"""
SELECT r.id AS repository_id, r.name AS repository_name,
r.description
FROM repositories r
WHERE r.name LIKE ? OR COALESCE(r.description, '') LIKE ?
WHERE (r.name LIKE ? OR COALESCE(r.description, '') LIKE ?)
{repository_filter}
""",
(needle, needle),
(needle, needle, *repository_params),
).fetchall()
ability_rows = connection.execute(
"""
f"""
SELECT r.id AS repository_id, r.name AS repository_name,
a.id AS ability_id, a.name AS ability_name,
a.description AS ability_description, a.confidence
FROM approved_abilities a
JOIN repositories r ON r.id = a.repository_id
WHERE a.name LIKE ? OR a.description LIKE ?
WHERE (a.name LIKE ? OR a.description LIKE ?)
{repository_filter}
""",
(needle, needle),
(needle, needle, *repository_params),
).fetchall()
capability_rows = connection.execute(
"""
f"""
SELECT r.id AS repository_id, r.name AS repository_name,
a.id AS ability_id, a.name AS ability_name,
c.id AS capability_id, c.name AS capability_name,
@@ -1311,12 +1331,13 @@ class RegistryStore:
FROM approved_capabilities c
JOIN approved_abilities a ON a.id = c.ability_id
JOIN repositories r ON r.id = c.repository_id
WHERE c.name LIKE ? OR c.description LIKE ?
WHERE (c.name LIKE ? OR c.description LIKE ?)
{repository_filter}
""",
(needle, needle),
(needle, needle, *repository_params),
).fetchall()
feature_rows = connection.execute(
"""
f"""
SELECT r.id AS repository_id, r.name AS repository_name,
a.id AS ability_id, a.name AS ability_name,
c.id AS capability_id, c.name AS capability_name,
@@ -1326,12 +1347,13 @@ class RegistryStore:
JOIN approved_capabilities c ON c.id = f.capability_id
JOIN approved_abilities a ON a.id = c.ability_id
JOIN repositories r ON r.id = f.repository_id
WHERE f.name LIKE ? OR f.type LIKE ? OR f.location LIKE ?
WHERE (f.name LIKE ? OR f.type LIKE ? OR f.location LIKE ?)
{repository_filter}
""",
(needle, needle, needle),
(needle, needle, needle, *repository_params),
).fetchall()
evidence_rows = connection.execute(
"""
f"""
SELECT r.id AS repository_id, r.name AS repository_name,
a.id AS ability_id, a.name AS ability_name,
c.id AS capability_id, c.name AS capability_name,
@@ -1340,9 +1362,10 @@ class RegistryStore:
JOIN approved_capabilities c ON c.id = e.capability_id
JOIN approved_abilities a ON a.id = c.ability_id
JOIN repositories r ON r.id = e.repository_id
WHERE e.type LIKE ? OR e.reference LIKE ? OR e.strength LIKE ?
WHERE (e.type LIKE ? OR e.reference LIKE ? OR e.strength LIKE ?)
{repository_filter}
""",
(needle, needle, needle),
(needle, needle, needle, *repository_params),
).fetchall()
results: list[SearchResult] = []
@@ -1472,6 +1495,55 @@ class RegistryStore:
def _evidence_confidence(self, strength: str) -> float:
return {"strong": 0.9, "medium": 0.6, "weak": 0.3}.get(strength, 0.5)
def _search_filter_repository_ids(
self,
connection: sqlite3.Connection,
*,
status: str | None,
language: str | None,
framework: str | None,
) -> list[int] | None:
filters: list[set[int]] = []
if status:
rows = connection.execute(
"SELECT id FROM repositories WHERE status = ?",
(status,),
).fetchall()
filters.append({row["id"] for row in rows})
if language:
rows = connection.execute(
"""
SELECT DISTINCT repository_id
FROM observed_facts
WHERE kind = 'language' AND name LIKE ?
""",
(language,),
).fetchall()
filters.append({row["repository_id"] for row in rows})
if framework:
rows = connection.execute(
"""
SELECT DISTINCT repository_id
FROM observed_facts
WHERE kind = 'framework' AND name LIKE ?
""",
(framework,),
).fetchall()
filters.append({row["repository_id"] for row in rows})
if not filters:
return None
repository_ids = set.intersection(*filters)
return sorted(repository_ids)
def _repository_filter_sql(
self,
repository_ids: list[int] | None,
) -> tuple[str, tuple[int, ...]]:
if repository_ids is None:
return "", ()
placeholders = ", ".join("?" for _ in repository_ids)
return f"AND r.id IN ({placeholders})", tuple(repository_ids)
def _insert_facts(
self,
connection: sqlite3.Connection,

View File

@@ -768,9 +768,20 @@ def get_ability_map(
@app.get("/search")
def search(
q: str,
status: str | None = None,
language: str | None = None,
framework: str | None = None,
service: RegistryService = Depends(get_service),
) -> list[dict[str, object]]:
return [asdict(result) for result in service.search(q)]
return [
asdict(result)
for result in service.search(
q,
status=status,
language=language,
framework=framework,
)
]
@app.get("/abilities")

View File

@@ -176,9 +176,21 @@ def repository_index(service: RegistryService = Depends(get_service)) -> HTMLRes
@router.get("/ui/search")
def search_page(
q: str = "",
status: str = "",
language: str = "",
framework: str = "",
service: RegistryService = Depends(get_service),
) -> HTMLResponse:
results = service.search(q) if q.strip() else []
results = (
service.search(
q,
status=status or None,
language=language or None,
framework=framework or None,
)
if q.strip()
else []
)
rows = "\n".join(
f"""
<tr>
@@ -205,9 +217,17 @@ def search_page(
<a class="button secondary" href="/ui">Repositories</a>
</div>
<section class="panel">
<form class="actions" method="get" action="/ui/search">
<form class="stack" method="get" action="/ui/search">
<input name="q" value="{escape(q)}" placeholder="Search approved registry entries">
<button type="submit">Search</button>
<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>
</div>
<div class="actions">
<button type="submit">Search</button>
<a class="button secondary" href="/ui/search">Clear</a>
</div>
</form>
</section>
<section class="panel" style="margin-top:18px">
@@ -241,6 +261,9 @@ def repository_detail(
repository = service.get_repository(repository_id)
runs = service.list_analysis_runs(repository_id)
ability_map = service.ability_map(repository_id)
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"})
run_rows = "\n".join(
f"""
<tr>
@@ -259,6 +282,7 @@ def repository_detail(
</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>
{render_repository_facts(languages, frameworks)}
<div class="grid">
<section class="panel">
<h2>Run Analysis</h2>
@@ -699,6 +723,20 @@ def render_candidate_graph(graph: dict, repository_id: int, analysis_run_id: int
return f'<div class="tree"><ul>{"".join(items)}</ul></div>'
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>'
def render_candidate_ability_actions(
ability: dict,
repository_id: int,

View File

@@ -137,6 +137,43 @@ def test_search_matches_features_and_evidence_with_context(tmp_path):
assert evidence_results[0].confidence == 0.9
def test_search_filters_by_status_language_and_framework(tmp_path):
source = tmp_path / "repo"
source.mkdir()
(source / "README.md").write_text("# Filterable\n", encoding="utf-8")
(source / "requirements.txt").write_text("fastapi\n", encoding="utf-8")
(source / "app.py").write_text(
"from fastapi import FastAPI\n"
"app = FastAPI()\n"
'@app.get("/health")\n'
"def health():\n"
" return {}\n",
encoding="utf-8",
)
service = make_service(tmp_path)
repository = service.register_repository(name="Filterable", url=str(source))
summary = service.analyze_repository(repository.id)
service.approve_candidate_graph(repository.id, summary.analysis_run.id)
results = service.search(
"repository",
status="indexed",
language="Python",
framework="FastAPI",
)
wrong_language_results = service.search(
"repository",
status="indexed",
language="TypeScript",
framework="FastAPI",
)
assert results
assert {result.repository_name for result in results} == {"Filterable"}
assert wrong_language_results == []
def test_register_repository_imports_metadata_when_name_is_omitted(tmp_path):
source = tmp_path / "metadata-source"
source.mkdir()

View File

@@ -213,6 +213,13 @@ def test_api_analysis_run_loop(tmp_path):
assert search_response.json()
assert "matched_field" in search_response.json()[0]
filtered_search_response = client.get(
"/search",
params={"q": "frontend", "status": "indexed"},
)
assert filtered_search_response.status_code == 200
assert filtered_search_response.json()
abilities_response = client.get("/abilities")
assert abilities_response.status_code == 200
assert abilities_response.json()[0]["name"] == "Frontend Delivery"
@@ -240,6 +247,7 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
source = tmp_path / "repo"
source.mkdir()
(source / "README.md").write_text("# UI Repo\n", encoding="utf-8")
(source / "requirements.txt").write_text("fastapi\n", encoding="utf-8")
(source / "app.py").write_text(
"from fastapi import FastAPI\n"
"app = FastAPI()\n"
@@ -300,11 +308,20 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
assert approved_detail.status_code == 200
assert "Approved Ability Map" in approved_detail.text
assert "Review UI Repo Repository Usefulness" in approved_detail.text
assert "Language: Python" in approved_detail.text
assert "Framework: FastAPI" in approved_detail.text
search_response = client.get("/ui/search", params={"q": "repository"})
assert search_response.status_code == 200
assert "UI Repo" in search_response.text
assert "Field" in search_response.text
filtered_search_response = client.get(
"/ui/search",
params={"q": "repository", "status": "indexed", "language": "Python"},
)
assert filtered_search_response.status_code == 200
assert "UI Repo" in filtered_search_response.text
finally:
app.dependency_overrides.clear()