|
29 | 29 | from __future__ import annotations |
30 | 30 |
|
31 | 31 | import asyncio |
| 32 | +import dataclasses |
32 | 33 | import json |
33 | 34 | import sqlite3 |
34 | 35 | import threading |
@@ -681,14 +682,15 @@ def get_versions(self, name: str) -> List[SkillRecord]: |
681 | 682 |
|
682 | 683 | Delegates to :class:`LineageTracker` (Epic 3.3). |
683 | 684 | """ |
684 | | - # Fix 5: Hydrate recent_analyses after delegation |
| 685 | + # Fix: Fully hydrate records after delegation (tags, tool_deps, critical_tools, recent_analyses) |
685 | 686 | records = self._lineage.get_evolution_chain(name) |
686 | | - return [self._hydrate_recent_analyses(record) for record in records] |
| 687 | + return self._hydrate_records(records) |
687 | 688 |
|
688 | 689 | @_db_retry() |
689 | 690 | def load_by_category(self, category: SkillCategory, *, active_only: bool = True) -> List[SkillRecord]: |
690 | 691 | """Delegate to SkillRepository for category queries.""" |
691 | | - return self._repo.load_by_category(category, active_only=active_only) |
| 692 | + records = self._repo.load_by_category(category, active_only=active_only) |
| 693 | + return self._hydrate_records(records) |
692 | 694 |
|
693 | 695 | @_db_retry() |
694 | 696 | def load_analyses( |
@@ -826,9 +828,9 @@ def get_ancestry(self, skill_id: str, max_depth: int = 10) -> List[SkillRecord]: |
826 | 828 |
|
827 | 829 | Delegates to :class:`LineageTracker` (Epic 3.3). |
828 | 830 | """ |
829 | | - # Fix 5: Hydrate recent_analyses after delegation |
| 831 | + # Fix: Fully hydrate records after delegation (tags, tool_deps, critical_tools, recent_analyses) |
830 | 832 | records = self._lineage.get_ancestors(skill_id, max_depth=max_depth) |
831 | | - return [self._hydrate_recent_analyses(record) for record in records] |
| 833 | + return self._hydrate_records(records) |
832 | 834 |
|
833 | 835 | @_db_retry() |
834 | 836 | def get_lineage_tree(self, skill_id: str, max_depth: int = 5) -> Dict[str, Any]: |
@@ -992,6 +994,82 @@ def _hydrate_recent_analyses(self, record: SkillRecord) -> SkillRecord: |
992 | 994 | """Hydrate recent_analyses for a SkillRecord from AnalysisStore delegation.""" |
993 | 995 | return self._analyses.hydrate_recent_analyses(record) |
994 | 996 |
|
| 997 | + def _hydrate_record(self, record: SkillRecord) -> SkillRecord: |
| 998 | + """Fully hydrate a SkillRecord with tags, tool_deps, critical_tools, and recent_analyses. |
| 999 | + |
| 1000 | + Used by facade methods that delegate to modules returning partially hydrated records. |
| 1001 | + """ |
| 1002 | + with self._reader() as conn: |
| 1003 | + # Hydrate tool dependencies and critical tools |
| 1004 | + dep_rows = conn.execute( |
| 1005 | + "SELECT tool_key, critical FROM skill_tool_deps WHERE skill_id=?", |
| 1006 | + (record.skill_id,), |
| 1007 | + ).fetchall() |
| 1008 | + |
| 1009 | + tool_dependencies = [r["tool_key"] for r in dep_rows] |
| 1010 | + critical_tools = [r["tool_key"] for r in dep_rows if r["critical"]] |
| 1011 | + |
| 1012 | + # Hydrate tags using TagSearch |
| 1013 | + tags = self._tag_search.get_tags(record.skill_id) |
| 1014 | + |
| 1015 | + # Hydrate recent_analyses using AnalysisStore |
| 1016 | + hydrated_record = self._analyses.hydrate_recent_analyses(record) |
| 1017 | + |
| 1018 | + # Return a new record with all fields hydrated |
| 1019 | + return dataclasses.replace( |
| 1020 | + hydrated_record, |
| 1021 | + tags=tags, |
| 1022 | + tool_dependencies=tool_dependencies, |
| 1023 | + critical_tools=critical_tools, |
| 1024 | + ) |
| 1025 | + |
| 1026 | + def _hydrate_records(self, records: List[SkillRecord]) -> List[SkillRecord]: |
| 1027 | + """Batch hydrate multiple SkillRecords — O(1) queries instead of O(N). |
| 1028 | +
|
| 1029 | + Batches tag, tool_dep, and analysis queries across all records. |
| 1030 | + """ |
| 1031 | + if not records: |
| 1032 | + return records |
| 1033 | + |
| 1034 | + skill_ids = [r.skill_id for r in records] |
| 1035 | + placeholders = ",".join("?" * len(skill_ids)) |
| 1036 | + |
| 1037 | + with self._reader() as conn: |
| 1038 | + # Batch 1: tool_deps — 1 query |
| 1039 | + dep_rows = conn.execute( |
| 1040 | + f"SELECT skill_id, tool_key, critical FROM skill_tool_deps " |
| 1041 | + f"WHERE skill_id IN ({placeholders})", |
| 1042 | + skill_ids, |
| 1043 | + ).fetchall() |
| 1044 | + |
| 1045 | + deps_by_skill: Dict[str, List[str]] = {} |
| 1046 | + critical_by_skill: Dict[str, List[str]] = {} |
| 1047 | + for row in dep_rows: |
| 1048 | + sid = row["skill_id"] |
| 1049 | + deps_by_skill.setdefault(sid, []).append(row["tool_key"]) |
| 1050 | + if row["critical"]: |
| 1051 | + critical_by_skill.setdefault(sid, []).append(row["tool_key"]) |
| 1052 | + |
| 1053 | + # Batch 2: tags — 1 query (via TagSearch) |
| 1054 | + tags_by_skill = self._tag_search.get_tags_batch(skill_ids) |
| 1055 | + |
| 1056 | + # Batch 3: analyses — 2 queries (via AnalysisStore) |
| 1057 | + analyses_by_skill = self._analyses.batch_load_recent_analyses( |
| 1058 | + skill_ids, SkillRecord.MAX_RECENT |
| 1059 | + ) |
| 1060 | + |
| 1061 | + # Assemble |
| 1062 | + return [ |
| 1063 | + dataclasses.replace( |
| 1064 | + record, |
| 1065 | + tags=tags_by_skill.get(record.skill_id, []), |
| 1066 | + tool_dependencies=deps_by_skill.get(record.skill_id, []), |
| 1067 | + critical_tools=critical_by_skill.get(record.skill_id, []), |
| 1068 | + recent_analyses=analyses_by_skill.get(record.skill_id, []), |
| 1069 | + ) |
| 1070 | + for record in records |
| 1071 | + ] |
| 1072 | + |
995 | 1073 | # Deserialization |
996 | 1074 | def _to_record(self, conn: sqlite3.Connection, row: sqlite3.Row) -> SkillRecord: |
997 | 1075 | """Deserialize a skill_records row + related rows → SkillRecord.""" |
@@ -1101,6 +1179,10 @@ def get_tags(self, skill_id: str) -> List[str]: |
1101 | 1179 | """Get all tags for a skill (facade to TagSearch).""" |
1102 | 1180 | return self._tag_search.get_tags(skill_id) |
1103 | 1181 |
|
| 1182 | + def get_tags_batch(self, skill_ids: List[str]) -> Dict[str, List[str]]: |
| 1183 | + """Batch-get tags for multiple skills (facade to TagSearch).""" |
| 1184 | + return self._tag_search.get_tags_batch(skill_ids) |
| 1185 | + |
1104 | 1186 | def get_all_tags(self) -> List[Dict[str, Any]]: |
1105 | 1187 | """Get all tags with usage counts (facade to TagSearch).""" |
1106 | 1188 | return self._tag_search.get_all_tags() |
|
0 commit comments