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:
|
def ability_map(self, repository_id: int) -> RepositoryAbilityMap:
|
||||||
return self.store.get_ability_map(repository_id)
|
return self.store.get_ability_map(repository_id)
|
||||||
|
|
||||||
def search(self, query: str) -> list[SearchResult]:
|
def search(
|
||||||
return self.store.search(query)
|
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)
|
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()
|
term = query.strip()
|
||||||
needle = f"%{term}%"
|
needle = f"%{term}%"
|
||||||
if not term:
|
if not term:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
with self.connect() as connection:
|
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(
|
repository_rows = connection.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT r.id AS repository_id, r.name AS repository_name,
|
SELECT r.id AS repository_id, r.name AS repository_name,
|
||||||
r.description
|
r.description
|
||||||
FROM repositories r
|
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()
|
).fetchall()
|
||||||
ability_rows = connection.execute(
|
ability_rows = connection.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT r.id AS repository_id, r.name AS repository_name,
|
SELECT r.id AS repository_id, r.name AS repository_name,
|
||||||
a.id AS ability_id, a.name AS ability_name,
|
a.id AS ability_id, a.name AS ability_name,
|
||||||
a.description AS ability_description, a.confidence
|
a.description AS ability_description, a.confidence
|
||||||
FROM approved_abilities a
|
FROM approved_abilities a
|
||||||
JOIN repositories r ON r.id = a.repository_id
|
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()
|
).fetchall()
|
||||||
capability_rows = connection.execute(
|
capability_rows = connection.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT r.id AS repository_id, r.name AS repository_name,
|
SELECT r.id AS repository_id, r.name AS repository_name,
|
||||||
a.id AS ability_id, a.name AS ability_name,
|
a.id AS ability_id, a.name AS ability_name,
|
||||||
c.id AS capability_id, c.name AS capability_name,
|
c.id AS capability_id, c.name AS capability_name,
|
||||||
@@ -1311,12 +1331,13 @@ class RegistryStore:
|
|||||||
FROM approved_capabilities c
|
FROM approved_capabilities c
|
||||||
JOIN approved_abilities a ON a.id = c.ability_id
|
JOIN approved_abilities a ON a.id = c.ability_id
|
||||||
JOIN repositories r ON r.id = c.repository_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()
|
).fetchall()
|
||||||
feature_rows = connection.execute(
|
feature_rows = connection.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT r.id AS repository_id, r.name AS repository_name,
|
SELECT r.id AS repository_id, r.name AS repository_name,
|
||||||
a.id AS ability_id, a.name AS ability_name,
|
a.id AS ability_id, a.name AS ability_name,
|
||||||
c.id AS capability_id, c.name AS capability_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_capabilities c ON c.id = f.capability_id
|
||||||
JOIN approved_abilities a ON a.id = c.ability_id
|
JOIN approved_abilities a ON a.id = c.ability_id
|
||||||
JOIN repositories r ON r.id = f.repository_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()
|
).fetchall()
|
||||||
evidence_rows = connection.execute(
|
evidence_rows = connection.execute(
|
||||||
"""
|
f"""
|
||||||
SELECT r.id AS repository_id, r.name AS repository_name,
|
SELECT r.id AS repository_id, r.name AS repository_name,
|
||||||
a.id AS ability_id, a.name AS ability_name,
|
a.id AS ability_id, a.name AS ability_name,
|
||||||
c.id AS capability_id, c.name AS capability_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_capabilities c ON c.id = e.capability_id
|
||||||
JOIN approved_abilities a ON a.id = c.ability_id
|
JOIN approved_abilities a ON a.id = c.ability_id
|
||||||
JOIN repositories r ON r.id = e.repository_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()
|
).fetchall()
|
||||||
|
|
||||||
results: list[SearchResult] = []
|
results: list[SearchResult] = []
|
||||||
@@ -1472,6 +1495,55 @@ class RegistryStore:
|
|||||||
def _evidence_confidence(self, strength: str) -> float:
|
def _evidence_confidence(self, strength: str) -> float:
|
||||||
return {"strong": 0.9, "medium": 0.6, "weak": 0.3}.get(strength, 0.5)
|
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(
|
def _insert_facts(
|
||||||
self,
|
self,
|
||||||
connection: sqlite3.Connection,
|
connection: sqlite3.Connection,
|
||||||
|
|||||||
@@ -768,9 +768,20 @@ def get_ability_map(
|
|||||||
@app.get("/search")
|
@app.get("/search")
|
||||||
def search(
|
def search(
|
||||||
q: str,
|
q: str,
|
||||||
|
status: str | None = None,
|
||||||
|
language: str | None = None,
|
||||||
|
framework: str | None = None,
|
||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> list[dict[str, object]]:
|
) -> 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")
|
@app.get("/abilities")
|
||||||
|
|||||||
@@ -176,9 +176,21 @@ def repository_index(service: RegistryService = Depends(get_service)) -> HTMLRes
|
|||||||
@router.get("/ui/search")
|
@router.get("/ui/search")
|
||||||
def search_page(
|
def search_page(
|
||||||
q: str = "",
|
q: str = "",
|
||||||
|
status: str = "",
|
||||||
|
language: str = "",
|
||||||
|
framework: str = "",
|
||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> HTMLResponse:
|
) -> 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(
|
rows = "\n".join(
|
||||||
f"""
|
f"""
|
||||||
<tr>
|
<tr>
|
||||||
@@ -205,9 +217,17 @@ def search_page(
|
|||||||
<a class="button secondary" href="/ui">Repositories</a>
|
<a class="button secondary" href="/ui">Repositories</a>
|
||||||
</div>
|
</div>
|
||||||
<section class="panel">
|
<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">
|
<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>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
<section class="panel" style="margin-top:18px">
|
<section class="panel" style="margin-top:18px">
|
||||||
@@ -241,6 +261,9 @@ def repository_detail(
|
|||||||
repository = service.get_repository(repository_id)
|
repository = service.get_repository(repository_id)
|
||||||
runs = service.list_analysis_runs(repository_id)
|
runs = service.list_analysis_runs(repository_id)
|
||||||
ability_map = service.ability_map(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(
|
run_rows = "\n".join(
|
||||||
f"""
|
f"""
|
||||||
<tr>
|
<tr>
|
||||||
@@ -259,6 +282,7 @@ def repository_detail(
|
|||||||
</div>
|
</div>
|
||||||
<p class="muted">{escape(repository.description or '')}</p>
|
<p class="muted">{escape(repository.description or '')}</p>
|
||||||
<p><span class="pill">{escape(repository.status)}</span> <span class="source">{escape(repository.url)}</span></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">
|
<div class="grid">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Run Analysis</h2>
|
<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>'
|
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(
|
def render_candidate_ability_actions(
|
||||||
ability: dict,
|
ability: dict,
|
||||||
repository_id: int,
|
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
|
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):
|
def test_register_repository_imports_metadata_when_name_is_omitted(tmp_path):
|
||||||
source = tmp_path / "metadata-source"
|
source = tmp_path / "metadata-source"
|
||||||
source.mkdir()
|
source.mkdir()
|
||||||
|
|||||||
@@ -213,6 +213,13 @@ def test_api_analysis_run_loop(tmp_path):
|
|||||||
assert search_response.json()
|
assert search_response.json()
|
||||||
assert "matched_field" in search_response.json()[0]
|
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")
|
abilities_response = client.get("/abilities")
|
||||||
assert abilities_response.status_code == 200
|
assert abilities_response.status_code == 200
|
||||||
assert abilities_response.json()[0]["name"] == "Frontend Delivery"
|
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 = tmp_path / "repo"
|
||||||
source.mkdir()
|
source.mkdir()
|
||||||
(source / "README.md").write_text("# UI Repo\n", encoding="utf-8")
|
(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(
|
(source / "app.py").write_text(
|
||||||
"from fastapi import FastAPI\n"
|
"from fastapi import FastAPI\n"
|
||||||
"app = 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_detail.status_code == 200
|
||||||
assert "Approved Ability Map" in approved_detail.text
|
assert "Approved Ability Map" in approved_detail.text
|
||||||
assert "Review UI Repo Repository Usefulness" 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"})
|
search_response = client.get("/ui/search", params={"q": "repository"})
|
||||||
assert search_response.status_code == 200
|
assert search_response.status_code == 200
|
||||||
assert "UI Repo" in search_response.text
|
assert "UI Repo" in search_response.text
|
||||||
assert "Field" 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:
|
finally:
|
||||||
app.dependency_overrides.clear()
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user