Files
a0_software_orchestrator/helpers/library/migrations.py
T
Software Orchestrator 5769c1cd22 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
2026-06-16 22:13:06 +00:00

537 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)