292 lines
15 KiB
SQL
292 lines
15 KiB
SQL
|
|
-- ============================================================================
|
||
|
|
-- ctxd — Context Daemon Schema
|
||
|
|
-- SQLite 3.x, WAL mode, FTS5
|
||
|
|
-- ============================================================================
|
||
|
|
|
||
|
|
-- WAL for concurrent reads during writes; foreign keys enforced
|
||
|
|
PRAGMA journal_mode = WAL;
|
||
|
|
PRAGMA foreign_keys = ON;
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- USERS
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE users (
|
||
|
|
user_id TEXT PRIMARY KEY, -- uuid or "joshua", "polly", "hermes-gateway"
|
||
|
|
display_name TEXT NOT NULL,
|
||
|
|
role TEXT NOT NULL DEFAULT 'contributor'
|
||
|
|
CHECK (role IN ('admin', 'contributor', 'service')),
|
||
|
|
token_hash TEXT, -- NULL = no auth (localhost/trusted)
|
||
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE UNIQUE INDEX idx_users_lower ON users (LOWER(user_id));
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- PROJECTS
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE projects (
|
||
|
|
project_id TEXT PRIMARY KEY, -- uuid or slug "remote-rig"
|
||
|
|
display_name TEXT NOT NULL,
|
||
|
|
description TEXT,
|
||
|
|
metadata_tags TEXT DEFAULT '[]', -- JSON array of tag strings e.g. '["ARCHITECTURE","3D-PRINTING"]'
|
||
|
|
shared_version INTEGER NOT NULL DEFAULT 0, -- monotonically increasing
|
||
|
|
auto_sync INTEGER NOT NULL DEFAULT 0, -- boolean: auto-write AGENTS.md to sync_path
|
||
|
|
sync_path TEXT, -- absolute path to project root (nullable)
|
||
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||
|
|
);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- PROJECT PERMISSIONS (admin overrides all)
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE project_permissions (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
|
||
|
|
user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
|
||
|
|
permission TEXT NOT NULL DEFAULT 'editor'
|
||
|
|
CHECK (permission IN ('owner', 'editor', 'viewer')),
|
||
|
|
UNIQUE(project_id, user_id)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- USER PROFILES (personal context — timezone, preferences, style)
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE user_profiles (
|
||
|
|
user_id TEXT PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE,
|
||
|
|
content TEXT NOT NULL DEFAULT '', -- markdown
|
||
|
|
version INTEGER NOT NULL DEFAULT 1,
|
||
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||
|
|
);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- PROJECT CONTEXT — THE AUTHORITATIVE SHARED COPY
|
||
|
|
-- This is the compiled markdown: context.md + decisions/ + runbooks/
|
||
|
|
-- that gets served to agents.
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE project_context (
|
||
|
|
project_id TEXT PRIMARY KEY REFERENCES projects(project_id) ON DELETE CASCADE,
|
||
|
|
content TEXT NOT NULL DEFAULT '', -- compiled markdown
|
||
|
|
version INTEGER NOT NULL DEFAULT 0, -- mirrors projects.shared_version
|
||
|
|
updated_by TEXT REFERENCES users(user_id),
|
||
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||
|
|
);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- CONTEXT FILES — individual files within a project (decisions/, runbooks/)
|
||
|
|
-- The daemon compiles these into project_context.content on demand.
|
||
|
|
-- version tracks this file's edit count (independent of the shared version).
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE context_files (
|
||
|
|
file_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
|
||
|
|
file_path TEXT NOT NULL, -- "decisions/001-use-go.md"
|
||
|
|
content TEXT NOT NULL DEFAULT '',
|
||
|
|
version INTEGER NOT NULL DEFAULT 1, -- per-file edit counter
|
||
|
|
updated_by TEXT REFERENCES users(user_id),
|
||
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
|
|
UNIQUE(project_id, file_path)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- USER WORKSPACES — per-user forks of a project
|
||
|
|
-- When a user edits, they work on their own copy. base_version tracks which
|
||
|
|
-- shared version they started from. current_version tracks their edits.
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE user_workspaces (
|
||
|
|
workspace_id TEXT PRIMARY KEY, -- uuid
|
||
|
|
user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
|
||
|
|
project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
|
||
|
|
status TEXT NOT NULL DEFAULT 'in_progress'
|
||
|
|
CHECK (status IN ('in_progress', 'submitted', 'merged', 'abandoned')),
|
||
|
|
base_version INTEGER NOT NULL, -- shared version at fork time
|
||
|
|
current_version INTEGER NOT NULL DEFAULT 1,
|
||
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
|
|
UNIQUE(user_id, project_id, status) -- one active workspace per user per project
|
||
|
|
);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- WORKSPACE FILES — per-user fork of context_files
|
||
|
|
-- Mirrors the same file_path as context_files but in the user's workspace.
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE workspace_files (
|
||
|
|
file_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
workspace_id TEXT NOT NULL REFERENCES user_workspaces(workspace_id) ON DELETE CASCADE,
|
||
|
|
file_path TEXT NOT NULL,
|
||
|
|
content TEXT NOT NULL DEFAULT '',
|
||
|
|
version INTEGER NOT NULL DEFAULT 1,
|
||
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
|
|
UNIQUE(workspace_id, file_path)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- CHANGE REQUESTS — submit / review / merge workflow
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE change_requests (
|
||
|
|
request_id TEXT PRIMARY KEY, -- uuid
|
||
|
|
workspace_id TEXT NOT NULL REFERENCES user_workspaces(workspace_id) ON DELETE CASCADE,
|
||
|
|
project_id TEXT NOT NULL REFERENCES projects(project_id),
|
||
|
|
submitted_by TEXT NOT NULL REFERENCES users(user_id),
|
||
|
|
status TEXT NOT NULL DEFAULT 'pending'
|
||
|
|
CHECK (status IN ('pending', 'approved', 'rejected', 'merged')),
|
||
|
|
-- Snapshot of what changed, stored inline so reviews survive workspace mutation
|
||
|
|
diff_summary TEXT, -- free-text summary of changes
|
||
|
|
target_version INTEGER NOT NULL, -- the shared version this would bump to
|
||
|
|
base_version INTEGER NOT NULL, -- the shared version they forked from
|
||
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||
|
|
);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- REVIEWS — approvals/rejections on change requests
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE reviews (
|
||
|
|
review_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
request_id TEXT NOT NULL REFERENCES change_requests(request_id) ON DELETE CASCADE,
|
||
|
|
reviewer_id TEXT NOT NULL REFERENCES users(user_id),
|
||
|
|
decision TEXT NOT NULL CHECK (decision IN ('approved', 'rejected')),
|
||
|
|
comments TEXT,
|
||
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||
|
|
UNIQUE(request_id, reviewer_id)
|
||
|
|
);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- SNAPSHOTS — point-in-time copies of project or workspace content
|
||
|
|
-- Stored as files on disk at the path in storage_path.
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE snapshots (
|
||
|
|
snapshot_id TEXT PRIMARY KEY, -- uuid
|
||
|
|
project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
|
||
|
|
-- NULL user_id = snapshot of the shared copy; non-NULL = snapshot of a user workspace
|
||
|
|
user_id TEXT REFERENCES users(user_id) ON DELETE CASCADE,
|
||
|
|
workspace_id TEXT REFERENCES user_workspaces(workspace_id) ON DELETE SET NULL,
|
||
|
|
version_from INTEGER, -- version range this snapshot covers
|
||
|
|
version_to INTEGER,
|
||
|
|
storage_path TEXT NOT NULL, -- relative to ~/.ctx/snapshots/
|
||
|
|
content_hash TEXT NOT NULL, -- sha256 of the compiled markdown
|
||
|
|
size_bytes INTEGER NOT NULL DEFAULT 0,
|
||
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Index for snapshot rotation queries
|
||
|
|
CREATE INDEX idx_snapshots_cleanup ON snapshots (project_id, user_id, created_at);
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- AUDIT LOG — append-only (INSERT only, never UPDATE or DELETE)
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE TABLE audit_log (
|
||
|
|
entry_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
user_id TEXT NOT NULL REFERENCES users(user_id),
|
||
|
|
agent_id TEXT NOT NULL DEFAULT 'cli', -- "hermes", "claude-code", "ctx"
|
||
|
|
session_id TEXT, -- opaque session identifier
|
||
|
|
project_id TEXT REFERENCES projects(project_id),
|
||
|
|
operation TEXT NOT NULL
|
||
|
|
CHECK (operation IN (
|
||
|
|
'read', 'update', 'create', 'delete',
|
||
|
|
'submit', 'approve', 'reject', 'merge',
|
||
|
|
'sync', 'search', 'export', 'restore',
|
||
|
|
'login', 'logout', 'import'
|
||
|
|
)),
|
||
|
|
entity_type TEXT, -- 'project', 'workspace', 'change_request', 'snapshot', 'user_profile'
|
||
|
|
entity_id TEXT, -- polymorphic reference
|
||
|
|
summary TEXT NOT NULL, -- human-readable: "Updated camera-node wiring section"
|
||
|
|
details_json TEXT, -- structured payload: diff, version numbers, etc.
|
||
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Audit queries by user, project, or time range
|
||
|
|
CREATE INDEX idx_audit_user ON audit_log (user_id, created_at);
|
||
|
|
CREATE INDEX idx_audit_project ON audit_log (project_id, created_at);
|
||
|
|
CREATE INDEX idx_audit_agent ON audit_log (agent_id, created_at);
|
||
|
|
CREATE INDEX idx_audit_op ON audit_log (operation, created_at);
|
||
|
|
|
||
|
|
-- Trigger: audit_log is append-only — enforce no updates or deletes at the DB level
|
||
|
|
CREATE TRIGGER tr_audit_log_no_update
|
||
|
|
BEFORE UPDATE ON audit_log
|
||
|
|
BEGIN
|
||
|
|
SELECT RAISE(ABORT, 'audit_log is append-only — no updates allowed');
|
||
|
|
END;
|
||
|
|
|
||
|
|
CREATE TRIGGER tr_audit_log_no_delete
|
||
|
|
BEFORE DELETE ON audit_log
|
||
|
|
BEGIN
|
||
|
|
SELECT RAISE(ABORT, 'audit_log is append-only — no deletes allowed');
|
||
|
|
END;
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- FULL-TEXT SEARCH (FTS5)
|
||
|
|
-- ============================================================================
|
||
|
|
CREATE VIRTUAL TABLE fts_context USING fts5(
|
||
|
|
content,
|
||
|
|
project_id UNINDEXED,
|
||
|
|
file_path UNINDEXED,
|
||
|
|
source_type UNINDEXED, -- 'project_context', 'context_file', 'user_profile', 'workspace_file'
|
||
|
|
tokenize='porter unicode61'
|
||
|
|
);
|
||
|
|
|
||
|
|
-- Triggers to keep FTS index in sync with project_context
|
||
|
|
CREATE TRIGGER tr_fts_project_context_insert AFTER INSERT ON project_context
|
||
|
|
BEGIN
|
||
|
|
INSERT INTO fts_context(rowid, content, project_id, file_path, source_type)
|
||
|
|
VALUES (NEW.rowid, NEW.content, NEW.project_id, 'context.md', 'project_context');
|
||
|
|
END;
|
||
|
|
|
||
|
|
CREATE TRIGGER tr_fts_project_context_update AFTER UPDATE ON project_context
|
||
|
|
BEGIN
|
||
|
|
DELETE FROM fts_context WHERE rowid = OLD.rowid;
|
||
|
|
INSERT INTO fts_context(rowid, content, project_id, file_path, source_type)
|
||
|
|
VALUES (NEW.rowid, NEW.content, NEW.project_id, 'context.md', 'project_context');
|
||
|
|
END;
|
||
|
|
|
||
|
|
CREATE TRIGGER tr_fts_project_context_delete AFTER DELETE ON project_context
|
||
|
|
BEGIN
|
||
|
|
DELETE FROM fts_context WHERE rowid = OLD.rowid;
|
||
|
|
END;
|
||
|
|
|
||
|
|
-- Triggers for context_files
|
||
|
|
CREATE TRIGGER tr_fts_context_files_insert AFTER INSERT ON context_files
|
||
|
|
BEGIN
|
||
|
|
INSERT INTO fts_context(rowid, content, project_id, file_path, source_type)
|
||
|
|
VALUES (NEW.file_id + 1000000, NEW.content, NEW.project_id, NEW.file_path, 'context_file');
|
||
|
|
END;
|
||
|
|
|
||
|
|
CREATE TRIGGER tr_fts_context_files_update AFTER UPDATE ON context_files
|
||
|
|
BEGIN
|
||
|
|
DELETE FROM fts_context WHERE rowid = OLD.file_id + 1000000;
|
||
|
|
INSERT INTO fts_context(rowid, content, project_id, file_path, source_type)
|
||
|
|
VALUES (NEW.file_id + 1000000, NEW.content, NEW.project_id, NEW.file_path, 'context_file');
|
||
|
|
END;
|
||
|
|
|
||
|
|
CREATE TRIGGER tr_fts_context_files_delete AFTER DELETE ON context_files
|
||
|
|
BEGIN
|
||
|
|
DELETE FROM fts_context WHERE rowid = OLD.file_id + 1000000;
|
||
|
|
END;
|
||
|
|
|
||
|
|
-- Triggers for user_profiles
|
||
|
|
CREATE TRIGGER tr_fts_user_profiles_insert AFTER INSERT ON user_profiles
|
||
|
|
BEGIN
|
||
|
|
INSERT INTO fts_context(rowid, content, project_id, file_path, source_type)
|
||
|
|
VALUES (NEW.rowid + 2000000, NEW.content, '~user~', NEW.user_id, 'user_profile');
|
||
|
|
END;
|
||
|
|
|
||
|
|
CREATE TRIGGER tr_fts_user_profiles_update AFTER UPDATE ON user_profiles
|
||
|
|
BEGIN
|
||
|
|
DELETE FROM fts_context WHERE rowid = OLD.rowid + 2000000;
|
||
|
|
INSERT INTO fts_context(rowid, content, project_id, file_path, source_type)
|
||
|
|
VALUES (NEW.rowid + 2000000, NEW.content, '~user~', NEW.user_id, 'user_profile');
|
||
|
|
END;
|
||
|
|
|
||
|
|
-- ============================================================================
|
||
|
|
-- SEED DATA (for development / first-run)
|
||
|
|
-- ============================================================================
|
||
|
|
INSERT INTO users (user_id, display_name, role) VALUES
|
||
|
|
('admin', 'Administrator', 'admin'),
|
||
|
|
('hermes-gateway', 'Hermes Agent', 'service');
|
||
|
|
|
||
|
|
INSERT INTO projects (project_id, display_name, description) VALUES
|
||
|
|
('welcome', 'Welcome', 'Getting started guide and documentation for ctxd'),
|
||
|
|
('remote-rig', 'RemoteRig', 'Multi-camera remote monitoring system');
|
||
|
|
|
||
|
|
-- Project context is seeded by the Python init code (cmd_init)
|
||
|
|
-- to ensure real newlines, not literal backslash-n from SQL strings.
|