generated from coulomb/repo-seed
search filters and inspection polish
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user