Initial commit: a0_software_orchestrator v1.0

- Auto-Registration-Bug behoben (register_project/get_project_id/resolve_project Trennung)
- 25 Tests gruen (Pytest)
- block_compactor-Tool refactored (Option B: Soft-Check statt Hard-Block)
- 4 Restbaustellen gefixt
- DB-Schema: plugin_settings-Tabelle hinzugefuegt
- 3 Schattenprojekte aus DB geloescht
- Plan v3 + Refactor-Plan + Worklog dokumentiert
This commit is contained in:
Software Orchestrator
2026-06-16 22:13:06 +00:00
commit 5769c1cd22
236 changed files with 17825 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
"""
A0 Software Orchestrator Patterns-Bibliothek
Selbstlernende Wissensdatenbank für Projekt-Patterns.
Exportiert:
- PatternDB: Singleton-Datenbank-Klasse
- PatternExtractor: Extrahiert Patterns aus Projekt-Artefakten
- extract_from_project: Convenience-Funktion
- get_db: Singleton-Zugriff
"""
from usr.plugins.a0_software_orchestrator.helpers.library.db import PatternDB, get_db
from usr.plugins.a0_software_orchestrator.helpers.library.extractor import PatternExtractor, extract_from_project
__all__ = ['PatternDB', 'PatternExtractor', 'extract_from_project', 'get_db']
__version__ = '1.0.0'
+823
View File
@@ -0,0 +1,823 @@
"""
A0 Software Orchestrator Patterns-Bibliothek
Zentrale Datenbank-Klasse mit FTS5, Vektor-Suche, Projekt-Registry,
Pattern-Feedback, Konflikt-Management und Aging.
Verwendung:
from library.db import PatternDB
db = PatternDB() # Singleton, verwendet Standard-Pfad
results = db.search_fts("FastAPI Docker")
patterns = db.search_semantic(query_text, top_k=5)
projects = db.get_active_projects()
"""
import sqlite3
import json
import hashlib
import threading
from pathlib import Path
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any, Tuple
# ---------------------------------------------------------------------------
# Singleton PatternDB
# ---------------------------------------------------------------------------
class PatternDB:
"""
Singleton-Datenbank-Klasse für die Patterns-Bibliothek.
Automatische Initialisierung beim ersten Zugriff.
"""
_instance = None
_lock = threading.Lock()
DEFAULT_DB_PATH = Path(__file__).parent / "patterns.db"
DEFAULT_SCHEMA_PATH = Path(__file__).parent / "schema.sql"
def __new__(cls, db_path: Optional[Path] = None):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
instance = super().__new__(cls)
instance._initialized = False
cls._instance = instance
return cls._instance
def __init__(self, db_path: Optional[Path] = None):
if self._initialized:
return
self.db_path = Path(db_path) if db_path else self.DEFAULT_DB_PATH
self.schema_path = self.DEFAULT_SCHEMA_PATH
self._conn: Optional[sqlite3.Connection] = None
self._vec_available: Optional[bool] = None
self._embedding_model = None
# Datenbank initialisieren
self._ensure_db()
self._initialized = True
# -----------------------------------------------------------------------
# Connection Management
# -----------------------------------------------------------------------
@property
def conn(self) -> sqlite3.Connection:
"""Thread-sichere Connection mit WAL-Mode."""
if self._conn is None:
self._conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
self._conn.row_factory = sqlite3.Row
self._conn.execute("PRAGMA journal_mode=WAL")
self._conn.execute("PRAGMA foreign_keys=ON")
self._conn.execute("PRAGMA cache_size=-64000") # 64 MB Cache
self._conn.execute("PRAGMA busy_timeout=5000") # 5 Sekunden Timeout
return self._conn
def _ensure_db(self):
"""Stellt sicher, dass die Datenbank existiert und das Schema aktuell ist."""
self.db_path.parent.mkdir(parents=True, exist_ok=True)
if not self.db_path.exists():
# Neue Datenbank: Basisschema ausführen
if self.schema_path.exists():
schema = self.schema_path.read_text(encoding='utf-8')
self.conn.executescript(schema)
self.conn.commit()
# Neue und existierende DBs immer auf Runtime-Schema migrieren.
self._run_migrations()
def _run_migrations(self):
"""Führt ausstehende Schema-Migrationen aus."""
cursor = self.conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'"
)
if cursor.fetchone() is None:
# Alte DB ohne schema_version initialisieren
self.conn.executescript(self.schema_path.read_text(encoding='utf-8'))
self.conn.commit()
# Weitere Migrationen aus migrations.py
try:
from .migrations import run_migrations
run_migrations(self)
except ImportError:
pass
def close(self):
"""Schließt die Datenbank-Verbindung."""
if self._conn:
self._conn.close()
self._conn = None
# -----------------------------------------------------------------------
# Projekt-Registry
# -----------------------------------------------------------------------
def register_project(self, name: str, path: str, tech_stack: Optional[Dict] = None,
description: str = "", git_url: str = "") -> int:
"""Registriert ein neues Projekt oder aktualisiert ein bestehendes.
ARCHITEKTUR-NOTE (Bugfix-Auto-Registration §3.6 / Fix 4 Befund):
Diese Methode gehört zum DB-Layer (helpers/library/db.py), NICHT zum
Project-Layer (helpers/db_state_store.py). Sie wird derzeit NUR von
extractor.py (Pattern-Extraktion aus Repo-Verzeichnissen) aufgerufen,
mit Namen aus `project_path` (Verzeichnisname). User-facing Project-
Registrierung läuft über `db_state_store.register_project()`, das seit
Plan v3 §3.1 Pattern+Blacklist validiert.
Daher: KEINE Plausi-Prüfung hier. Bewusst out-of-scope, weil:
1. Einziger Caller ist ein internes Library-Tool (extractor.py).
2. extractor.py nutzt Verzeichnisnamen, die bereits durch
project_path-Lookup semi-kontrolliert sind.
3. Plan v3 hat DB-Layer-Plausi explizit als separater Fix markiert.
Wenn ein neuer Caller mit user-input-Namen diese Methode aufruft,
MUSS er Pattern+Blacklist-Prüfung VORAB durchführen (siehe
`db_state_store._validate_project_name`).
"""
tech_json = json.dumps(tech_stack) if tech_stack is not None else "{}"
cur = self.conn.execute("""
INSERT INTO projects (name, project_path, git_url, tech_stack, description)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(name) DO UPDATE SET
project_path = excluded.project_path,
git_url = excluded.git_url,
tech_stack = excluded.tech_stack,
description = excluded.description
""", (name, path, git_url, tech_json, description))
row = self.conn.execute("SELECT id FROM projects WHERE name = ?", (name,)).fetchone()
project_id = int(row[0] if row else cur.lastrowid)
self.conn.execute("""
INSERT INTO project_state (project_id, status, phase, last_active_at)
VALUES (?, 'active', 'intake', datetime('now'))
ON CONFLICT(project_id) DO UPDATE SET last_active_at = datetime('now')
""", (project_id,))
self.conn.commit()
return project_id
def update_project_phase(self, name: str, phase: str, plan_mode: str = None):
"""Aktualisiert Phase und Plan-Mode eines Projekts."""
project_id = self.register_project(name, "", description="auto-created by PatternDB")
if plan_mode:
self.conn.execute("""
UPDATE project_state
SET phase = ?, plan_mode = ?, last_active_at = datetime('now'), updated_at = datetime('now')
WHERE project_id = ?
""", (phase, plan_mode, project_id))
else:
self.conn.execute("""
UPDATE project_state
SET phase = ?, last_active_at = datetime('now'), updated_at = datetime('now')
WHERE project_id = ?
""", (phase, project_id))
self.conn.commit()
def update_project_metrics(self, name: str, total_tasks: int = None,
completed_tasks: int = None, open_errors: int = None):
"""Aktualisiert die Projekt-Metriken."""
project_id = self.register_project(name, "", description="auto-created by PatternDB")
updates = []
params: List[Any] = []
if total_tasks is not None:
updates.append("total_tasks = ?"); params.append(total_tasks)
if completed_tasks is not None:
updates.append("completed_tasks = ?"); params.append(completed_tasks)
if open_errors is not None:
updates.append("open_errors = ?"); params.append(open_errors)
if updates:
updates.append("last_active_at = datetime('now')")
updates.append("updated_at = datetime('now')")
params.append(project_id)
self.conn.execute(f"UPDATE project_state SET {', '.join(updates)} WHERE project_id = ?", params)
self.conn.commit()
def get_active_projects(self) -> List[sqlite3.Row]:
"""Alle aktiven Projekte."""
return self.conn.execute("""
SELECT p.*, ps.status, ps.phase, ps.plan_mode, ps.total_tasks,
ps.completed_tasks, ps.open_errors, ps.last_active_at, ps.completed_at
FROM projects p
JOIN project_state ps ON ps.project_id = p.id
WHERE ps.status = 'active'
ORDER BY COALESCE(ps.last_active_at, p.created_at) DESC
""").fetchall()
def get_project_summary(self, project_name: str) -> Optional[sqlite3.Row]:
"""Kurzübersicht eines Projekts."""
return self.conn.execute("""
SELECT p.name AS project_name, ps.status, ps.phase, ps.plan_mode,
ps.completed_tasks || '/' || ps.total_tasks AS progress,
ps.open_errors, COALESCE(p.patterns_extracted, 0) AS patterns_extracted,
p.tech_stack, ps.last_active_at, p.description
FROM projects p
JOIN project_state ps ON ps.project_id = p.id
WHERE p.name = ?
""", (project_name,)).fetchone()
def get_projects_by_tech(self, tech: str) -> List[sqlite3.Row]:
"""Alle Projekte mit bestimmter Technologie."""
return self.conn.execute("""
SELECT p.*, ps.status, ps.phase, ps.plan_mode
FROM projects p
JOIN project_state ps ON ps.project_id = p.id
WHERE p.tech_stack LIKE ?
ORDER BY p.name
""", (f'%{tech}%',)).fetchall()
def get_orphaned_projects(self, days: int = 7) -> List[sqlite3.Row]:
"""Projekte, die >N Tage nicht aktiv waren."""
threshold = (datetime.utcnow() - timedelta(days=days)).isoformat()
return self.conn.execute("""
SELECT p.*, ps.status, ps.phase, ps.plan_mode, ps.last_active_at
FROM projects p
JOIN project_state ps ON ps.project_id = p.id
WHERE ps.status = 'active'
AND COALESCE(ps.last_active_at, p.created_at) < ?
""", (threshold,)).fetchall()
def set_project_status(self, name: str, status: str, notes: str = ""):
"""Setzt den Projekt-Status (active, paused, completed, archived, failed)."""
project_id = self.register_project(name, "", description=notes or "auto-created by PatternDB")
if status == 'completed':
self.conn.execute("""
UPDATE project_state
SET status = ?, completed_at = datetime('now'), updated_at = datetime('now')
WHERE project_id = ?
""", (status, project_id))
else:
self.conn.execute("""
UPDATE project_state
SET status = ?, last_active_at = datetime('now'), updated_at = datetime('now')
WHERE project_id = ?
""", (status, project_id))
if notes:
self.conn.execute("UPDATE projects SET description = ? WHERE id = ?", (notes, project_id))
self.conn.commit()
# -----------------------------------------------------------------------
# Patterns CRUD
# -----------------------------------------------------------------------
def add_pattern(self, title: str, category: str, pattern_type: str,
description: str, **kwargs) -> int:
"""
Fügt ein neues Pattern hinzu.
Args:
title: Kurztitel
category: docker, python, frontend, etc.
pattern_type: code_snippet, error_solution, etc.
description: Beschreibung
**kwargs: subcategory, code_example, when_to_use, why_it_works,
pitfalls, source_project_id, source_task_id, source_error,
source_file, framework_version, tags (Liste),
validated (0=auto, 1=reviewed, 2=manual)
Returns:
int: ID des neuen Patterns
"""
tags = kwargs.pop('tags', [])
columns = ['title', 'category', 'pattern_type', 'description']
values = [title, category, pattern_type, description]
allowed_kwargs = [
'subcategory', 'code_example', 'when_to_use', 'why_it_works',
'pitfalls', 'source_project_id', 'source_task_id', 'source_error',
'source_file', 'framework_version', 'validated'
]
for key in allowed_kwargs:
if key in kwargs and kwargs[key] is not None:
columns.append(key)
values.append(kwargs[key])
placeholders = ', '.join(['?'] * len(columns))
columns_str = ', '.join(columns)
cursor = self.conn.execute(
f"INSERT INTO patterns ({columns_str}) VALUES ({placeholders})",
values
)
pattern_id = cursor.lastrowid
# Tags hinzufügen
for tag_name in tags:
self._add_tag(pattern_id, tag_name)
self.conn.commit()
return pattern_id
def update_pattern(self, pattern_id: int, **kwargs) -> bool:
"""Aktualisiert ein bestehendes Pattern."""
allowed = [
'title', 'category', 'subcategory', 'pattern_type', 'description',
'code_example', 'when_to_use', 'why_it_works', 'pitfalls',
'framework_version', 'validated', 'validation_date', 'validated_by'
]
updates = []
params: List[Any] = []
for key in allowed:
if key in kwargs:
updates.append(f"{key} = ?")
params.append(kwargs[key])
if not updates:
return False
updates.append("updated_at = datetime('now')")
params.append(pattern_id)
self.conn.execute(
f"UPDATE patterns SET {', '.join(updates)} WHERE id = ?",
params
)
self.conn.commit()
# Tags aktualisieren
if 'tags' in kwargs:
self.conn.execute("DELETE FROM pattern_tags WHERE pattern_id = ?", (pattern_id,))
for tag_name in kwargs['tags']:
self._add_tag(pattern_id, tag_name)
return True
def get_pattern(self, pattern_id: int) -> Optional[sqlite3.Row]:
"""Lädt ein einzelnes Pattern mit allen Details."""
return self.conn.execute(
"SELECT * FROM patterns WHERE id = ?", (pattern_id,)
).fetchone()
def get_patterns_by_category(self, category: str, pattern_type: str = None,
validated_only: bool = True,
limit: int = 20) -> List[sqlite3.Row]:
"""Patterns nach Kategorie filtern."""
sql = "SELECT * FROM patterns WHERE category = ?"
params: List[Any] = [category]
if pattern_type:
sql += " AND pattern_type = ?"
params.append(pattern_type)
if validated_only:
sql += " AND validated >= 0"
sql += " ORDER BY usage_count DESC, success_rate DESC LIMIT ?"
params.append(limit)
return self.conn.execute(sql, params).fetchall()
def get_pattern_tags(self, pattern_id: int) -> List[str]:
"""Alle Tags eines Patterns."""
rows = self.conn.execute("""
SELECT t.name FROM tags t
JOIN pattern_tags pt ON t.id = pt.tag_id
WHERE pt.pattern_id = ?
""", (pattern_id,)).fetchall()
return [r[0] for r in rows]
def _add_tag(self, pattern_id: int, tag_name: str):
"""Fügt einen Tag hinzu und verknüpft ihn mit einem Pattern."""
self.conn.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", (tag_name,))
tag_id = self.conn.execute(
"SELECT id FROM tags WHERE name = ?", (tag_name,)
).fetchone()[0]
self.conn.execute(
"INSERT OR IGNORE INTO pattern_tags (pattern_id, tag_id) VALUES (?, ?)",
(pattern_id, tag_id)
)
# -----------------------------------------------------------------------
# FTS5 Volltextsuche
# -----------------------------------------------------------------------
def search_fts(self, query: str, limit: int = 10,
category: str = None, pattern_type: str = None,
validated_only: bool = True) -> List[sqlite3.Row]:
"""
Volltextsuche mit FTS5.
Args:
query: Suchbegriffe (FTS5-Syntax: "FastAPI Docker", "error AND fix", etc.)
limit: Maximale Ergebnisse
category: Optional, nach Kategorie filtern
pattern_type: Optional, nach Typ filtern
validated_only: Nur validierte Patterns (>= 0)
Returns:
Liste von Pattern-Rows mit rank-Spalte
"""
# FTS5-Abfrage vorbereiten (Wildcards für Teilwortsuche)
fts_query = ' OR '.join(f'"{term}"*' for term in query.split())
sql = """
SELECT p.*, rank
FROM patterns_fts
JOIN patterns p ON patterns_fts.rowid = p.id
WHERE patterns_fts MATCH ?
"""
params: List[Any] = [fts_query]
if category:
sql += " AND p.category = ?"
params.append(category)
if pattern_type:
sql += " AND p.pattern_type = ?"
params.append(pattern_type)
if validated_only:
sql += " AND p.validated >= 0"
sql += " ORDER BY rank LIMIT ?"
params.append(limit)
return self.conn.execute(sql, params).fetchall()
# -----------------------------------------------------------------------
# Vektor-Suche (sqlite-vec)
# -----------------------------------------------------------------------
@property
def vec_available(self) -> bool:
"""Prüft, ob sqlite-vec verfügbar ist."""
if self._vec_available is None:
try:
import sqlite_vec
self._vec_available = True
self._init_vec_table()
except ImportError:
self._vec_available = False
return self._vec_available
def _init_vec_table(self):
"""Initialisiert die Vektor-Tabelle, wenn sqlite-vec vorhanden ist."""
try:
import sqlite_vec
self.conn.enable_load_extension(True)
sqlite_vec.load(self.conn)
self.conn.execute("""
CREATE VIRTUAL TABLE IF NOT EXISTS pattern_embeddings USING vec0(
embedding float[384]
)
""")
self.conn.commit()
except Exception as e:
print(f"[PatternDB] sqlite-vec konnte nicht initialisiert werden: {e}")
self._vec_available = False
def _get_embedding_model(self):
"""Lädt das Embedding-Modell (lazy, nur wenn benötigt)."""
if self._embedding_model is None:
try:
from sentence_transformers import SentenceTransformer
self._embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
except ImportError:
print("[PatternDB] sentence-transformers nicht installiert. "
"Vektor-Suche nicht verfügbar.")
self._vec_available = False
return self._embedding_model
def store_embedding(self, pattern_id: int, text: str):
"""
Speichert das Embedding eines Patterns für die Vektor-Suche.
Args:
pattern_id: ID des Patterns
text: Text zum Embedden (description + code_example)
"""
if not self.vec_available:
return
model = self._get_embedding_model()
if model is None:
return
embedding = model.encode(text)
# Bestehendes Embedding löschen (vec0 hat keine UPDATE-Logik)
self.conn.execute(
"DELETE FROM pattern_embeddings WHERE rowid = ?", (pattern_id,)
)
self.conn.execute(
"INSERT INTO pattern_embeddings (rowid, embedding) VALUES (?, ?)",
(pattern_id, embedding.tobytes())
)
self.conn.commit()
def search_semantic(self, query: str, top_k: int = 10,
category: str = None, pattern_type: str = None,
validated_only: bool = True) -> List[Tuple[sqlite3.Row, float]]:
"""
Semantische Vektor-Suche. Fallback auf FTS5, wenn sqlite-vec nicht verfügbar.
Args:
query: Natürlichsprachliche Suchanfrage
top_k: Anzahl Ergebnisse
category: Optional, Kategorie-Filter
pattern_type: Optional, Typ-Filter
validated_only: Nur validierte Patterns
Returns:
Liste von (Pattern-Row, similarity_score) Tupeln, absteigend nach Ähnlichkeit
"""
if not self.vec_available:
# Fallback auf FTS5
rows = self.search_fts(query, limit=top_k, category=category,
pattern_type=pattern_type, validated_only=validated_only)
return [(r, -r['rank']) for r in rows] # rank ist negativ, invertieren
model = self._get_embedding_model()
if model is None:
rows = self.search_fts(query, limit=top_k, category=category,
pattern_type=pattern_type, validated_only=validated_only)
return [(r, -r['rank']) for r in rows]
# Query embedden
query_embedding = model.encode(query)
# KNN-Suche
categories_where = ""
if category:
categories_where = f"AND p.category = '{category}'"
if pattern_type:
categories_where += f" AND p.pattern_type = '{pattern_type}'"
if validated_only:
categories_where += " AND p.validated >= 0"
sql = f"""
SELECT p.*, vec_distance_cosine(pe.embedding, ?) AS distance
FROM pattern_embeddings pe
JOIN patterns p ON pe.rowid = p.id
WHERE 1=1 {categories_where}
ORDER BY distance ASC
LIMIT ?
"""
rows = self.conn.execute(sql, (query_embedding.tobytes(), top_k)).fetchall()
# Distance in Similarity umrechnen (1 - distance)
return [(r, 1.0 - r['distance']) for r in rows]
# -----------------------------------------------------------------------
# Pattern-Feedback & Lernen
# -----------------------------------------------------------------------
def add_feedback(self, pattern_id: int, project_id: int, outcome: str,
reason: str = "", context_diff: str = "",
alternative_used: str = "",
alternative_pattern_id: int = None) -> int:
"""
Speichert Feedback zu einem Pattern (positiv oder negativ).
Aktualisiert automatisch die Pattern-Statistiken.
"""
feedback_id = self.conn.execute("""
INSERT INTO pattern_feedback
(pattern_id, project_id, outcome, reason, context_diff,
alternative_used, alternative_pattern_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (pattern_id, project_id, outcome, reason, context_diff,
alternative_used, alternative_pattern_id)).lastrowid
# Pattern-Statistiken aktualisieren
if outcome == 'success':
self.conn.execute(
"UPDATE patterns SET success_count = success_count + 1, usage_count = usage_count + 1, last_used_at = datetime('now') WHERE id = ?",
(pattern_id,)
)
elif outcome == 'failure':
self.conn.execute(
"UPDATE patterns SET failure_count = failure_count + 1, usage_count = usage_count + 1, last_used_at = datetime('now') WHERE id = ?",
(pattern_id,)
)
elif outcome == 'partial_success':
self.conn.execute(
"UPDATE patterns SET success_count = success_count + 1, failure_count = failure_count + 1, usage_count = usage_count + 1, last_used_at = datetime('now') WHERE id = ?",
(pattern_id,)
)
# Success-Rate neu berechnen
self.conn.execute("""
UPDATE patterns
SET success_rate = CASE
WHEN success_count + failure_count > 0
THEN CAST(success_count AS REAL) / (success_count + failure_count)
ELSE 0.0
END
WHERE id = ?
""", (pattern_id,))
self.conn.commit()
return feedback_id
def record_usage(self, pattern_id: int):
"""Vermerkt, dass ein Pattern verwendet wurde (ohne Erfolg/Misserfolg)."""
self.conn.execute(
"UPDATE patterns SET usage_count = usage_count + 1, last_used_at = datetime('now') WHERE id = ?",
(pattern_id,)
)
self.conn.commit()
# -----------------------------------------------------------------------
# Pattern-Konflikte
# -----------------------------------------------------------------------
def add_conflict(self, pattern_a_id: int, pattern_b_id: int,
conflict_type: str, description: str = "",
resolution_context: str = "") -> int:
"""Registriert einen Konflikt zwischen zwei Patterns."""
return self.conn.execute("""
INSERT OR IGNORE INTO pattern_conflicts
(pattern_a_id, pattern_b_id, conflict_type, description, resolution_context)
VALUES (?, ?, ?, ?, ?)
""", (pattern_a_id, pattern_b_id, conflict_type, description, resolution_context)).lastrowid
def get_conflicts(self) -> List[sqlite3.Row]:
"""Alle ungelösten Konflikte."""
return self.conn.execute("""
SELECT * FROM v_conflicts WHERE resolved_by = 'system'
""").fetchall()
def resolve_conflict(self, conflict_id: int, resolution: str, resolved_by: str = "quality_reviewer"):
"""Löst einen Pattern-Konflikt auf."""
self.conn.execute("""
UPDATE pattern_conflicts
SET resolution_context = ?, resolved_by = ?, resolved_at = datetime('now')
WHERE id = ?
""", (resolution, resolved_by, conflict_id))
self.conn.commit()
# -----------------------------------------------------------------------
# Pattern-Aging
# -----------------------------------------------------------------------
def update_staleness_scores(self):
"""
Berechnet den staleness_score für alle Patterns neu.
Faktoren:
- Alter (Tage seit Erstellung): 0-100 → 0.0-0.4
- Letzte Verwendung (Tage): 0-365 → 0.0-0.3
- Letzte Validierung (Tage): 0-365 → 0.0-0.2
- Framework-Version vorhanden? nein → +0.1
"""
self.conn.execute("""
UPDATE patterns SET staleness_score = (
MIN(1.0,
-- Alter-Faktor: 0.4 nach 365 Tagen
(julianday('now') - julianday(created_at)) / 365.0 * 0.4 +
-- Letzte-Verwendung-Faktor: 0.3 nach 365 Tagen
CASE WHEN last_used_at IS NOT NULL
THEN (julianday('now') - julianday(last_used_at)) / 365.0 * 0.3
ELSE 0.3 -- Nie verwendet = voller Abzug
END +
-- Validierungs-Faktor: 0.2 nach 365 Tagen
CASE WHEN validation_date IS NOT NULL
THEN (julianday('now') - julianday(validation_date)) / 365.0 * 0.2
ELSE 0.2 -- Nie validiert = voller Abzug
END +
-- Framework-Version fehlt: +0.1
CASE WHEN framework_version IS NULL OR framework_version = ''
THEN 0.1 ELSE 0.0 END
)
)
WHERE validated >= 0
""")
self.conn.commit()
def deprecate_stale_patterns(self, threshold: float = 0.7):
"""Markiert stark veraltete Patterns als deprecated (validated = -1)."""
self.conn.execute("""
UPDATE patterns SET validated = -1, updated_at = datetime('now')
WHERE staleness_score >= ? AND validated >= 0
""", (threshold,))
self.conn.commit()
def get_stale_patterns(self, threshold: float = 0.5) -> List[sqlite3.Row]:
"""Patterns, die zu veralten drohen."""
return self.conn.execute("""
SELECT * FROM patterns
WHERE staleness_score >= ? AND validated >= 0
ORDER BY staleness_score DESC
""", (threshold,)).fetchall()
# -----------------------------------------------------------------------
# Relationen
# -----------------------------------------------------------------------
def add_relation(self, pattern_a_id: int, pattern_b_id: int,
relation_type: str, strength: float = 1.0, notes: str = ""):
"""Fügt eine Beziehung zwischen zwei Patterns hinzu."""
self.conn.execute("""
INSERT OR IGNORE INTO pattern_relations
(pattern_a_id, pattern_b_id, relation_type, strength, notes)
VALUES (?, ?, ?, ?, ?)
""", (pattern_a_id, pattern_b_id, relation_type, strength, notes))
self.conn.commit()
def get_related_patterns(self, pattern_id: int,
relation_type: str = None) -> List[sqlite3.Row]:
"""Verwandte Patterns eines Patterns."""
sql = """
SELECT p.*, pr.relation_type, pr.strength
FROM pattern_relations pr
JOIN patterns p ON (
CASE WHEN pr.pattern_a_id = ? THEN pr.pattern_b_id = p.id
ELSE pr.pattern_a_id = p.id END
)
WHERE (pr.pattern_a_id = ? OR pr.pattern_b_id = ?)
"""
params: List[Any] = [pattern_id, pattern_id, pattern_id]
if relation_type:
sql += " AND pr.relation_type = ?"
params.append(relation_type)
return self.conn.execute(sql, params).fetchall()
# -----------------------------------------------------------------------
# Lern-Log
# -----------------------------------------------------------------------
def log_learning(self, pattern_id: int, project_id: int, trigger: str,
source_file: str = "", source_content: str = ""):
"""Protokolliert einen Lern-Vorgang."""
content_hash = hashlib.md5(source_content.encode()).hexdigest() if source_content else None
self.conn.execute("""
INSERT INTO learn_log (pattern_id, project_id, trigger, source_file, source_content_hash)
VALUES (?, ?, ?, ?, ?)
""", (pattern_id, project_id, trigger, source_file, content_hash))
self.conn.commit()
# -----------------------------------------------------------------------
# Backup
# -----------------------------------------------------------------------
def backup(self, backup_path: Path = None) -> Path:
"""Erstellt ein Backup der Datenbank."""
if backup_path is None:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_path = self.db_path.parent / f"patterns_backup_{timestamp}.db"
# SQLite-Backup via Backup-API
backup_conn = sqlite3.connect(str(backup_path))
self.conn.backup(backup_conn)
backup_conn.close()
return backup_path
# -----------------------------------------------------------------------
# Statistik & Reporting
# -----------------------------------------------------------------------
def get_stats(self) -> Dict[str, Any]:
"""Gesamtstatistik der Bibliothek."""
total_patterns = self.conn.execute("SELECT COUNT(*) FROM patterns").fetchone()[0]
validated = self.conn.execute(
"SELECT COUNT(*) FROM patterns WHERE validated >= 0"
).fetchone()[0]
deprecated = self.conn.execute(
"SELECT COUNT(*) FROM patterns WHERE validated = -1"
).fetchone()[0]
unvalidated = self.conn.execute(
"SELECT COUNT(*) FROM patterns WHERE validated = 0"
).fetchone()[0]
categories = self.conn.execute("""
SELECT category, COUNT(*) as cnt FROM patterns
WHERE validated >= 0 GROUP BY category ORDER BY cnt DESC
""").fetchall()
active_projects = self.conn.execute(
"SELECT COUNT(*) FROM project_state WHERE status = 'active'"
).fetchone()[0]
total_feedback = self.conn.execute(
"SELECT COUNT(*) FROM pattern_feedback"
).fetchone()[0]
avg_success_rate = self.conn.execute("""
SELECT AVG(success_rate) FROM patterns WHERE usage_count > 0 AND validated >= 0
""").fetchone()[0] or 0.0
return {
'total_patterns': total_patterns,
'validated_patterns': validated,
'unvalidated_patterns': unvalidated,
'deprecated_patterns': deprecated,
'active_projects': active_projects,
'total_feedback_entries': total_feedback,
'average_success_rate': round(avg_success_rate * 100, 1),
'categories': {r['category']: r['cnt'] for r in categories},
'vec_available': self.vec_available,
}
# ---------------------------------------------------------------------------
# Convenience-Funktionen
# ---------------------------------------------------------------------------
def get_db() -> PatternDB:
"""Singleton-Zugriff."""
return PatternDB()
+470
View File
@@ -0,0 +1,470 @@
"""
A0 Software Orchestrator Pattern-Extraktor
Extrahiert Patterns aus abgeschlossenen Projekt-Artefakten und speichert sie
in der Patterns-Datenbank.
Kontinuierliche Trigger:
- error_fixed: known_errors.md wurde aktualisiert
- task_completed: Task in task_graph.json als 'done' markiert
- deploy_ok: runtime_report.md zeigt Erfolg
- decision_made: decisions.md neuer Eintrag
- release_done: Release Audit abgeschlossen
"""
import json
import re
import hashlib
from pathlib import Path
from typing import Optional, List, Dict, Any
from datetime import datetime
from usr.plugins.a0_software_orchestrator.helpers.library.db import PatternDB, get_db
# ---------------------------------------------------------------------------
# Extraktor-Klasse
# ---------------------------------------------------------------------------
class PatternExtractor:
"""
Extrahiert Patterns aus Projekt-Artefakten.
Verwendung:
extractor = PatternExtractor(project_path)
patterns = extractor.extract_from_known_errors()
patterns += extractor.extract_from_decisions()
patterns += extractor.extract_from_architecture()
patterns += extractor.extract_from_docker()
patterns += extractor.extract_from_deployment()
"""
def __init__(self, project_path: Path, db: PatternDB = None):
self.project_path = Path(project_path)
self.a0_path = self.project_path / ".a0"
self.db = db or get_db()
self.project_name = self.project_path.name
self.project_id = None
# Projekt in Registry finden/registrieren
self._ensure_project_registered()
def _ensure_project_registered(self):
"""Stellt sicher, dass das Projekt in der Registry ist."""
existing = self.db.conn.execute(
"SELECT id FROM projects WHERE project_path = ? OR name = ?",
(str(self.project_path), self.project_name)
).fetchone()
if existing:
self.project_id = existing[0]
else:
# Tech-Stack aus Projekt-Artefakten erkennen
tech_stack = self._detect_tech_stack()
self.project_id = self.db.register_project(
name=self.project_name,
path=str(self.project_path),
tech_stack=tech_stack,
description=f"Automatisch registriert am {datetime.now().isoformat()}"
)
def _detect_tech_stack(self) -> Dict[str, Any]:
"""Erkennt den Tech-Stack aus Projekt-Dateien."""
stack = {}
# Python
if (self.project_path / "requirements.txt").exists():
content = (self.project_path / "requirements.txt").read_text()
if 'fastapi' in content.lower():
stack['backend'] = 'FastAPI'
elif 'flask' in content.lower():
stack['backend'] = 'Flask'
elif 'django' in content.lower():
stack['backend'] = 'Django'
if 'sqlalchemy' in content.lower():
stack['orm'] = 'SQLAlchemy'
if 'pydantic' in content.lower():
stack['validation'] = 'Pydantic'
if 'alembic' in content.lower():
stack['migrations'] = 'Alembic'
# Node/Frontend
if (self.project_path / "package.json").exists():
try:
pkg = json.loads((self.project_path / "package.json").read_text())
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
if 'react' in deps:
stack['frontend'] = 'React'
if 'next' in deps:
stack['frontend'] = 'Next.js'
if 'vue' in deps:
stack['frontend'] = 'Vue'
if 'typescript' in deps:
stack['language'] = 'TypeScript'
if 'vite' in deps:
stack['bundler'] = 'Vite'
except (json.JSONDecodeError, KeyError):
pass
# Docker
if (self.project_path / "Dockerfile").exists():
stack['docker'] = True
if (self.project_path / "docker-compose.yml").exists():
stack['docker_compose'] = True
# Datenbank aus env.md oder config
env_md = self.a0_path / "env.md"
if env_md.exists():
content = env_md.read_text().lower()
if 'postgresql' in content or 'postgres' in content:
stack['db'] = 'PostgreSQL'
elif 'mysql' in content:
stack['db'] = 'MySQL'
elif 'sqlite' in content:
stack['db'] = 'SQLite'
return stack
# -----------------------------------------------------------------------
# Extraktoren für verschiedene Artefakte
# -----------------------------------------------------------------------
def extract_from_known_errors(self) -> List[int]:
"""
Extrahiert error_solution-Patterns aus known_errors.md.
Nur Einträge, die eine Lösung enthalten (nicht nur Beschreibung).
"""
errors_file = self.a0_path / "known_errors.md"
if not errors_file.exists():
return []
content = errors_file.read_text(encoding='utf-8')
content_hash = hashlib.md5(content.encode()).hexdigest()
# Prüfen, ob diese Datei bereits extrahiert wurde
already = self.db.conn.execute(
"SELECT id FROM learn_log WHERE source_file = ? AND source_content_hash = ?",
(str(errors_file), content_hash)
).fetchone()
if already:
return [] # Keine Änderungen seit letzter Extraktion
pattern_ids = []
# Einfache Heuristik: Nach "## Error:" oder "### Lösung:" Blöcken suchen
# Block-Split an Doppel-Newlines
blocks = content.split('\n\n')
for block in blocks:
block = block.strip()
if not block or len(block) < 20:
continue
# Nur Blöcke mit Lösung extrahieren
has_solution = any(kw in block.lower() for kw in ['lösung', 'solution', 'fix', 'behoben', 'resolved'])
if not has_solution:
continue
# Titel aus erster Zeile
lines = block.split('\n')
title = lines[0].lstrip('#').strip()[:100]
if not title:
title = block[:80] + '...' if len(block) > 80 else block
# Pattern speichern
pid = self.db.add_pattern(
title=title,
category='error_fix',
pattern_type='error_solution',
description=block[:500],
code_example=self._extract_code_block(block),
source_project_id=self.project_id,
source_file=str(errors_file),
source_error=title,
validated=0 # Automatisch extrahiert → ungeprüft
)
# Embedding speichern (für Vektor-Suche)
self.db.store_embedding(pid, block[:1000])
# Lern-Log
self.db.log_learning(pid, self.project_id, 'error_fixed',
str(errors_file), content)
pattern_ids.append(pid)
return pattern_ids
def extract_from_decisions(self) -> List[int]:
"""
Extrahiert architecture_decision-Patterns aus decisions.md.
"""
decisions_file = self.a0_path / "decisions.md"
if not decisions_file.exists():
return []
content = decisions_file.read_text(encoding='utf-8')
content_hash = hashlib.md5(content.encode()).hexdigest()
already = self.db.conn.execute(
"SELECT id FROM learn_log WHERE source_file = ? AND source_content_hash = ?",
(str(decisions_file), content_hash)
).fetchone()
if already:
return []
pattern_ids = []
# ADR-Blöcke: ## ADR-001: Titel
adr_blocks = re.split(r'\n## ADR-\d+:', content)[1:] # Skip header
for i, block in enumerate(adr_blocks):
lines = block.strip().split('\n')
title = lines[0].strip() if lines else f"ADR-{i+1}"
# Entscheidung + Begründung extrahieren
decision = ""
rationale = ""
for line in lines:
if 'entscheidung' in line.lower() or 'decision' in line.lower():
decision = line.split(':', 1)[-1].strip() if ':' in line else line
if 'begründung' in line.lower() or 'rationale' in line.lower():
rationale = line.split(':', 1)[-1].strip() if ':' in line else line
description = f"Entscheidung: {decision}\nBegründung: {rationale}" if decision else block[:300]
pid = self.db.add_pattern(
title=title[:100],
category='architecture',
pattern_type='architecture_decision',
description=description,
why_it_works=rationale[:500] if rationale else None,
source_project_id=self.project_id,
source_file=str(decisions_file),
validated=0
)
self.db.store_embedding(pid, description)
self.db.log_learning(pid, self.project_id, 'decision_made',
str(decisions_file), content)
pattern_ids.append(pid)
return pattern_ids
def extract_from_architecture(self) -> List[int]:
"""
Extrahiert Patterns aus architecture.md.
Erkennt: Tech-Stack, Architekturmuster, Docker-Setup.
"""
arch_file = self.a0_path / "architecture.md"
if not arch_file.exists():
return []
content = arch_file.read_text(encoding='utf-8')
content_hash = hashlib.md5(content.encode()).hexdigest()
already = self.db.conn.execute(
"SELECT id FROM learn_log WHERE source_file = ? AND source_content_hash = ?",
(str(arch_file), content_hash)
).fetchone()
if already:
return []
pattern_ids = []
# Nach bekannten Architekturmustern suchen
patterns_to_detect = [
('FastAPI', 'python', 'fastapi', 'best_practice'),
('SQLAlchemy', 'database', 'sqlalchemy', 'best_practice'),
('React', 'frontend', 'react', 'best_practice'),
('Docker', 'docker', None, 'setup_guide'),
('JWT', 'security', None, 'best_practice'),
('REST API', 'architecture', None, 'architecture_decision'),
]
for tech, category, subcat, ptype in patterns_to_detect:
if tech.lower() in content.lower():
# Kontext um das Keyword extrahieren
idx = content.lower().find(tech.lower())
start = max(0, idx - 100)
end = min(len(content), idx + 300)
context = content[start:end].strip()
pid = self.db.add_pattern(
title=f"{tech} in {self.project_name}",
category=category,
subcategory=subcat,
pattern_type=ptype,
description=context,
source_project_id=self.project_id,
source_file=str(arch_file),
validated=0
)
self.db.store_embedding(pid, context)
self.db.log_learning(pid, self.project_id, 'release_audit',
str(arch_file), content)
pattern_ids.append(pid)
return pattern_ids
def extract_from_docker(self) -> List[int]:
"""
Extrahiert Docker-Patterns aus Dockerfile und docker-compose.yml.
"""
pattern_ids = []
dockerfile = self.project_path / "Dockerfile"
compose_file = self.project_path / "docker-compose.yml"
if dockerfile.exists():
content = dockerfile.read_text()
content_hash = hashlib.md5(content.encode()).hexdigest()
already = self.db.conn.execute(
"SELECT id FROM learn_log WHERE source_file = ? AND source_content_hash = ?",
(str(dockerfile), content_hash)
).fetchone()
if not already:
pid = self.db.add_pattern(
title=f"Dockerfile aus {self.project_name}",
category='docker',
pattern_type='setup_guide',
description=f"Docker-Konfiguration für {self.project_name}",
code_example=content,
source_project_id=self.project_id,
source_file=str(dockerfile),
validated=0
)
self.db.store_embedding(pid, content[:1000])
self.db.log_learning(pid, self.project_id, 'deployment_success',
str(dockerfile), content)
pattern_ids.append(pid)
if compose_file.exists():
content = compose_file.read_text()
content_hash = hashlib.md5(content.encode()).hexdigest()
already = self.db.conn.execute(
"SELECT id FROM learn_log WHERE source_file = ? AND source_content_hash = ?",
(str(compose_file), content_hash)
).fetchone()
if not already:
pid = self.db.add_pattern(
title=f"Docker Compose aus {self.project_name}",
category='docker',
pattern_type='deployment_config',
description=f"Docker-Compose-Konfiguration für {self.project_name}",
code_example=content,
source_project_id=self.project_id,
source_file=str(compose_file),
validated=0
)
self.db.store_embedding(pid, content[:1000])
self.db.log_learning(pid, self.project_id, 'deployment_success',
str(compose_file), content)
pattern_ids.append(pid)
return pattern_ids
def extract_from_deployment(self) -> List[int]:
"""
Extrahiert Deployment-Patterns aus runtime_report.md und env.md.
"""
pattern_ids = []
for filename in ['runtime_report.md', 'env.md']:
filepath = self.a0_path / filename
if not filepath.exists():
continue
content = filepath.read_text()
content_hash = hashlib.md5(content.encode()).hexdigest()
already = self.db.conn.execute(
"SELECT id FROM learn_log WHERE source_file = ? AND source_content_hash = ?",
(str(filepath), content_hash)
).fetchone()
if not already:
pid = self.db.add_pattern(
title=f"{filename.replace('.md','')} aus {self.project_name}",
category='deployment',
pattern_type='setup_guide',
description=content[:500],
source_project_id=self.project_id,
source_file=str(filepath),
validated=0
)
self.db.store_embedding(pid, content[:1000])
self.db.log_learning(pid, self.project_id, 'deployment_success',
str(filepath), content)
pattern_ids.append(pid)
return pattern_ids
def extract_all(self) -> Dict[str, List[int]]:
"""
Führt alle Extraktoren aus und gibt Summary zurück.
"""
results = {
'errors': self.extract_from_known_errors(),
'decisions': self.extract_from_decisions(),
'architecture': self.extract_from_architecture(),
'docker': self.extract_from_docker(),
'deployment': self.extract_from_deployment(),
}
# Projekt-Metriken updaten
total = sum(len(v) for v in results.values())
if total > 0:
self.db.conn.execute("""
UPDATE projects
SET patterns_extracted = COALESCE(patterns_extracted, 0) + ?,
last_extraction_at = datetime('now')
WHERE id = ?
""", (total, self.project_id))
self.db.conn.commit()
return results
@staticmethod
def _extract_code_block(text: str) -> Optional[str]:
"""Extrahiert einen Code-Block (```...```) aus Text."""
match = re.search(r'```[\s\S]*?```', text)
if match:
code = match.group(0)
# Markdown-Fences entfernen
code = re.sub(r'^```\w*\n?', '', code)
code = re.sub(r'\n?```$', '', code)
return code.strip()
return None
# ---------------------------------------------------------------------------
# Convenience-Funktion
# ---------------------------------------------------------------------------
def extract_from_project(project_path: str) -> Dict[str, Any]:
"""
Extrahiert Patterns aus einem Projekt und gibt eine Zusammenfassung zurück.
Args:
project_path: Pfad zum Projekt-Root
Returns:
Dict mit Summary der Extraktion
"""
extractor = PatternExtractor(Path(project_path))
results = extractor.extract_all()
total = sum(len(v) for v in results.values())
return {
'project': extractor.project_name,
'project_id': extractor.project_id,
'total_patterns_extracted': total,
'by_category': results,
'timestamp': datetime.now().isoformat()
}
+536
View File
@@ -0,0 +1,536 @@
"""
A0 Software Orchestrator Schema-Migrationen
Verwaltet inkrementelle Updates der patterns.db.
"""
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .db import PatternDB
# ---------------------------------------------------------------------------
# Migrations-Definitionen
# ---------------------------------------------------------------------------
# Key = Versionsnummer, Value = SQL-Statement
# Migrationen werden in aufsteigender Reihenfolge ausgeführt.
MIGRATIONS = {
1: """
-- Version 1: Initiales Schema (siehe schema.sql)
-- Diese Migration ist nur ein Marker das vollständige Schema
-- wird in schema.sql verwaltet und bei neuen DBs ausgeführt.
SELECT 1; -- No-Op, Marker
""",
2: """
-- Version 2: Normalisiertes Projekt-Registry-Schema
-- Teilt projects_registry in drei Tabellen:
-- projects = stabile Identität (name, git_url, description, tech_stack)
-- project_state = laufender Zustand (phase, status, tasks, errors, …)
-- project_snapshots = Historie (state_json, trigger, commit_hash)
-- 2a. Neue Tabellen anlegen
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
git_url TEXT,
description TEXT DEFAULT '',
tech_stack TEXT DEFAULT '{}',
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS project_state (
project_id INTEGER PRIMARY KEY,
status TEXT DEFAULT 'active',
phase TEXT DEFAULT 'intake',
plan_mode TEXT,
total_tasks INTEGER DEFAULT 0,
completed_tasks INTEGER DEFAULT 0,
open_errors INTEGER DEFAULT 0,
last_active_at TEXT,
completed_at TEXT,
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES projects(id)
);
CREATE TABLE IF NOT EXISTS project_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
snapshot_at TEXT DEFAULT (datetime('now')),
state_json TEXT NOT NULL,
trigger TEXT,
commit_hash TEXT,
FOREIGN KEY (project_id) REFERENCES projects(id)
);
-- 2b. Daten aus projects_registry migrieren
INSERT INTO projects (name, git_url, description, tech_stack, created_at)
SELECT project_name, git_url,
COALESCE(description, ''),
COALESCE(tech_stack, '{}'),
COALESCE(created_at, datetime('now'))
FROM projects_registry
WHERE project_name IS NOT NULL;
INSERT INTO project_state (project_id, status, phase, plan_mode, total_tasks,
completed_tasks, open_errors, last_active_at, completed_at)
SELECT p.id, r.status, r.phase, r.plan_mode,
COALESCE(r.total_tasks, 0), COALESCE(r.completed_tasks, 0),
COALESCE(r.open_errors, 0), r.last_active_at, r.completed_at
FROM projects_registry r
JOIN projects p ON r.project_name = p.name;
-- 2c. Alte Tabelle umbenennen (Sicherheit kein DROP)
ALTER TABLE projects_registry RENAME TO projects_registry_legacy;
""",
3: """
-- Version 3: Orchestrator-State-Tabellen (orch_*)
-- Migration: .a0/*.json/.md und .a0proj/handover/*.md → patterns.db
-- Ersetzt file-basierten State durch DB-Tabellen, project-scoped.
-- 3a. Generischer Key/Value Store (JSON-Values)
-- ersetzt: project_state.json, orchestrator_mode.json,
-- task_graph.json, tool_capabilities.json,
-- tool_registry.json, manifests/*.json
CREATE TABLE IF NOT EXISTS orch_kv (
project_id INTEGER NOT NULL,
key TEXT NOT NULL,
value_json TEXT NOT NULL,
updated_at TEXT DEFAULT (datetime('now')),
PRIMARY KEY (project_id, key),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_orch_kv_project ON orch_kv(project_id);
-- 3b. Append-only Worklog
-- ersetzt: .a0/worklog.md
CREATE TABLE IF NOT EXISTS orch_worklog (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
phase TEXT,
work_block TEXT,
agent TEXT,
summary TEXT,
details TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_orch_worklog_project
ON orch_worklog(project_id, created_at DESC);
-- 3c. Todos (offene + abgeschlossene)
-- ersetzt: .a0/todo.md
CREATE TABLE IF NOT EXISTS orch_todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
content TEXT NOT NULL,
status TEXT DEFAULT 'open', -- open, in_progress, done, blocked, cancelled
priority INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
started_at TEXT,
completed_at TEXT,
notes TEXT,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_orch_todos_project
ON orch_todos(project_id, status);
-- 3d. Current Status (1-Zeilen-Projektstatus)
-- ersetzt: .a0/current_status.md
CREATE TABLE IF NOT EXISTS orch_status (
project_id INTEGER PRIMARY KEY,
content TEXT NOT NULL,
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
-- 3e. Next Steps (offene Schritte)
-- ersetzt: .a0/next_steps.md
CREATE TABLE IF NOT EXISTS orch_next_steps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
content TEXT NOT NULL,
status TEXT DEFAULT 'pending', -- pending, in_progress, done, blocked
order_idx INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
completed_at TEXT,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_orch_next_steps_project
ON orch_next_steps(project_id, order_idx);
-- 3f. Known Errors (offene + gelöste)
-- ersetzt: .a0/known_errors.md
CREATE TABLE IF NOT EXISTS orch_errors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
error_id TEXT,
title TEXT NOT NULL,
severity TEXT DEFAULT 'medium', -- low, medium, high, critical
status TEXT DEFAULT 'open', -- open, investigating, resolved, wontfix
context TEXT,
resolution TEXT,
discovered TEXT DEFAULT (datetime('now')),
resolved_at TEXT,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_orch_errors_project
ON orch_errors(project_id, status);
-- 3g. Block-Compact Marker (ersetzt .a0/resume.md + .a0/conversation_summary.md)
CREATE TABLE IF NOT EXISTS orch_block_compact (
project_id INTEGER PRIMARY KEY,
last_block_id TEXT,
last_compact_at TEXT,
next_block TEXT,
block_summary TEXT,
decisions_json TEXT, -- JSON array
key_findings_json TEXT, -- JSON array
open_questions_json TEXT, -- JSON array
resume_md TEXT,
conversation_summary_md TEXT,
context_ratio REAL,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
-- 3h. Session Snapshots (ersetzt .a0/session_snapshots/*.json)
CREATE TABLE IF NOT EXISTS orch_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
snapshot_at TEXT DEFAULT (datetime('now')),
trigger TEXT, -- e.g. 'block_compact', 'phase_transition', 'manual'
commit_hash TEXT,
state_json TEXT NOT NULL,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_orch_snapshots_project
ON orch_snapshots(project_id, snapshot_at DESC);
-- 3i. Scorecard Entries (ersetzt .a0/project_scorecard.md)
CREATE TABLE IF NOT EXISTS orch_scorecard (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
category TEXT NOT NULL,
score INTEGER NOT NULL,
max_score INTEGER DEFAULT 100,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_orch_scorecard_project
ON orch_scorecard(project_id, created_at DESC);
-- 3j. Briefings (ersetzt .a0proj/handover/*.md)
-- Persistente Handover-Briefings für Persona-Modus
CREATE TABLE IF NOT EXISTS orch_briefings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
specialist TEXT NOT NULL,
topic TEXT NOT NULL,
section_md TEXT NOT NULL,
summary TEXT,
decisions_json TEXT,
artifacts_json TEXT,
recent_turns_json TEXT,
return_condition TEXT,
handover_reason TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_orch_briefings_project
ON orch_briefings(project_id, specialist, created_at DESC);
""",
4: """
-- Version 4: Quality gate runtime tables.
CREATE TABLE IF NOT EXISTS orch_quality_config (
project_id INTEGER PRIMARY KEY,
tech_stack TEXT NOT NULL DEFAULT 'unknown',
coverage_min_pct INTEGER DEFAULT 70,
strict_types INTEGER DEFAULT 1,
require_security_scan INTEGER DEFAULT 1,
require_openapi_check INTEGER DEFAULT 0,
custom_checks_json TEXT DEFAULT '[]',
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS orch_quality_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
gate_level TEXT NOT NULL,
decision TEXT NOT NULL,
checks_json TEXT NOT NULL,
blockers_json TEXT NOT NULL,
next_action TEXT,
duration_ms INTEGER DEFAULT 0,
triggered_by TEXT DEFAULT 'manual',
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_orch_quality_runs_project
ON orch_quality_runs(project_id, created_at DESC);
""",
5: """
-- Version 5: Globale Plugin-Settings (kein Project-Scope)
-- Ersetzt den project_id=0-Workaround in orch_kv (plan_mode_guard,
-- siehe Bugfix-Auto-Registration §3.7). plugin_settings hat KEINE
-- FK auf projects, ist also für plugin-weite Konfiguration.
CREATE TABLE IF NOT EXISTS plugin_settings (
key TEXT PRIMARY KEY NOT NULL,
value_json TEXT NOT NULL,
updated_at TEXT DEFAULT (datetime('now'))
);
"""
}
def _table_exists(db: "PatternDB", name: str) -> bool:
row = db.conn.execute(
"SELECT 1 FROM sqlite_master WHERE type IN ('table','view') AND name = ?",
(name,),
).fetchone()
return row is not None
def _column_exists(db: "PatternDB", table: str, column: str) -> bool:
try:
return any(r[1] == column for r in db.conn.execute(f"PRAGMA table_info({table})"))
except Exception:
return False
def _add_column_if_missing(db: "PatternDB", table: str, column: str, ddl: str) -> None:
if _table_exists(db, table) and not _column_exists(db, table, column):
db.conn.execute(f"ALTER TABLE {table} ADD COLUMN {ddl}")
def ensure_runtime_schema(db: "PatternDB") -> None:
"""Idempotently enforce all runtime tables/columns regardless of migration history."""
db.conn.executescript("""
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
git_url TEXT,
description TEXT DEFAULT '',
tech_stack TEXT DEFAULT '{}',
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS project_state (
project_id INTEGER PRIMARY KEY,
status TEXT DEFAULT 'active',
phase TEXT DEFAULT 'intake',
plan_mode TEXT,
total_tasks INTEGER DEFAULT 0,
completed_tasks INTEGER DEFAULT 0,
open_errors INTEGER DEFAULT 0,
last_active_at TEXT,
completed_at TEXT,
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS project_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
snapshot_at TEXT DEFAULT (datetime('now')),
state_json TEXT NOT NULL,
trigger TEXT,
commit_hash TEXT,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS orch_kv (
project_id INTEGER NOT NULL,
key TEXT NOT NULL,
value_json TEXT NOT NULL,
updated_at TEXT DEFAULT (datetime('now')),
PRIMARY KEY (project_id, key),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS orch_worklog (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
phase TEXT,
work_block TEXT,
agent TEXT,
summary TEXT,
details TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS orch_todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
content TEXT NOT NULL,
status TEXT DEFAULT 'open',
priority INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
started_at TEXT,
completed_at TEXT,
notes TEXT,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS orch_status (
project_id INTEGER PRIMARY KEY,
content TEXT NOT NULL,
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS orch_next_steps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
content TEXT NOT NULL,
status TEXT DEFAULT 'pending',
order_idx INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
completed_at TEXT,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS orch_errors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
error_id TEXT,
title TEXT NOT NULL,
severity TEXT DEFAULT 'medium',
status TEXT DEFAULT 'open',
context TEXT,
resolution TEXT,
discovered TEXT DEFAULT (datetime('now')),
resolved_at TEXT,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS orch_block_compact (
project_id INTEGER PRIMARY KEY,
last_block_id TEXT,
last_compact_at TEXT,
next_block TEXT,
block_summary TEXT,
decisions_json TEXT,
key_findings_json TEXT,
open_questions_json TEXT,
resume_md TEXT,
conversation_summary_md TEXT,
context_ratio REAL,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS orch_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
snapshot_at TEXT DEFAULT (datetime('now')),
trigger TEXT,
commit_hash TEXT,
state_json TEXT NOT NULL,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS orch_scorecard (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
category TEXT NOT NULL,
score INTEGER NOT NULL,
max_score INTEGER DEFAULT 100,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS orch_briefings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
specialist TEXT NOT NULL,
topic TEXT NOT NULL,
section_md TEXT NOT NULL,
summary TEXT,
decisions_json TEXT,
artifacts_json TEXT,
recent_turns_json TEXT,
return_condition TEXT,
handover_reason TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS orch_quality_config (
project_id INTEGER PRIMARY KEY,
tech_stack TEXT NOT NULL DEFAULT 'unknown',
coverage_min_pct INTEGER DEFAULT 70,
strict_types INTEGER DEFAULT 1,
require_security_scan INTEGER DEFAULT 1,
require_openapi_check INTEGER DEFAULT 0,
custom_checks_json TEXT DEFAULT '[]',
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS orch_quality_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
gate_level TEXT NOT NULL,
decision TEXT NOT NULL,
checks_json TEXT NOT NULL,
blockers_json TEXT NOT NULL,
next_action TEXT,
duration_ms INTEGER DEFAULT 0,
triggered_by TEXT DEFAULT 'manual',
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
""")
for ddl in [
"project_path TEXT",
"patterns_extracted INTEGER DEFAULT 0",
"last_extraction_at TEXT",
]:
col = ddl.split()[0]
_add_column_if_missing(db, "projects", col, ddl)
db.conn.executescript("""
CREATE INDEX IF NOT EXISTS idx_projects_name ON projects(name);
CREATE INDEX IF NOT EXISTS idx_project_state_status ON project_state(status, phase);
CREATE INDEX IF NOT EXISTS idx_orch_worklog_project ON orch_worklog(project_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_orch_todos_project ON orch_todos(project_id, status);
CREATE INDEX IF NOT EXISTS idx_orch_next_steps_project ON orch_next_steps(project_id, order_idx);
CREATE INDEX IF NOT EXISTS idx_orch_errors_project ON orch_errors(project_id, status);
CREATE INDEX IF NOT EXISTS idx_orch_snapshots_project ON orch_snapshots(project_id, snapshot_at DESC);
CREATE INDEX IF NOT EXISTS idx_orch_scorecard_project ON orch_scorecard(project_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_orch_briefings_project ON orch_briefings(project_id, specialist, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_orch_quality_runs_project ON orch_quality_runs(project_id, created_at DESC);
""")
db.conn.commit()
def run_migrations(db: "PatternDB"):
"""
Führt ausstehende Schema-Migrationen aus.
Args:
db: PatternDB-Instanz
"""
# Aktuelle Schema-Version ermitteln
cursor = db.conn.execute(
"SELECT MAX(version) FROM schema_version"
)
current = cursor.fetchone()[0] or 0
# Fehlende Migrationen in Reihenfolge ausführen
for version in sorted(MIGRATIONS.keys()):
if version <= current:
continue
try:
db.conn.executescript(MIGRATIONS[version])
db.conn.execute(
"INSERT INTO schema_version (version, description) VALUES (?, ?)",
(version, f"Migration {version}")
)
db.conn.commit()
print(f"[PatternDB] ✅ Migration {version} ausgeführt")
except Exception as e:
print(f"[PatternDB] ❌ Migration {version} fehlgeschlagen: {e}")
raise
ensure_runtime_schema(db)
+305
View File
@@ -0,0 +1,305 @@
-- ============================================================
-- A0 Software Orchestrator Patterns-Bibliothek
-- SQLite Schema v1.0.0
-- ============================================================
-- -----------------------------------------------------------
-- 1. PROJEKT-REGISTRY (zentrale Projekt-Liste)
-- -----------------------------------------------------------
CREATE TABLE IF NOT EXISTS projects_registry (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- Identifikation
project_name TEXT NOT NULL UNIQUE,
project_path TEXT NOT NULL,
git_url TEXT,
-- Status
status TEXT NOT NULL DEFAULT 'active',
-- active, paused, completed, archived, failed
phase TEXT,
-- intake, requirements, architecture, implementation,
-- testing, deployment, maintenance
plan_mode TEXT,
-- planning_only, implementation_allowed,
-- runtime_verification_allowed, deployment_preparation_allowed,
-- release_handoff_allowed, maintenance_allowed
-- Tech-Stack (JSON)
tech_stack TEXT,
-- {"backend":"FastAPI","frontend":"React","db":"SQLite",
-- "orm":"SQLAlchemy","docker":true}
-- Metriken
total_tasks INTEGER DEFAULT 0,
completed_tasks INTEGER DEFAULT 0,
open_errors INTEGER DEFAULT 0,
-- Zeitstempel
created_at TEXT DEFAULT (datetime('now')),
started_at TEXT,
last_active_at TEXT DEFAULT (datetime('now')),
completed_at TEXT,
-- Verknüpfung zur Patterns-Bibliothek
patterns_extracted INTEGER DEFAULT 0,
last_extraction_at TEXT,
-- Notizen
description TEXT,
notes TEXT
);
CREATE INDEX IF NOT EXISTS idx_projects_status ON projects_registry(status, phase);
CREATE INDEX IF NOT EXISTS idx_projects_active ON projects_registry(last_active_at);
CREATE INDEX IF NOT EXISTS idx_projects_tech ON projects_registry(tech_stack);
-- -----------------------------------------------------------
-- 2. PATTERNS (das eigentliche Wissen)
-- -----------------------------------------------------------
CREATE TABLE IF NOT EXISTS patterns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- Basis
title TEXT NOT NULL,
category TEXT NOT NULL,
-- docker, python, frontend, deployment, error_fix,
-- architecture, framework, workflow, database, security
subcategory TEXT,
-- z.B. 'fastapi', 'react', 'sqlalchemy', 'postgresql'
pattern_type TEXT NOT NULL,
-- code_snippet, architecture_decision, error_solution,
-- best_practice, setup_guide, deployment_config,
-- workflow_pattern, anti_pattern
-- Inhalt
description TEXT NOT NULL,
code_example TEXT,
when_to_use TEXT,
why_it_works TEXT,
pitfalls TEXT,
-- Meta
source_project_id INTEGER REFERENCES projects_registry(id),
source_task_id TEXT,
source_error TEXT,
source_file TEXT,
-- Qualität & Validierung
validated INTEGER DEFAULT 0,
-- 0 = automatisch extrahiert, ungeprüft
-- 1 = durch quality_reviewer bestätigt
-- 2 = manuell erstellt/geprüft
-- -1 = veraltet/deprecated
validation_date TEXT,
validated_by TEXT,
-- Nutzungs-Statistik
usage_count INTEGER DEFAULT 0,
success_count INTEGER DEFAULT 0,
failure_count INTEGER DEFAULT 0,
success_rate REAL DEFAULT 0.0,
-- Aging
staleness_score REAL DEFAULT 0.0,
-- 0.0 = frisch, 1.0 = stark veraltet
framework_version TEXT,
-- z.B. "FastAPI 0.115", "React 19", "Python 3.12"
-- Zeitstempel
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
last_used_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_patterns_category ON patterns(category, pattern_type);
CREATE INDEX IF NOT EXISTS idx_patterns_project ON patterns(source_project_id);
CREATE INDEX IF NOT EXISTS idx_patterns_valid ON patterns(validated);
CREATE INDEX IF NOT EXISTS idx_patterns_stale ON patterns(staleness_score);
-- -----------------------------------------------------------
-- 3. TAGS (flexible Verschlagwortung)
-- -----------------------------------------------------------
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL
);
CREATE TABLE IF NOT EXISTS pattern_tags (
pattern_id INTEGER REFERENCES patterns(id) ON DELETE CASCADE,
tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (pattern_id, tag_id)
);
-- -----------------------------------------------------------
-- 4. PATTERN-BEZIEHUNGEN
-- -----------------------------------------------------------
CREATE TABLE IF NOT EXISTS pattern_relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pattern_a_id INTEGER REFERENCES patterns(id) ON DELETE CASCADE,
pattern_b_id INTEGER REFERENCES patterns(id) ON DELETE CASCADE,
relation_type TEXT NOT NULL,
-- 'often_used_with', 'alternative_to', 'depends_on',
-- 'replaces', 'is_replaced_by', 'conflicts_with'
strength REAL DEFAULT 1.0,
notes TEXT,
created_at TEXT DEFAULT (datetime('now')),
UNIQUE(pattern_a_id, pattern_b_id, relation_type)
);
-- -----------------------------------------------------------
-- 5. PATTERN-KONFLIKTE (widersprüchliche Patterns)
-- -----------------------------------------------------------
CREATE TABLE IF NOT EXISTS pattern_conflicts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pattern_a_id INTEGER REFERENCES patterns(id) ON DELETE CASCADE,
pattern_b_id INTEGER REFERENCES patterns(id) ON DELETE CASCADE,
conflict_type TEXT NOT NULL,
-- 'direct_contradiction', 'context_dependent', 'version_specific'
description TEXT,
resolution_context TEXT,
-- JSON: {"if": "high_traffic", "use": "A", "if": "simple", "use": "B"}
resolved_by TEXT DEFAULT 'system',
-- 'system', 'quality_reviewer', 'manual'
resolved_at TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- -----------------------------------------------------------
-- 6. PATTERN-FEEDBACK (negative + positive Lernerfahrungen)
-- -----------------------------------------------------------
CREATE TABLE IF NOT EXISTS pattern_feedback (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pattern_id INTEGER REFERENCES patterns(id) ON DELETE CASCADE,
project_id INTEGER REFERENCES projects_registry(id),
outcome TEXT NOT NULL,
-- 'success', 'failure', 'partial_success', 'not_applicable'
reason TEXT,
context_diff TEXT,
-- Was war anders als beim ursprünglichen Pattern?
-- Bei Fehlschlag: Was wurde stattdessen verwendet?
alternative_used TEXT,
alternative_pattern_id INTEGER REFERENCES patterns(id),
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_feedback_pattern ON pattern_feedback(pattern_id);
CREATE INDEX IF NOT EXISTS idx_feedback_project ON pattern_feedback(project_id);
-- -----------------------------------------------------------
-- 7. LERN-LOG (Historie für Debugging)
-- -----------------------------------------------------------
CREATE TABLE IF NOT EXISTS learn_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pattern_id INTEGER REFERENCES patterns(id),
project_id INTEGER REFERENCES projects_registry(id),
trigger TEXT NOT NULL,
-- 'task_completed', 'error_fixed', 'deployment_success',
-- 'decision_made', 'release_audit', 'manual_entry'
source_file TEXT,
source_content_hash TEXT,
extracted_at TEXT DEFAULT (datetime('now'))
);
-- -----------------------------------------------------------
-- 8. SCHEMA-VERSION (Migration-Tracking)
-- -----------------------------------------------------------
CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY,
description TEXT,
applied_at TEXT DEFAULT (datetime('now'))
);
-- Initiale Version eintragen
INSERT OR IGNORE INTO schema_version (version, description)
VALUES (1, 'Initiales Schema: patterns, projects_registry, tags, relations, conflicts, feedback, learn_log');
-- -----------------------------------------------------------
-- 9. FTS5 VOLLTEXT-INDEX
-- -----------------------------------------------------------
CREATE VIRTUAL TABLE IF NOT EXISTS patterns_fts USING fts5(
title,
description,
code_example,
when_to_use,
why_it_works,
pitfalls,
content='patterns',
content_rowid='id'
);
-- Trigger: Automatische FTS-Synchronisation
CREATE TRIGGER IF NOT EXISTS patterns_ai AFTER INSERT ON patterns BEGIN
INSERT INTO patterns_fts(rowid, title, description, code_example, when_to_use, why_it_works, pitfalls)
VALUES (new.id, new.title, new.description, new.code_example, new.when_to_use, new.why_it_works, new.pitfalls);
END;
CREATE TRIGGER IF NOT EXISTS patterns_ad AFTER DELETE ON patterns BEGIN
INSERT INTO patterns_fts(patterns_fts, rowid, title, description, code_example, when_to_use, why_it_works, pitfalls)
VALUES ('delete', old.id, old.title, old.description, old.code_example, old.when_to_use, old.why_it_works, old.pitfalls);
END;
CREATE TRIGGER IF NOT EXISTS patterns_au AFTER UPDATE ON patterns BEGIN
INSERT INTO patterns_fts(patterns_fts, rowid, title, description, code_example, when_to_use, why_it_works, pitfalls)
VALUES ('delete', old.id, old.title, old.description, old.code_example, old.when_to_use, old.why_it_works, old.pitfalls);
INSERT INTO patterns_fts(rowid, title, description, code_example, when_to_use, why_it_works, pitfalls)
VALUES (new.id, new.title, new.description, new.code_example, new.when_to_use, new.why_it_works, new.pitfalls);
END;
-- -----------------------------------------------------------
-- 10. VEKTOR-TABELLE (sqlite-vec, optional)
-- -----------------------------------------------------------
-- Wird von db.py dynamisch erstellt, wenn sqlite-vec verfügbar ist.
-- CREATE VIRTUAL TABLE IF NOT EXISTS pattern_embeddings USING vec0(
-- embedding float[384] -- all-MiniLM-L6-v2 (Standard-Modell)
-- );
-- ============================================================
-- NÜTZLICHE VIEWS
-- ============================================================
-- Aktive Projekt-Übersicht
CREATE VIEW IF NOT EXISTS v_active_projects AS
SELECT
project_name,
status,
phase,
completed_tasks || '/' || total_tasks AS progress,
open_errors,
patterns_extracted,
last_active_at
FROM projects_registry
WHERE status = 'active'
ORDER BY last_active_at DESC;
-- Validierten Patterns mit Nutzungsstatistik
CREATE VIEW IF NOT EXISTS v_ready_patterns AS
SELECT
p.id, p.title, p.category, p.pattern_type,
p.usage_count, p.success_count, p.failure_count,
CASE WHEN p.success_count + p.failure_count > 0
THEN ROUND(p.success_count * 100.0 / (p.success_count + p.failure_count), 1)
ELSE NULL END AS success_pct,
p.staleness_score,
p.created_at, p.last_used_at
FROM patterns p
WHERE p.validated >= 0 -- Nur aktive, nicht deprecated
ORDER BY p.usage_count DESC;
-- Konflikt-Übersicht
CREATE VIEW IF NOT EXISTS v_conflicts AS
SELECT
pc.id,
a.title AS pattern_a,
b.title AS pattern_b,
pc.conflict_type,
pc.resolution_context,
pc.resolved_by
FROM pattern_conflicts pc
JOIN patterns a ON pc.pattern_a_id = a.id
JOIN patterns b ON pc.pattern_b_id = b.id;