kin/core/db.py

286 lines
9 KiB
Python
Raw Normal View History

"""
Kin SQLite database schema and connection management.
All tables from DESIGN.md section 3.5 State Management.
"""
import sqlite3
from pathlib import Path
DB_PATH = Path(__file__).parent.parent / "kin.db"
SCHEMA = """
-- Проекты (центральный реестр)
CREATE TABLE IF NOT EXISTS projects (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
path TEXT NOT NULL,
tech_stack JSON,
status TEXT DEFAULT 'active',
priority INTEGER DEFAULT 5,
pm_prompt TEXT,
claude_md_path TEXT,
forgejo_repo TEXT,
language TEXT DEFAULT 'ru',
execution_mode TEXT NOT NULL DEFAULT 'review',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Задачи (привязаны к проекту)
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL REFERENCES projects(id),
title TEXT NOT NULL,
status TEXT DEFAULT 'pending',
priority INTEGER DEFAULT 5,
assigned_role TEXT,
parent_task_id TEXT REFERENCES tasks(id),
brief JSON,
spec JSON,
review JSON,
test_result JSON,
security_result JSON,
forgejo_issue_id INTEGER,
execution_mode TEXT,
blocked_reason TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Решения и грабли (внешняя память PM-агента)
CREATE TABLE IF NOT EXISTS decisions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id TEXT NOT NULL REFERENCES projects(id),
task_id TEXT REFERENCES tasks(id),
type TEXT NOT NULL,
category TEXT,
title TEXT NOT NULL,
description TEXT NOT NULL,
tags JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Логи агентов (дебаг, обучение, cost tracking)
CREATE TABLE IF NOT EXISTS agent_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id TEXT NOT NULL REFERENCES projects(id),
task_id TEXT REFERENCES tasks(id),
agent_role TEXT NOT NULL,
session_id TEXT,
action TEXT NOT NULL,
input_summary TEXT,
output_summary TEXT,
tokens_used INTEGER,
model TEXT,
cost_usd REAL,
success BOOLEAN,
error_message TEXT,
duration_seconds INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Модули проекта (карта для PM)
CREATE TABLE IF NOT EXISTS modules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id TEXT NOT NULL REFERENCES projects(id),
name TEXT NOT NULL,
type TEXT NOT NULL,
path TEXT NOT NULL,
description TEXT,
owner_role TEXT,
dependencies JSON,
UNIQUE(project_id, name)
);
-- Pipelines (история запусков)
CREATE TABLE IF NOT EXISTS pipelines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL REFERENCES tasks(id),
project_id TEXT NOT NULL REFERENCES projects(id),
route_type TEXT NOT NULL,
steps JSON NOT NULL,
status TEXT DEFAULT 'running',
total_cost_usd REAL,
total_tokens INTEGER,
total_duration_seconds INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME
);
-- Post-pipeline хуки
CREATE TABLE IF NOT EXISTS hooks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id TEXT NOT NULL REFERENCES projects(id),
name TEXT NOT NULL,
event TEXT NOT NULL,
trigger_module_path TEXT,
trigger_module_type TEXT,
command TEXT NOT NULL,
working_dir TEXT,
timeout_seconds INTEGER DEFAULT 120,
enabled INTEGER DEFAULT 1,
created_at TEXT DEFAULT (datetime('now'))
);
-- Лог выполнений хуков
CREATE TABLE IF NOT EXISTS hook_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hook_id INTEGER NOT NULL REFERENCES hooks(id),
project_id TEXT NOT NULL REFERENCES projects(id),
task_id TEXT,
success INTEGER NOT NULL,
exit_code INTEGER,
output TEXT,
error TEXT,
duration_seconds REAL,
created_at TEXT DEFAULT (datetime('now'))
);
-- Кросс-проектные зависимости
CREATE TABLE IF NOT EXISTS project_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_project TEXT NOT NULL REFERENCES projects(id),
to_project TEXT NOT NULL REFERENCES projects(id),
type TEXT NOT NULL,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Тикеты от пользователей
CREATE TABLE IF NOT EXISTS support_tickets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id TEXT NOT NULL REFERENCES projects(id),
source TEXT NOT NULL,
client_id TEXT,
client_message TEXT NOT NULL,
classification TEXT,
guard_result TEXT,
guard_reason TEXT,
anamnesis JSON,
task_id TEXT REFERENCES tasks(id),
response TEXT,
response_approved BOOLEAN DEFAULT FALSE,
status TEXT DEFAULT 'new',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
resolved_at DATETIME
);
-- Настройки бота для каждого проекта
CREATE TABLE IF NOT EXISTS support_bot_config (
project_id TEXT PRIMARY KEY REFERENCES projects(id),
telegram_bot_token TEXT,
welcome_message TEXT,
faq JSON,
auto_reply BOOLEAN DEFAULT FALSE,
require_approval BOOLEAN DEFAULT TRUE,
brand_voice TEXT,
forbidden_topics JSON,
escalation_keywords JSON
);
-- Индексы
CREATE INDEX IF NOT EXISTS idx_tasks_project_status ON tasks(project_id, status);
CREATE INDEX IF NOT EXISTS idx_decisions_project ON decisions(project_id);
CREATE INDEX IF NOT EXISTS idx_decisions_tags ON decisions(tags);
CREATE INDEX IF NOT EXISTS idx_agent_logs_project ON agent_logs(project_id, created_at);
CREATE INDEX IF NOT EXISTS idx_agent_logs_cost ON agent_logs(project_id, cost_usd);
CREATE INDEX IF NOT EXISTS idx_tickets_project ON support_tickets(project_id, status);
CREATE INDEX IF NOT EXISTS idx_tickets_client ON support_tickets(client_id);
"""
def get_connection(db_path: Path = DB_PATH) -> sqlite3.Connection:
conn = sqlite3.connect(str(db_path))
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
conn.row_factory = sqlite3.Row
return conn
def _migrate(conn: sqlite3.Connection):
"""Run migrations for existing databases."""
# Check if language column exists on projects
proj_cols = {r[1] for r in conn.execute("PRAGMA table_info(projects)").fetchall()}
if "language" not in proj_cols:
conn.execute("ALTER TABLE projects ADD COLUMN language TEXT DEFAULT 'ru'")
conn.commit()
if "execution_mode" not in proj_cols:
conn.execute("ALTER TABLE projects ADD COLUMN execution_mode TEXT NOT NULL DEFAULT 'review'")
conn.commit()
# Check if execution_mode column exists on tasks
task_cols = {r[1] for r in conn.execute("PRAGMA table_info(tasks)").fetchall()}
if "execution_mode" not in task_cols:
conn.execute("ALTER TABLE tasks ADD COLUMN execution_mode TEXT")
conn.commit()
if "blocked_reason" not in task_cols:
conn.execute("ALTER TABLE tasks ADD COLUMN blocked_reason TEXT")
conn.commit()
if "autocommit_enabled" not in proj_cols:
conn.execute("ALTER TABLE projects ADD COLUMN autocommit_enabled INTEGER DEFAULT 0")
conn.commit()
# Rename legacy 'auto' → 'auto_complete' (KIN-063)
conn.execute(
"UPDATE projects SET execution_mode = 'auto_complete' WHERE execution_mode = 'auto'"
)
conn.execute(
"UPDATE tasks SET execution_mode = 'auto_complete' WHERE execution_mode = 'auto'"
)
conn.commit()
def _seed_default_hooks(conn: sqlite3.Connection):
"""Seed default hooks for the kin project (idempotent).
Creates rebuild-frontend hook only when:
- project 'kin' exists in the projects table
- the hook doesn't already exist (no duplicate)
"""
kin_exists = conn.execute(
"SELECT 1 FROM projects WHERE id = 'kin'"
).fetchone()
if not kin_exists:
return
exists = conn.execute(
"SELECT 1 FROM hooks"
" WHERE project_id = 'kin'"
" AND name = 'rebuild-frontend'"
" AND event = 'pipeline_completed'"
).fetchone()
if not exists:
conn.execute(
"""INSERT INTO hooks (project_id, name, event, command, enabled)
VALUES ('kin', 'rebuild-frontend', 'pipeline_completed',
'cd /Users/grosfrumos/projects/kin/web/frontend && npm run build',
1)"""
)
conn.commit()
# Enable autocommit for kin project (opt-in, idempotent)
conn.execute(
"UPDATE projects SET autocommit_enabled=1 WHERE id='kin' AND autocommit_enabled=0"
)
conn.commit()
def init_db(db_path: Path = DB_PATH) -> sqlite3.Connection:
conn = get_connection(db_path)
conn.executescript(SCHEMA)
conn.commit()
_migrate(conn)
_seed_default_hooks(conn)
return conn
if __name__ == "__main__":
conn = init_db()
tables = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
).fetchall()
print(f"Initialized {len(tables)} tables:")
for t in tables:
print(f" - {t['name']}")
conn.close()