diff --git a/src/repo_registry/core/service.py b/src/repo_registry/core/service.py index 6bbe187..32428ac 100644 --- a/src/repo_registry/core/service.py +++ b/src/repo_registry/core/service.py @@ -587,10 +587,14 @@ class RegistryService: status: str | None = None, language: str | None = None, framework: str | None = None, + ability: str | None = None, + capability: str | None = None, ) -> list[SearchResult]: return self.store.search( query, status=status, language=language, framework=framework, + ability=ability, + capability=capability, ) diff --git a/src/repo_registry/storage/sqlite.py b/src/repo_registry/storage/sqlite.py index a75b88e..1edadfa 100644 --- a/src/repo_registry/storage/sqlite.py +++ b/src/repo_registry/storage/sqlite.py @@ -1282,6 +1282,8 @@ class RegistryStore: status: str | None = None, language: str | None = None, framework: str | None = None, + ability: str | None = None, + capability: str | None = None, ) -> list[SearchResult]: term = query.strip() needle = f"%{term}%" @@ -1294,6 +1296,8 @@ class RegistryStore: status=status, language=language, framework=framework, + ability=ability, + capability=capability, ) if repository_ids is not None and not repository_ids: return [] @@ -1502,6 +1506,8 @@ class RegistryStore: status: str | None, language: str | None, framework: str | None, + ability: str | None, + capability: str | None, ) -> list[int] | None: filters: list[set[int]] = [] if status: @@ -1530,6 +1536,26 @@ class RegistryStore: (framework,), ).fetchall() filters.append({row["repository_id"] for row in rows}) + if ability: + rows = connection.execute( + """ + SELECT DISTINCT repository_id + FROM approved_abilities + WHERE name LIKE ? OR description LIKE ? + """, + (f"%{ability}%", f"%{ability}%"), + ).fetchall() + filters.append({row["repository_id"] for row in rows}) + if capability: + rows = connection.execute( + """ + SELECT DISTINCT repository_id + FROM approved_capabilities + WHERE name LIKE ? OR description LIKE ? + """, + (f"%{capability}%", f"%{capability}%"), + ).fetchall() + filters.append({row["repository_id"] for row in rows}) if not filters: return None repository_ids = set.intersection(*filters) diff --git a/src/repo_registry/web_api/app.py b/src/repo_registry/web_api/app.py index 6dcc34a..57383e6 100644 --- a/src/repo_registry/web_api/app.py +++ b/src/repo_registry/web_api/app.py @@ -771,6 +771,8 @@ def search( status: str | None = None, language: str | None = None, framework: str | None = None, + ability: str | None = None, + capability: str | None = None, service: RegistryService = Depends(get_service), ) -> list[dict[str, object]]: return [ @@ -780,6 +782,8 @@ def search( status=status, language=language, framework=framework, + ability=ability, + capability=capability, ) ] diff --git a/src/repo_registry/web_ui/views.py b/src/repo_registry/web_ui/views.py index 4b619d6..cd67515 100644 --- a/src/repo_registry/web_ui/views.py +++ b/src/repo_registry/web_ui/views.py @@ -179,6 +179,8 @@ def search_page( status: str = "", language: str = "", framework: str = "", + ability: str = "", + capability: str = "", service: RegistryService = Depends(get_service), ) -> HTMLResponse: results = ( @@ -187,6 +189,8 @@ def search_page( status=status or None, language=language or None, framework=framework or None, + ability=ability or None, + capability=capability or None, ) if q.strip() else [] @@ -194,7 +198,7 @@ def search_page( rows = "\n".join( f""" - {escape(result.repository_name)} + {escape(result.repository_name)} {escape(result.match_type)} {escape(result.match_name)} @@ -223,6 +227,8 @@ def search_page( + +
@@ -930,7 +936,7 @@ def render_ability_map(ability_map: dict) -> str: ) capabilities.append( f""" -
  • +
  • {escape(capability['name'])}

    {escape(capability['description'])}

    @@ -939,7 +945,7 @@ def render_ability_map(ability_map: dict) -> str: ) items.append( f""" -
  • +
  • {escape(ability['name'])}

    {escape(ability['description'])}

    @@ -949,6 +955,15 @@ def render_ability_map(ability_map: dict) -> str: return f'
      {"".join(items)}
    ' +def search_result_href(result: dict) -> str: + href = f"/ui/repos/{result['repository_id']}" + if result.get("capability_id"): + return f"{href}#capability-{result['capability_id']}" + if result.get("ability_id"): + return f"{href}#ability-{result['ability_id']}" + return href + + def render_sources(source_refs: list[dict]) -> str: if not source_refs: return "" diff --git a/tests/test_registry_service.py b/tests/test_registry_service.py index 3a6cf37..c20cd30 100644 --- a/tests/test_registry_service.py +++ b/tests/test_registry_service.py @@ -161,6 +161,8 @@ def test_search_filters_by_status_language_and_framework(tmp_path): status="indexed", language="Python", framework="FastAPI", + ability="Repository Usefulness", + capability="Repository Structure", ) wrong_language_results = service.search( "repository", @@ -168,10 +170,18 @@ def test_search_filters_by_status_language_and_framework(tmp_path): language="TypeScript", framework="FastAPI", ) + wrong_capability_results = service.search( + "repository", + status="indexed", + language="Python", + framework="FastAPI", + capability="Email Routing", + ) assert results assert {result.repository_name for result in results} == {"Filterable"} assert wrong_language_results == [] + assert wrong_capability_results == [] def test_register_repository_imports_metadata_when_name_is_omitted(tmp_path): diff --git a/tests/test_web_api.py b/tests/test_web_api.py index efb3f5c..3b369fa 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -215,7 +215,12 @@ def test_api_analysis_run_loop(tmp_path): filtered_search_response = client.get( "/search", - params={"q": "frontend", "status": "indexed"}, + params={ + "q": "frontend", + "status": "indexed", + "ability": "Frontend", + "capability": "Frontend Stack", + }, ) assert filtered_search_response.status_code == 200 assert filtered_search_response.json() @@ -315,10 +320,17 @@ def test_ui_register_analyze_and_approve_loop(tmp_path): assert search_response.status_code == 200 assert "UI Repo" in search_response.text assert "Field" in search_response.text + assert "#capability-" in search_response.text or "#ability-" in search_response.text filtered_search_response = client.get( "/ui/search", - params={"q": "repository", "status": "indexed", "language": "Python"}, + params={ + "q": "repository", + "status": "indexed", + "language": "Python", + "ability": "Repository Usefulness", + "capability": "Repository", + }, ) assert filtered_search_response.status_code == 200 assert "UI Repo" in filtered_search_response.text