Compare commits
No commits in common. "65c1f560e423094e2194965edda855e9ce3c5b49" and "2039f5ae48880ff1567fccef5efbae6a3fb463ef" have entirely different histories.
65c1f560e4
...
2039f5ae48
|
|
@ -61,24 +61,6 @@
|
||||||
- 25 unit tests (all passing): engine lifecycle, DB CRDT doc persistence, task/note CRDT create/read/mutate/merge round-trips, crypto derivation, stateless read validation, no-engine error paths.
|
- 25 unit tests (all passing): engine lifecycle, DB CRDT doc persistence, task/note CRDT create/read/mutate/merge round-trips, crypto derivation, stateless read validation, no-engine error paths.
|
||||||
- Full ADR at `docs/ARCHITECTURE/ADR/0005-ffi-bridge-flutter-rust-bridge.md`.
|
- Full ADR at `docs/ARCHITECTURE/ADR/0005-ffi-bridge-flutter-rust-bridge.md`.
|
||||||
|
|
||||||
## [2026-08-22] - ADL-007: chronogrid_core Schema Extension — Phase 2 Missing Entities (Task 2.1)
|
|
||||||
|
|
||||||
- **Status**: Accepted
|
|
||||||
- **Context**: Task 2.1 required extending `docs/API_CONTRACTS/schema.sql` with 6 missing entity tables identified in the ADL-006 architecture review as a critical gap blocking Phase 3 and Phase 4 development.
|
|
||||||
- **Decision**:
|
|
||||||
- Added 6 new tables: `time_blocks`, `attachments`, `calendar_feeds`, `node_peers`, `automation_rules`, `ingestion_configs`.
|
|
||||||
- All new tables include `hlc_timestamp TEXT NOT NULL` for CRDT compatibility, except `node_peers` which is managed by `chronogrid_net` and does not participate in CRDT sync.
|
|
||||||
- `node_peers` has no FK to `namespaces` — it is a global peer registry scoped to the node, not to a namespace.
|
|
||||||
- `attachments.storage_path` stores relative paths inside the encrypted vault directory; file bytes are stored outside SQLite to avoid BLOB size limits.
|
|
||||||
- `calendar_feeds` and `ingestion_configs` store credentials as encrypted blobs (`encrypted_password_blob`, `encrypted_imap_password_blob`) — never plaintext.
|
|
||||||
- `ingestion_configs` uses a `config_type` discriminator (`imap` | `file_watcher` | `hotkey`) with nullable type-specific fields, avoiding a separate table per ingestion type.
|
|
||||||
- `crdt_docs.entity_type` now supports `'time_block'` in addition to `'task'` and `'note'`.
|
|
||||||
- `EXPECTED_TABLES` in `crates/chronogrid_core/src/db.rs` updated from 7 to 13 entries; schema validation enforces all 13 tables on every database open.
|
|
||||||
- 12 new indexes added for all new FK columns and common query patterns.
|
|
||||||
- 8 new unit tests added covering CRUD round-trips, FK cascade/SET NULL behavior, and `time_block` CRDT doc persistence.
|
|
||||||
- Verification script confirmed: all 13 tables present in schema.sql (PASS), Flutter analysis clean (PASS).
|
|
||||||
- Full ADR at `docs/ARCHITECTURE/ADR/0007-schema-extension-phase2-entities.md`.
|
|
||||||
|
|
||||||
## [2026-08-22] - ADL-006: Architecture Review Board Assessment & Master Development Plan v2
|
## [2026-08-22] - ADL-006: Architecture Review Board Assessment & Master Development Plan v2
|
||||||
|
|
||||||
- **Status**: Accepted
|
- **Status**: Accepted
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,7 @@ pub const KEY_LEN: usize = 32;
|
||||||
const SCHEMA_SQL: &str = include_str!("../../../docs/API_CONTRACTS/schema.sql");
|
const SCHEMA_SQL: &str = include_str!("../../../docs/API_CONTRACTS/schema.sql");
|
||||||
|
|
||||||
/// Expected tables that must exist after schema initialization.
|
/// Expected tables that must exist after schema initialization.
|
||||||
///
|
|
||||||
/// Includes the 7 original Phase 1 tables plus the 6 Phase 2 extension tables
|
|
||||||
/// added in Task 2.1. The total is 13 tables as specified in the Phase 2
|
|
||||||
/// completion criteria.
|
|
||||||
const EXPECTED_TABLES: &[&str] = &[
|
const EXPECTED_TABLES: &[&str] = &[
|
||||||
// Phase 1 — original tables
|
|
||||||
"namespaces",
|
"namespaces",
|
||||||
"projects",
|
"projects",
|
||||||
"tasks",
|
"tasks",
|
||||||
|
|
@ -44,13 +39,6 @@ const EXPECTED_TABLES: &[&str] = &[
|
||||||
"task_note_links",
|
"task_note_links",
|
||||||
"ingestion_logs",
|
"ingestion_logs",
|
||||||
"crdt_docs",
|
"crdt_docs",
|
||||||
// Phase 2 — extension tables (Task 2.1)
|
|
||||||
"time_blocks",
|
|
||||||
"attachments",
|
|
||||||
"calendar_feeds",
|
|
||||||
"node_peers",
|
|
||||||
"automation_rules",
|
|
||||||
"ingestion_configs",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// ── Database struct ──────────────────────────────────────────────────────────
|
// ── Database struct ──────────────────────────────────────────────────────────
|
||||||
|
|
@ -268,25 +256,11 @@ mod tests {
|
||||||
let db = Database::open_in_memory(&TEST_KEY).unwrap();
|
let db = Database::open_in_memory(&TEST_KEY).unwrap();
|
||||||
|
|
||||||
let expected_indexes = [
|
let expected_indexes = [
|
||||||
// Phase 1 indexes
|
|
||||||
"idx_tasks_namespace",
|
"idx_tasks_namespace",
|
||||||
"idx_tasks_eisenhower",
|
"idx_tasks_eisenhower",
|
||||||
"idx_tasks_due_date",
|
"idx_tasks_due_date",
|
||||||
"idx_projects_parent",
|
"idx_projects_parent",
|
||||||
"idx_crdt_docs_type",
|
"idx_crdt_docs_type",
|
||||||
// Phase 2 indexes (Task 2.1)
|
|
||||||
"idx_time_blocks_namespace",
|
|
||||||
"idx_time_blocks_task",
|
|
||||||
"idx_time_blocks_start",
|
|
||||||
"idx_attachments_note",
|
|
||||||
"idx_attachments_ingestion",
|
|
||||||
"idx_calendar_feeds_namespace",
|
|
||||||
"idx_node_peers_swarm",
|
|
||||||
"idx_node_peers_last_seen",
|
|
||||||
"idx_automation_rules_namespace",
|
|
||||||
"idx_automation_rules_trigger",
|
|
||||||
"idx_ingestion_configs_namespace",
|
|
||||||
"idx_ingestion_configs_type",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for &idx in &expected_indexes {
|
for &idx in &expected_indexes {
|
||||||
|
|
@ -302,22 +276,6 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifies the total table count matches the Phase 2 completion criterion
|
|
||||||
/// of exactly 13 tables (7 original + 6 Phase 2 extension tables).
|
|
||||||
#[test]
|
|
||||||
fn schema_has_exactly_13_tables() {
|
|
||||||
let db = Database::open_in_memory(&TEST_KEY).unwrap();
|
|
||||||
let count: i64 = db
|
|
||||||
.connection()
|
|
||||||
.query_row(
|
|
||||||
"SELECT count(*) FROM sqlite_master WHERE type='table';",
|
|
||||||
[],
|
|
||||||
|r| r.get(0),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(count, 13, "schema must contain exactly 13 tables (7 Phase 1 + 6 Phase 2)");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn foreign_keys_are_enabled() {
|
fn foreign_keys_are_enabled() {
|
||||||
let db = Database::open_in_memory(&TEST_KEY).unwrap();
|
let db = Database::open_in_memory(&TEST_KEY).unwrap();
|
||||||
|
|
@ -553,283 +511,4 @@ mod tests {
|
||||||
let db = Database::open_in_memory(&TEST_KEY).unwrap();
|
let db = Database::open_in_memory(&TEST_KEY).unwrap();
|
||||||
assert!(!db.delete_crdt_doc("nonexistent").unwrap());
|
assert!(!db.delete_crdt_doc("nonexistent").unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Phase 2 extension table CRUD round-trips (Task 2.1) ──────────────
|
|
||||||
|
|
||||||
/// Helper: inserts a minimal namespace + task into `c` for FK-dependent tests.
|
|
||||||
fn seed_namespace_and_task(c: &Connection) {
|
|
||||||
c.execute(
|
|
||||||
"INSERT INTO namespaces (id, name, created_at, updated_at) VALUES ('ns1','Test',1,1);",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
c.execute(
|
|
||||||
"INSERT INTO tasks (id, namespace_id, title, created_at, hlc_timestamp)
|
|
||||||
VALUES ('t1','ns1','Seed Task',1,'hlc-seed');",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper: inserts a minimal namespace + note into `c` for FK-dependent tests.
|
|
||||||
fn seed_namespace_and_note(c: &Connection) {
|
|
||||||
c.execute(
|
|
||||||
"INSERT INTO namespaces (id, name, created_at, updated_at) VALUES ('ns1','Test',1,1);",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
c.execute(
|
|
||||||
"INSERT INTO notes (id, namespace_id, title, content_markdown, created_at, updated_at, hlc_timestamp)
|
|
||||||
VALUES ('n1','ns1','Seed Note','# body',1,1,'hlc-note');",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn crud_time_blocks() {
|
|
||||||
let db = Database::open_in_memory(&TEST_KEY).unwrap();
|
|
||||||
let c = db.connection();
|
|
||||||
seed_namespace_and_task(c);
|
|
||||||
|
|
||||||
// Insert a time block linked to the task.
|
|
||||||
c.execute(
|
|
||||||
"INSERT INTO time_blocks
|
|
||||||
(id, namespace_id, task_id, title, start_time, end_time, created_at, updated_at, hlc_timestamp)
|
|
||||||
VALUES ('tb1','ns1','t1','Deep Work',1000,3600000,1,1,'hlc-tb1');",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let title: String = c
|
|
||||||
.query_row(
|
|
||||||
"SELECT title FROM time_blocks WHERE id='tb1';",
|
|
||||||
[],
|
|
||||||
|r| r.get(0),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(title, "Deep Work");
|
|
||||||
|
|
||||||
// Verify FK: deleting the task must SET NULL on time_blocks.task_id.
|
|
||||||
c.execute("DELETE FROM tasks WHERE id='t1';", []).unwrap();
|
|
||||||
let task_id: Option<String> = c
|
|
||||||
.query_row(
|
|
||||||
"SELECT task_id FROM time_blocks WHERE id='tb1';",
|
|
||||||
[],
|
|
||||||
|r| r.get(0),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert!(task_id.is_none(), "task_id must be NULL after task deletion");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn crud_attachments() {
|
|
||||||
let db = Database::open_in_memory(&TEST_KEY).unwrap();
|
|
||||||
let c = db.connection();
|
|
||||||
seed_namespace_and_note(c);
|
|
||||||
|
|
||||||
c.execute(
|
|
||||||
"INSERT INTO attachments
|
|
||||||
(id, note_id, file_name, mime_type, file_size_bytes, storage_path, checksum_sha256, created_at, hlc_timestamp)
|
|
||||||
VALUES ('att1','n1','report.pdf','application/pdf',204800,'vault/att1.pdf','deadbeef',1,'hlc-att1');",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let fname: String = c
|
|
||||||
.query_row(
|
|
||||||
"SELECT file_name FROM attachments WHERE id='att1';",
|
|
||||||
[],
|
|
||||||
|r| r.get(0),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(fname, "report.pdf");
|
|
||||||
|
|
||||||
// Cascade: deleting the note must remove the attachment.
|
|
||||||
c.execute("DELETE FROM notes WHERE id='n1';", []).unwrap();
|
|
||||||
let count: i64 = c
|
|
||||||
.query_row("SELECT count(*) FROM attachments;", [], |r| r.get(0))
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(count, 0, "attachment must be cascade-deleted with its note");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn crud_calendar_feeds() {
|
|
||||||
let db = Database::open_in_memory(&TEST_KEY).unwrap();
|
|
||||||
let c = db.connection();
|
|
||||||
c.execute(
|
|
||||||
"INSERT INTO namespaces (id, name, created_at, updated_at) VALUES ('ns1','Test',1,1);",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
c.execute(
|
|
||||||
"INSERT INTO calendar_feeds
|
|
||||||
(id, namespace_id, display_name, feed_url, created_at, updated_at, hlc_timestamp)
|
|
||||||
VALUES ('cf1','ns1','Work Cal','https://caldav.example.com/cal',1,1,'hlc-cf1');",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let url: String = c
|
|
||||||
.query_row(
|
|
||||||
"SELECT feed_url FROM calendar_feeds WHERE id='cf1';",
|
|
||||||
[],
|
|
||||||
|r| r.get(0),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(url, "https://caldav.example.com/cal");
|
|
||||||
|
|
||||||
// Cascade: deleting the namespace must remove the feed.
|
|
||||||
c.execute("DELETE FROM namespaces WHERE id='ns1';", []).unwrap();
|
|
||||||
let count: i64 = c
|
|
||||||
.query_row("SELECT count(*) FROM calendar_feeds;", [], |r| r.get(0))
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(count, 0, "calendar_feed must be cascade-deleted with its namespace");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn crud_node_peers() {
|
|
||||||
let db = Database::open_in_memory(&TEST_KEY).unwrap();
|
|
||||||
let c = db.connection();
|
|
||||||
|
|
||||||
// node_peers has no FK to namespaces — it is a global peer registry.
|
|
||||||
c.execute(
|
|
||||||
"INSERT INTO node_peers
|
|
||||||
(id, public_key_bytes, swarm_key_fingerprint, is_trusted, created_at, updated_at)
|
|
||||||
VALUES ('peer-abc123', X'AABBCCDD', 'fp0011223344556677', 1, 1, 1);",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let trusted: i64 = c
|
|
||||||
.query_row(
|
|
||||||
"SELECT is_trusted FROM node_peers WHERE id='peer-abc123';",
|
|
||||||
[],
|
|
||||||
|r| r.get(0),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(trusted, 1);
|
|
||||||
|
|
||||||
// Update last_seen_at.
|
|
||||||
c.execute(
|
|
||||||
"UPDATE node_peers SET last_seen_at=9999 WHERE id='peer-abc123';",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let last_seen: i64 = c
|
|
||||||
.query_row(
|
|
||||||
"SELECT last_seen_at FROM node_peers WHERE id='peer-abc123';",
|
|
||||||
[],
|
|
||||||
|r| r.get(0),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(last_seen, 9999);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn crud_automation_rules() {
|
|
||||||
let db = Database::open_in_memory(&TEST_KEY).unwrap();
|
|
||||||
let c = db.connection();
|
|
||||||
c.execute(
|
|
||||||
"INSERT INTO namespaces (id, name, created_at, updated_at) VALUES ('ns1','Test',1,1);",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
c.execute(
|
|
||||||
"INSERT INTO automation_rules
|
|
||||||
(id, namespace_id, name, trigger_type, trigger_config, action_type, action_config, created_at, updated_at, hlc_timestamp)
|
|
||||||
VALUES ('ar1','ns1','Auto-assign','task_created','{\"project\":\"p1\"}','set_field','{\"field\":\"status\",\"value\":\"in_progress\"}',1,1,'hlc-ar1');",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let name: String = c
|
|
||||||
.query_row(
|
|
||||||
"SELECT name FROM automation_rules WHERE id='ar1';",
|
|
||||||
[],
|
|
||||||
|r| r.get(0),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(name, "Auto-assign");
|
|
||||||
|
|
||||||
// Cascade: deleting the namespace must remove the rule.
|
|
||||||
c.execute("DELETE FROM namespaces WHERE id='ns1';", []).unwrap();
|
|
||||||
let count: i64 = c
|
|
||||||
.query_row("SELECT count(*) FROM automation_rules;", [], |r| r.get(0))
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(count, 0, "automation_rule must be cascade-deleted with its namespace");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn crud_ingestion_configs() {
|
|
||||||
let db = Database::open_in_memory(&TEST_KEY).unwrap();
|
|
||||||
let c = db.connection();
|
|
||||||
c.execute(
|
|
||||||
"INSERT INTO namespaces (id, name, created_at, updated_at) VALUES ('ns1','Test',1,1);",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// IMAP config
|
|
||||||
c.execute(
|
|
||||||
"INSERT INTO ingestion_configs
|
|
||||||
(id, namespace_id, config_type, display_name, imap_host, imap_port, imap_username, imap_use_tls, created_at, updated_at, hlc_timestamp)
|
|
||||||
VALUES ('ic1','ns1','imap','Work IMAP','mail.example.com',993,'user@example.com',1,1,1,'hlc-ic1');",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// File-watcher config
|
|
||||||
c.execute(
|
|
||||||
"INSERT INTO ingestion_configs
|
|
||||||
(id, namespace_id, config_type, display_name, watch_path, watch_extensions, created_at, updated_at, hlc_timestamp)
|
|
||||||
VALUES ('ic2','ns1','file_watcher','Docs Watcher','/home/user/docs','[\"md\",\"pdf\"]',1,1,'hlc-ic2');",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let count: i64 = c
|
|
||||||
.query_row("SELECT count(*) FROM ingestion_configs;", [], |r| r.get(0))
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(count, 2);
|
|
||||||
|
|
||||||
let host: String = c
|
|
||||||
.query_row(
|
|
||||||
"SELECT imap_host FROM ingestion_configs WHERE id='ic1';",
|
|
||||||
[],
|
|
||||||
|r| r.get(0),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(host, "mail.example.com");
|
|
||||||
|
|
||||||
// Cascade: deleting the namespace must remove both configs.
|
|
||||||
c.execute("DELETE FROM namespaces WHERE id='ns1';", []).unwrap();
|
|
||||||
let count: i64 = c
|
|
||||||
.query_row("SELECT count(*) FROM ingestion_configs;", [], |r| r.get(0))
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(count, 0, "ingestion_configs must be cascade-deleted with their namespace");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn crdt_doc_time_block_entity_type() {
|
|
||||||
// Verifies that 'time_block' is a valid entity_type value in crdt_docs.
|
|
||||||
let db = Database::open_in_memory(&TEST_KEY).unwrap();
|
|
||||||
db.save_crdt_doc("tb-001", "time_block", b"fake-am-bytes", 1000)
|
|
||||||
.unwrap();
|
|
||||||
let loaded = db.load_crdt_doc("tb-001").unwrap();
|
|
||||||
assert_eq!(loaded, Some(b"fake-am-bytes".to_vec()));
|
|
||||||
|
|
||||||
// Confirm entity_type is stored correctly.
|
|
||||||
let entity_type: String = db
|
|
||||||
.connection()
|
|
||||||
.query_row(
|
|
||||||
"SELECT entity_type FROM crdt_docs WHERE entity_id='tb-001';",
|
|
||||||
[],
|
|
||||||
|r| r.get(0),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(entity_type, "time_block");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,151 +84,15 @@ CREATE TABLE IF NOT EXISTS ingestion_logs (
|
||||||
);
|
);
|
||||||
|
|
||||||
-- CRDT Document Storage (Automerge binary state)
|
-- CRDT Document Storage (Automerge binary state)
|
||||||
-- entity_type: 'task' | 'note' | 'time_block'
|
|
||||||
CREATE TABLE IF NOT EXISTS crdt_docs (
|
CREATE TABLE IF NOT EXISTS crdt_docs (
|
||||||
entity_id TEXT PRIMARY KEY, -- References task.id, note.id, or time_blocks.id
|
entity_id TEXT PRIMARY KEY, -- References task.id or note.id
|
||||||
entity_type TEXT NOT NULL, -- 'task', 'note', or 'time_block'
|
entity_type TEXT NOT NULL, -- 'task' or 'note'
|
||||||
doc_bytes BLOB NOT NULL, -- Serialized Automerge document
|
doc_bytes BLOB NOT NULL, -- Serialized Automerge document
|
||||||
updated_at INTEGER NOT NULL -- Unix Epoch (ms)
|
updated_at INTEGER NOT NULL -- Unix Epoch (ms)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ==============================================================================
|
|
||||||
-- Phase 2 Extension Tables
|
|
||||||
-- ==============================================================================
|
|
||||||
|
|
||||||
-- Calendar Time-Blocking Entries (linked to tasks)
|
|
||||||
-- Represents a scheduled block of time on the calendar grid for a task.
|
|
||||||
CREATE TABLE IF NOT EXISTS time_blocks (
|
|
||||||
id TEXT PRIMARY KEY, -- UUIDv4
|
|
||||||
namespace_id TEXT NOT NULL,
|
|
||||||
task_id TEXT, -- Nullable: a block may exist without a task
|
|
||||||
title TEXT NOT NULL, -- Display label for the block
|
|
||||||
start_time INTEGER NOT NULL, -- Unix Epoch (ms) — block start
|
|
||||||
end_time INTEGER NOT NULL, -- Unix Epoch (ms) — block end
|
|
||||||
color_hex TEXT DEFAULT '#4A90D9', -- Calendar display color
|
|
||||||
is_all_day INTEGER DEFAULT 0, -- 1 = all-day event, 0 = timed block
|
|
||||||
recurrence_rule TEXT, -- iCal RRULE string (nullable)
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL,
|
|
||||||
hlc_timestamp TEXT NOT NULL,
|
|
||||||
FOREIGN KEY(namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE SET NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Binary / File Attachments (linked to notes or ingestion logs)
|
|
||||||
CREATE TABLE IF NOT EXISTS attachments (
|
|
||||||
id TEXT PRIMARY KEY, -- UUIDv4
|
|
||||||
note_id TEXT, -- Nullable: attachment may belong to a note
|
|
||||||
ingestion_log_id TEXT, -- Nullable: attachment may belong to an ingestion log
|
|
||||||
file_name TEXT NOT NULL,
|
|
||||||
mime_type TEXT NOT NULL,
|
|
||||||
file_size_bytes INTEGER NOT NULL,
|
|
||||||
storage_path TEXT NOT NULL, -- Relative path inside the encrypted vault directory
|
|
||||||
checksum_sha256 TEXT NOT NULL, -- SHA-256 hex digest for integrity verification
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
hlc_timestamp TEXT NOT NULL,
|
|
||||||
FOREIGN KEY(note_id) REFERENCES notes(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY(ingestion_log_id) REFERENCES ingestion_logs(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CalDAV External Feed Configuration (per namespace)
|
|
||||||
CREATE TABLE IF NOT EXISTS calendar_feeds (
|
|
||||||
id TEXT PRIMARY KEY, -- UUIDv4
|
|
||||||
namespace_id TEXT NOT NULL,
|
|
||||||
display_name TEXT NOT NULL,
|
|
||||||
feed_url TEXT NOT NULL, -- CalDAV server URL
|
|
||||||
username TEXT, -- CalDAV auth username (nullable for public feeds)
|
|
||||||
encrypted_password_blob TEXT, -- Encrypted credential blob (AES-GCM, key from keychain)
|
|
||||||
sync_interval_seconds INTEGER DEFAULT 300,
|
|
||||||
last_synced_at INTEGER, -- Unix Epoch (ms), nullable until first sync
|
|
||||||
is_enabled INTEGER DEFAULT 1,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL,
|
|
||||||
hlc_timestamp TEXT NOT NULL,
|
|
||||||
FOREIGN KEY(namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- P2P Peer Registry (NodeId, public key, last seen, swarm membership)
|
|
||||||
CREATE TABLE IF NOT EXISTS node_peers (
|
|
||||||
id TEXT PRIMARY KEY, -- NodeId: Ed25519 public key hash (hex)
|
|
||||||
display_name TEXT, -- Optional human-readable label
|
|
||||||
public_key_bytes BLOB NOT NULL, -- Raw Ed25519 public key bytes (32 bytes)
|
|
||||||
swarm_key_fingerprint TEXT NOT NULL, -- First 16 hex chars of the swarm key SHA-256 hash
|
|
||||||
last_seen_at INTEGER, -- Unix Epoch (ms), nullable until first contact
|
|
||||||
last_known_addrs TEXT, -- JSON array of multiaddr strings
|
|
||||||
is_trusted INTEGER DEFAULT 0, -- 1 = explicitly trusted peer
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Local Trigger / Action Automation Rules
|
|
||||||
CREATE TABLE IF NOT EXISTS automation_rules (
|
|
||||||
id TEXT PRIMARY KEY, -- UUIDv4
|
|
||||||
namespace_id TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
trigger_type TEXT NOT NULL, -- 'task_created', 'task_status_changed', 'ingestion_received', 'schedule'
|
|
||||||
trigger_config TEXT NOT NULL, -- JSON blob: trigger-specific parameters
|
|
||||||
action_type TEXT NOT NULL, -- 'set_field', 'create_task', 'send_notification', 'run_script'
|
|
||||||
action_config TEXT NOT NULL, -- JSON blob: action-specific parameters
|
|
||||||
is_enabled INTEGER DEFAULT 1,
|
|
||||||
execution_count INTEGER DEFAULT 0,
|
|
||||||
last_executed_at INTEGER, -- Unix Epoch (ms), nullable
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL,
|
|
||||||
hlc_timestamp TEXT NOT NULL,
|
|
||||||
FOREIGN KEY(namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- IMAP Account and File-Watcher Ingestion Configuration (per namespace)
|
|
||||||
CREATE TABLE IF NOT EXISTS ingestion_configs (
|
|
||||||
id TEXT PRIMARY KEY, -- UUIDv4
|
|
||||||
namespace_id TEXT NOT NULL,
|
|
||||||
config_type TEXT NOT NULL, -- 'imap' | 'file_watcher' | 'hotkey'
|
|
||||||
display_name TEXT NOT NULL,
|
|
||||||
-- IMAP-specific fields (nullable for non-IMAP types)
|
|
||||||
imap_host TEXT,
|
|
||||||
imap_port INTEGER,
|
|
||||||
imap_username TEXT,
|
|
||||||
encrypted_imap_password_blob TEXT, -- Encrypted credential blob
|
|
||||||
imap_mailbox TEXT DEFAULT 'INBOX',
|
|
||||||
imap_use_tls INTEGER DEFAULT 1,
|
|
||||||
-- File-watcher-specific fields (nullable for non-file-watcher types)
|
|
||||||
watch_path TEXT, -- Absolute path to the monitored directory
|
|
||||||
watch_extensions TEXT, -- JSON array of file extensions, e.g. ["md","pdf","txt"]
|
|
||||||
-- Shared scheduling
|
|
||||||
poll_interval_seconds INTEGER DEFAULT 300,
|
|
||||||
last_polled_at INTEGER, -- Unix Epoch (ms), nullable
|
|
||||||
is_enabled INTEGER DEFAULT 1,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL,
|
|
||||||
hlc_timestamp TEXT NOT NULL,
|
|
||||||
FOREIGN KEY(namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ==============================================================================
|
|
||||||
-- Indexes — Original Tables
|
|
||||||
-- ==============================================================================
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_tasks_namespace ON tasks(namespace_id);
|
CREATE INDEX IF NOT EXISTS idx_tasks_namespace ON tasks(namespace_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_tasks_eisenhower ON tasks(eisenhower_quadrant);
|
CREATE INDEX IF NOT EXISTS idx_tasks_eisenhower ON tasks(eisenhower_quadrant);
|
||||||
CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date);
|
CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date);
|
||||||
CREATE INDEX IF NOT EXISTS idx_projects_parent ON projects(parent_id);
|
CREATE INDEX IF NOT EXISTS idx_projects_parent ON projects(parent_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_crdt_docs_type ON crdt_docs(entity_type);
|
CREATE INDEX IF NOT EXISTS idx_crdt_docs_type ON crdt_docs(entity_type);
|
||||||
|
|
||||||
-- ==============================================================================
|
|
||||||
-- Indexes — Phase 2 Extension Tables
|
|
||||||
-- ==============================================================================
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_time_blocks_namespace ON time_blocks(namespace_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_time_blocks_task ON time_blocks(task_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_time_blocks_start ON time_blocks(start_time);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_attachments_note ON attachments(note_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_attachments_ingestion ON attachments(ingestion_log_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_calendar_feeds_namespace ON calendar_feeds(namespace_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_node_peers_swarm ON node_peers(swarm_key_fingerprint);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_node_peers_last_seen ON node_peers(last_seen_at);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_automation_rules_namespace ON automation_rules(namespace_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_automation_rules_trigger ON automation_rules(trigger_type);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_ingestion_configs_namespace ON ingestion_configs(namespace_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_ingestion_configs_type ON ingestion_configs(config_type);
|
|
||||||
|
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
# ADR-0007: Schema Extension — Phase 2 Missing Entities (Task 2.1)
|
|
||||||
|
|
||||||
**Date:** 2026-08-22
|
|
||||||
**Status:** Accepted
|
|
||||||
**Task:** 2.1 — Extend Schema with Missing Entities
|
|
||||||
**Author:** Autonomous Agent Cycle
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The Phase 2 completion criteria for ChronoGrid P2P require 13 total database tables (7 original Phase 1 tables + 6 new Phase 2 extension tables). The architecture review (ADL-006) identified 6 missing schema tables as a critical gap blocking Phase 3 (Flutter UI requires calendar/time-block entities) and Phase 4 (AI/automation requires ingestion and rule entities).
|
|
||||||
|
|
||||||
The canonical schema at `docs/API_CONTRACTS/schema.sql` is embedded at compile time via `include_str!` in `crates/chronogrid_core/src/db.rs`, making it the single source of truth for all database structure.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
### New Tables Added
|
|
||||||
|
|
||||||
Six new tables were added to `docs/API_CONTRACTS/schema.sql`:
|
|
||||||
|
|
||||||
#### 1. `time_blocks`
|
|
||||||
- **Purpose:** Calendar time-blocking entries linked to tasks. Represents a scheduled block of time on the calendar grid.
|
|
||||||
- **Key fields:** `id`, `namespace_id`, `task_id` (nullable FK → tasks, SET NULL), `title`, `start_time`, `end_time`, `color_hex`, `is_all_day`, `recurrence_rule`, `hlc_timestamp`
|
|
||||||
- **CRDT:** Supports `entity_type = 'time_block'` in `crdt_docs` table (documented in comment and tested).
|
|
||||||
- **Indexes:** `idx_time_blocks_namespace`, `idx_time_blocks_task`, `idx_time_blocks_start`
|
|
||||||
|
|
||||||
#### 2. `attachments`
|
|
||||||
- **Purpose:** Binary/file attachments linked to notes or ingestion logs.
|
|
||||||
- **Key fields:** `id`, `note_id` (nullable FK → notes, CASCADE), `ingestion_log_id` (nullable FK → ingestion_logs, CASCADE), `file_name`, `mime_type`, `file_size_bytes`, `storage_path`, `checksum_sha256`, `hlc_timestamp`
|
|
||||||
- **Design note:** `storage_path` is a relative path inside the encrypted vault directory; actual file bytes are stored outside SQLite to avoid BLOB size limits.
|
|
||||||
- **Indexes:** `idx_attachments_note`, `idx_attachments_ingestion`
|
|
||||||
|
|
||||||
#### 3. `calendar_feeds`
|
|
||||||
- **Purpose:** CalDAV external feed configuration per namespace.
|
|
||||||
- **Key fields:** `id`, `namespace_id` (FK → namespaces, CASCADE), `display_name`, `feed_url`, `username`, `encrypted_password_blob` (AES-GCM encrypted, key from keychain), `sync_interval_seconds`, `last_synced_at`, `is_enabled`, `hlc_timestamp`
|
|
||||||
- **Security:** Credentials are stored as encrypted blobs — never plaintext.
|
|
||||||
- **Indexes:** `idx_calendar_feeds_namespace`
|
|
||||||
|
|
||||||
#### 4. `node_peers`
|
|
||||||
- **Purpose:** P2P peer registry — NodeId, public key, last seen, swarm membership.
|
|
||||||
- **Key fields:** `id` (NodeId: Ed25519 public key hash hex), `public_key_bytes` (BLOB, 32 bytes), `swarm_key_fingerprint`, `last_seen_at`, `last_known_addrs` (JSON array of multiaddr strings), `is_trusted`
|
|
||||||
- **Design note:** No FK to `namespaces` — peers are global to the node, not namespace-scoped.
|
|
||||||
- **No `hlc_timestamp`:** Peer registry entries are managed by the networking layer, not the CRDT engine.
|
|
||||||
- **Indexes:** `idx_node_peers_swarm`, `idx_node_peers_last_seen`
|
|
||||||
|
|
||||||
#### 5. `automation_rules`
|
|
||||||
- **Purpose:** Local trigger/action rule definitions for the automation engine.
|
|
||||||
- **Key fields:** `id`, `namespace_id` (FK → namespaces, CASCADE), `name`, `trigger_type` (`task_created` | `task_status_changed` | `ingestion_received` | `schedule`), `trigger_config` (JSON), `action_type` (`set_field` | `create_task` | `send_notification` | `run_script`), `action_config` (JSON), `is_enabled`, `execution_count`, `last_executed_at`, `hlc_timestamp`
|
|
||||||
- **Indexes:** `idx_automation_rules_namespace`, `idx_automation_rules_trigger`
|
|
||||||
|
|
||||||
#### 6. `ingestion_configs`
|
|
||||||
- **Purpose:** IMAP account and file-watcher configuration per namespace.
|
|
||||||
- **Key fields:** `id`, `namespace_id` (FK → namespaces, CASCADE), `config_type` (`imap` | `file_watcher` | `hotkey`), IMAP-specific fields (`imap_host`, `imap_port`, `imap_username`, `encrypted_imap_password_blob`, `imap_mailbox`, `imap_use_tls`), file-watcher fields (`watch_path`, `watch_extensions` JSON array), `poll_interval_seconds`, `last_polled_at`, `hlc_timestamp`
|
|
||||||
- **Design note:** Single table covers multiple ingestion types via `config_type` discriminator; type-specific fields are nullable for non-applicable types.
|
|
||||||
- **Indexes:** `idx_ingestion_configs_namespace`, `idx_ingestion_configs_type`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## CRDT Compatibility
|
|
||||||
|
|
||||||
All 6 new tables include `hlc_timestamp TEXT NOT NULL` (except `node_peers`, which is managed by the networking layer and does not participate in CRDT sync). The `crdt_docs` table comment was updated to document `time_block` as a valid `entity_type` value alongside `task` and `note`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code Changes
|
|
||||||
|
|
||||||
| File | Change |
|
|
||||||
|------|--------|
|
|
||||||
| `docs/API_CONTRACTS/schema.sql` | Added 6 new tables + 12 new indexes |
|
|
||||||
| `crates/chronogrid_core/src/db.rs` | Updated `EXPECTED_TABLES` from 7 to 13 entries |
|
|
||||||
| `crates/chronogrid_core/src/db.rs` | Added 8 new unit tests covering all Phase 2 tables |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Test Coverage Added
|
|
||||||
|
|
||||||
- `schema_has_exactly_13_tables` — asserts total table count = 13
|
|
||||||
- `open_in_memory_creates_indexes` — expanded to cover all 17 indexes (5 Phase 1 + 12 Phase 2)
|
|
||||||
- `crud_time_blocks` — insert, read, FK SET NULL on task deletion
|
|
||||||
- `crud_attachments` — insert, read, CASCADE delete with note
|
|
||||||
- `crud_calendar_feeds` — insert, read, CASCADE delete with namespace
|
|
||||||
- `crud_node_peers` — insert, read, update `last_seen_at`
|
|
||||||
- `crud_automation_rules` — insert, read, CASCADE delete with namespace
|
|
||||||
- `crud_ingestion_configs` — IMAP + file-watcher configs, CASCADE delete
|
|
||||||
- `crdt_doc_time_block_entity_type` — verifies `time_block` entity_type in `crdt_docs`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
- **Positive:** All Phase 2 completion criteria for schema are met (13 tables confirmed by verification script).
|
|
||||||
- **Positive:** `EXPECTED_TABLES` validation in `db.rs` now enforces all 13 tables on every database open, preventing schema drift.
|
|
||||||
- **Positive:** Existing Phase 1 tests are unaffected — all DDL uses `CREATE TABLE IF NOT EXISTS`.
|
|
||||||
- **Neutral:** `node_peers` intentionally omits `hlc_timestamp` as it is managed by `chronogrid_net`, not the CRDT engine.
|
|
||||||
- **Neutral:** `attachments.storage_path` stores relative paths; the vault directory root is resolved at runtime by the application layer.
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
## Task 2.1 — Extend Schema with Missing Entities
|
## Task 2.1 — Extend Schema with Missing Entities
|
||||||
|
|
||||||
- [x] **Task 2.1**: Extend `docs/API_CONTRACTS/schema.sql` with the following new tables:
|
- [ ] **Task 2.1**: Extend `docs/API_CONTRACTS/schema.sql` with the following new tables:
|
||||||
- `time_blocks` — Calendar time-blocking entries linked to tasks
|
- `time_blocks` — Calendar time-blocking entries linked to tasks
|
||||||
- `attachments` — Binary/file attachments linked to notes or ingestion logs
|
- `attachments` — Binary/file attachments linked to notes or ingestion logs
|
||||||
- `calendar_feeds` — CalDAV external feed configuration per namespace
|
- `calendar_feeds` — CalDAV external feed configuration per namespace
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue