1- """
1+ """SkillStore — SQLite persistence facade for skill quality tracking and evolution.
2+
3+ Architecture (Phase 3 decomposition):
4+
5+ SkillStore (facade) — store.py
6+ ├── MigrationManager — Schema creation & versioning (migration_manager.py)
7+ ├── SkillRepository — CRUD operations (skill_repository.py)
8+ ├── LineageTracker — Lineage traversal & evolution (lineage_tracker.py)
9+ ├── AnalysisStore — Execution analysis persistence (analysis_store.py)
10+ └── TagSearch — Tag indexing & search operations (tag_search.py)
11+
212Storage location: <project_root>/.openspace/openspace.db
13+
314Tables:
415 skill_records — SkillRecord main table
516 skill_lineage_parents — Lineage parent-child relationships (many-to-many)
617 execution_analyses — ExecutionAnalysis records (one per task)
718 skill_judgments — Per-skill judgments within an analysis
819 skill_tool_deps — Tool dependencies
920 skill_tags — Auxiliary tags
21+
22+ The SkillStore class serves as a unified facade that:
23+ 1. Manages the persistent SQLite connection and transaction safety
24+ 2. Delegates specialized operations to focused modules
25+ 3. Coordinates cross-module workflows (e.g., evolve_skill touches both lineage & repository)
26+ 4. Provides async API via asyncio.to_thread for thread-safe database access
1027"""
1128
1229from __future__ import annotations
@@ -81,9 +98,13 @@ def wrapper(*args, **kwargs):
8198
8299
83100class SkillStore :
84- """SQLite persistence engine — Skill quality tracking and evolution ledger .
101+ """SQLite persistence facade for skill quality tracking and evolution.
85102
86- Architecture:
103+ Phase 3 Architecture:
104+ Delegates specialized operations to focused modules while maintaining unified API.
105+ All modules share the same connection and lock for transaction consistency.
106+
107+ Concurrency:
87108 Write path: async method → asyncio.to_thread → _xxx_sync → self._mu lock → self._conn
88109 Read path: sync method → self._reader() → independent short connection (WAL parallel read)
89110
@@ -185,8 +206,6 @@ def _cleanup_wal_on_startup(self) -> None:
185206 if f .exists ():
186207 f .unlink ()
187208
188-
189-
190209 # Lifecycle
191210 def close (self ) -> None :
192211 """Close the persistent connection. Subsequent ops will raise.
@@ -280,7 +299,15 @@ def _sync_from_registry_sync(
280299 created = 0
281300 refreshed = 0
282301 with self ._mu :
283- self ._conn .execute ("BEGIN" )
302+ # Try to start transaction; if we're already in one, continue without BEGIN
303+ try :
304+ self ._conn .execute ("BEGIN" )
305+ need_commit = True
306+ except sqlite3 .OperationalError as e :
307+ if "cannot start a transaction within a transaction" in str (e ):
308+ need_commit = False # Already in transaction
309+ else :
310+ raise # Some other error
284311 try :
285312 # Fetch all existing records keyed by skill_id
286313 rows = self ._conn .execute (
@@ -374,9 +401,11 @@ def _sync_from_registry_sync(
374401 created += 1
375402 logger .debug (f"sync_from_registry: created { meta .name } [{ meta .skill_id } ]" )
376403
377- self ._conn .commit ()
404+ if need_commit :
405+ self ._conn .commit ()
378406 except Exception :
379- self ._conn .rollback ()
407+ if need_commit :
408+ self ._conn .rollback ()
380409 raise
381410
382411 if created or refreshed :
@@ -453,12 +482,22 @@ def _save_record_sync(self, record: SkillRecord) -> None:
453482 """
454483 self ._ensure_open ()
455484 with self ._mu :
456- self ._conn .execute ("BEGIN" )
485+ # Try to start transaction; if we're already in one, continue without BEGIN
486+ try :
487+ self ._conn .execute ("BEGIN" )
488+ need_commit = True
489+ except sqlite3 .OperationalError as e :
490+ if "cannot start a transaction within a transaction" in str (e ):
491+ need_commit = False # Already in transaction
492+ else :
493+ raise # Some other error
457494 try :
458495 self ._upsert (record )
459- self ._conn .commit ()
496+ if need_commit :
497+ self ._conn .commit ()
460498 except Exception :
461- self ._conn .rollback ()
499+ if need_commit :
500+ self ._conn .rollback ()
462501 raise
463502
464503 @_db_retry ()
@@ -470,13 +509,23 @@ def _save_records_sync(self, records: List[SkillRecord]) -> None:
470509 """
471510 self ._ensure_open ()
472511 with self ._mu :
473- self ._conn .execute ("BEGIN" )
512+ # Try to start transaction; if we're already in one, continue without BEGIN
513+ try :
514+ self ._conn .execute ("BEGIN" )
515+ need_commit = True
516+ except sqlite3 .OperationalError as e :
517+ if "cannot start a transaction within a transaction" in str (e ):
518+ need_commit = False # Already in transaction
519+ else :
520+ raise # Some other error
474521 try :
475522 for r in records :
476523 self ._upsert (r )
477- self ._conn .commit ()
524+ if need_commit :
525+ self ._conn .commit ()
478526 except Exception :
479- self ._conn .rollback ()
527+ if need_commit :
528+ self ._conn .rollback ()
480529 raise
481530
482531 @_db_retry ()
@@ -493,7 +542,15 @@ def _record_analysis_sync(self, analysis: ExecutionAnalysis) -> None:
493542 """
494543 self ._ensure_open ()
495544 with self ._mu :
496- self ._conn .execute ("BEGIN" )
545+ # Try to start transaction; if we're already in one, continue without BEGIN
546+ try :
547+ self ._conn .execute ("BEGIN" )
548+ need_commit = True
549+ except sqlite3 .OperationalError as e :
550+ if "cannot start a transaction within a transaction" in str (e ):
551+ need_commit = False # Already in transaction
552+ else :
553+ raise # Some other error
497554 try :
498555 # Delegate analysis storage to AnalysisStore (Epic 3.4)
499556 self ._analyses .insert_analysis (analysis )
@@ -517,9 +574,11 @@ def _record_analysis_sync(self, analysis: ExecutionAnalysis) -> None:
517574 (applied , completed , fallback , now_iso , j .skill_id ),
518575 )
519576
520- self ._conn .commit ()
577+ if need_commit :
578+ self ._conn .commit ()
521579 except Exception :
522- self ._conn .rollback ()
580+ if need_commit :
581+ self ._conn .rollback ()
523582 raise
524583
525584 @_db_retry ()
@@ -532,6 +591,9 @@ def _evolve_skill_sync(
532591
533592 Delegates to :class:`LineageTracker` (Epic 3.3).
534593 """
594+ # Note: We don't acquire self._mu here because the lineage tracker
595+ # will acquire it and both use the same mutex (would cause deadlock).
596+ # The lineage tracker handles its own transaction management.
535597 self ._lineage .record_derivation (new_record , parent_skill_ids )
536598
537599 @_db_retry ()
@@ -811,16 +873,26 @@ def clear(self) -> None:
811873 """Delete all data (keeps schema)."""
812874 self ._ensure_open ()
813875 with self ._mu :
814- self ._conn .execute ("BEGIN" )
876+ # Try to start transaction; if we're already in one, continue without BEGIN
877+ try :
878+ self ._conn .execute ("BEGIN" )
879+ need_commit = True
880+ except sqlite3 .OperationalError as e :
881+ if "cannot start a transaction within a transaction" in str (e ):
882+ need_commit = False # Already in transaction
883+ else :
884+ raise # Some other error
815885 try :
816886 # CASCADE on skill_records cleans up: lineage_parents, tool_deps, tags
817887 self ._conn .execute ("DELETE FROM skill_records" )
818888 # Delegate analysis clearing to AnalysisStore (Epic 3.4)
819889 self ._analyses .clear_all_analyses ()
820- self ._conn .commit ()
890+ if need_commit :
891+ self ._conn .commit ()
821892 logger .info ("SkillStore cleared" )
822893 except Exception :
823- self ._conn .rollback ()
894+ if need_commit :
895+ self ._conn .rollback ()
824896 raise
825897
826898 def vacuum (self ) -> None :
@@ -1053,8 +1125,29 @@ def get_all_tags(self) -> List[Dict[str, Any]]:
10531125 return self ._tag_search .get_all_tags ()
10541126
10551127 def sync_tags (self , skill_id : str , tags : List [str ]) -> None :
1056- """Synchronize tags for a skill (facade to TagSearch)."""
1057- return self ._tag_search .sync_tags (skill_id , tags )
1128+ """Synchronize tags for a skill (facade to TagSearch).
1129+
1130+ Must manage transaction explicitly — TagSearch shared-mode
1131+ delegates commit responsibility to the caller (us).
1132+ """
1133+ self ._ensure_open ()
1134+ with self ._mu :
1135+ try :
1136+ self ._conn .execute ("BEGIN" )
1137+ need_commit = True
1138+ except sqlite3 .OperationalError as e :
1139+ if "cannot start a transaction within a transaction" in str (e ):
1140+ need_commit = False
1141+ else :
1142+ raise
1143+ try :
1144+ self ._tag_search .sync_tags (skill_id , tags )
1145+ if need_commit :
1146+ self ._conn .commit ()
1147+ except Exception :
1148+ if need_commit :
1149+ self ._conn .rollback ()
1150+ raise
10581151
10591152 # ── Migration Management (Epic 3.6) ─────────────────────────────────
10601153
@@ -1070,6 +1163,7 @@ def set_schema_version(self, version: int) -> None:
10701163 """Set schema version (facade to MigrationManager).
10711164
10721165 DEPRECATED: Use ensure_current_schema() instead.
1166+ This method is kept for backward compatibility with existing tests.
10731167 """
10741168 return self ._migrations ._set_schema_version (version )
10751169
0 commit comments