@cerefox/memory 0.7.2 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/bin/cerefox.js +1357 -361
  2. package/dist/frontend/assets/{index-BzAPcCXA.js → index-CAp2_lFX.js} +2 -2
  3. package/dist/frontend/assets/index-CAp2_lFX.js.map +1 -0
  4. package/dist/frontend/index.html +1 -1
  5. package/dist/server-assets/_shared/ef-meta/index.ts +97 -0
  6. package/dist/server-assets/_shared/embeddings/index.ts +175 -0
  7. package/dist/server-assets/_shared/mcp-tools/_chunker.ts +187 -0
  8. package/dist/server-assets/_shared/mcp-tools/_projects.ts +121 -0
  9. package/dist/server-assets/_shared/mcp-tools/_utils.ts +73 -0
  10. package/dist/server-assets/_shared/mcp-tools/audit-log.ts +95 -0
  11. package/dist/server-assets/_shared/mcp-tools/get-document.ts +73 -0
  12. package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +26 -0
  13. package/dist/server-assets/_shared/mcp-tools/get-help.ts +90 -0
  14. package/dist/server-assets/_shared/mcp-tools/index.ts +67 -0
  15. package/dist/server-assets/_shared/mcp-tools/ingest.ts +315 -0
  16. package/dist/server-assets/_shared/mcp-tools/list-metadata-keys.ts +55 -0
  17. package/dist/server-assets/_shared/mcp-tools/list-projects.ts +59 -0
  18. package/dist/server-assets/_shared/mcp-tools/list-versions.ts +72 -0
  19. package/dist/server-assets/_shared/mcp-tools/metadata-search.ts +154 -0
  20. package/dist/server-assets/_shared/mcp-tools/search.ts +193 -0
  21. package/dist/server-assets/_shared/mcp-tools/set-document-projects.ts +163 -0
  22. package/dist/server-assets/_shared/mcp-tools/types.ts +92 -0
  23. package/dist/server-assets/db/migrations/0003_add_document_versions.sql +91 -0
  24. package/dist/server-assets/db/migrations/0004_add_audit_log_review_status_archived.sql +71 -0
  25. package/dist/server-assets/db/migrations/0005_metadata_search.sql +628 -0
  26. package/dist/server-assets/db/migrations/0006_usage_log.sql +255 -0
  27. package/dist/server-assets/db/migrations/0007_usage_log_requestor.sql +178 -0
  28. package/dist/server-assets/db/migrations/0008_soft_delete.sql +130 -0
  29. package/dist/server-assets/db/migrations/0009_audit_log_restore_operation.sql +20 -0
  30. package/dist/server-assets/db/migrations/0010_requestor_enforcement_config.sql +12 -0
  31. package/dist/server-assets/db/migrations/0011_title_boosting.sql +48 -0
  32. package/dist/server-assets/db/rpcs.sql +1723 -0
  33. package/dist/server-assets/db/schema.sql +380 -0
  34. package/dist/server-assets/supabase/functions/cerefox-get-audit-log/index.ts +117 -0
  35. package/dist/server-assets/supabase/functions/cerefox-get-document/index.ts +138 -0
  36. package/dist/server-assets/supabase/functions/cerefox-ingest/index.ts +819 -0
  37. package/dist/server-assets/supabase/functions/cerefox-list-projects/index.ts +96 -0
  38. package/dist/server-assets/supabase/functions/cerefox-list-versions/index.ts +113 -0
  39. package/dist/server-assets/supabase/functions/cerefox-mcp/index.ts +294 -0
  40. package/dist/server-assets/supabase/functions/cerefox-mcp/shared.ts +42 -0
  41. package/dist/server-assets/supabase/functions/cerefox-metadata/index.ts +99 -0
  42. package/dist/server-assets/supabase/functions/cerefox-metadata-search/index.ts +146 -0
  43. package/dist/server-assets/supabase/functions/cerefox-search/index.ts +382 -0
  44. package/docs/guides/connect-agents.md +78 -3
  45. package/docs/guides/migration-v0.5.md +50 -0
  46. package/docs/guides/quickstart.md +6 -2
  47. package/package.json +3 -2
  48. package/dist/frontend/assets/index-BzAPcCXA.js.map +0 -1
@@ -0,0 +1,255 @@
1
+ -- Migration 0006: Usage tracking + config table (Iteration 16C)
2
+ --
3
+ -- New tables:
4
+ -- cerefox_config -- key-value config (e.g., usage_tracking_enabled)
5
+ -- cerefox_usage_log -- read operation log for analytics
6
+ --
7
+ -- New RPCs:
8
+ -- cerefox_get_config, cerefox_set_config
9
+ -- cerefox_log_usage
10
+ -- cerefox_list_usage_log
11
+ -- cerefox_usage_summary
12
+
13
+ -- ── 1. Config table ──────────────────────────────────────────────────────────
14
+
15
+ CREATE TABLE IF NOT EXISTS cerefox_config (
16
+ key TEXT PRIMARY KEY,
17
+ value TEXT NOT NULL
18
+ );
19
+
20
+ -- RLS: deny direct access; all access goes through SECURITY DEFINER RPCs
21
+ ALTER TABLE cerefox_config ENABLE ROW LEVEL SECURITY;
22
+
23
+ -- Seed default config
24
+ INSERT INTO cerefox_config (key, value)
25
+ VALUES ('usage_tracking_enabled', 'false')
26
+ ON CONFLICT (key) DO NOTHING;
27
+
28
+ -- ── 2. Usage log table ───────────────────────────────────────────────────────
29
+
30
+ CREATE TABLE IF NOT EXISTS cerefox_usage_log (
31
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
32
+ logged_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
33
+ operation TEXT NOT NULL,
34
+ access_path TEXT NOT NULL,
35
+ reader TEXT,
36
+ document_id UUID REFERENCES cerefox_documents(id) ON DELETE SET NULL,
37
+ project_id UUID REFERENCES cerefox_projects(id) ON DELETE SET NULL,
38
+ query_text TEXT,
39
+ result_count INT,
40
+ extra JSONB DEFAULT '{}'::JSONB
41
+ );
42
+
43
+ -- RLS: deny direct access
44
+ ALTER TABLE cerefox_usage_log ENABLE ROW LEVEL SECURITY;
45
+
46
+ -- Indexes
47
+ CREATE INDEX IF NOT EXISTS idx_usage_log_logged_at
48
+ ON cerefox_usage_log (logged_at DESC);
49
+ CREATE INDEX IF NOT EXISTS idx_usage_log_operation_logged_at
50
+ ON cerefox_usage_log (operation, logged_at DESC);
51
+ CREATE INDEX IF NOT EXISTS idx_usage_log_access_path
52
+ ON cerefox_usage_log (access_path);
53
+ CREATE INDEX IF NOT EXISTS idx_usage_log_reader
54
+ ON cerefox_usage_log (reader) WHERE reader IS NOT NULL;
55
+ CREATE INDEX IF NOT EXISTS idx_usage_log_document_id
56
+ ON cerefox_usage_log (document_id) WHERE document_id IS NOT NULL;
57
+
58
+ -- ── 3. Config RPCs ───────────────────────────────────────────────────────────
59
+
60
+ CREATE OR REPLACE FUNCTION cerefox_get_config(p_key TEXT)
61
+ RETURNS TEXT
62
+ LANGUAGE sql
63
+ SECURITY DEFINER
64
+ STABLE
65
+ SET search_path = public, pg_catalog
66
+ AS $$
67
+ SELECT value FROM cerefox_config WHERE key = p_key;
68
+ $$;
69
+
70
+ CREATE OR REPLACE FUNCTION cerefox_set_config(p_key TEXT, p_value TEXT)
71
+ RETURNS VOID
72
+ LANGUAGE plpgsql
73
+ SECURITY DEFINER
74
+ SET search_path = public, pg_catalog
75
+ AS $$
76
+ DECLARE
77
+ v_allowed TEXT[] := ARRAY['usage_tracking_enabled'];
78
+ BEGIN
79
+ IF NOT (p_key = ANY(v_allowed)) THEN
80
+ RAISE EXCEPTION 'Unknown config key: %. Allowed keys: %', p_key, v_allowed;
81
+ END IF;
82
+
83
+ INSERT INTO cerefox_config (key, value)
84
+ VALUES (p_key, p_value)
85
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
86
+ END;
87
+ $$;
88
+
89
+ -- ── 4. Log usage RPC ─────────────────────────────────────────────────────────
90
+ -- Checks config first; no-op if tracking is disabled.
91
+
92
+ CREATE OR REPLACE FUNCTION cerefox_log_usage(
93
+ p_operation TEXT,
94
+ p_access_path TEXT,
95
+ p_reader TEXT DEFAULT NULL,
96
+ p_document_id UUID DEFAULT NULL,
97
+ p_project_id UUID DEFAULT NULL,
98
+ p_query_text TEXT DEFAULT NULL,
99
+ p_result_count INT DEFAULT NULL,
100
+ p_extra JSONB DEFAULT '{}'::JSONB
101
+ )
102
+ RETURNS VOID
103
+ LANGUAGE plpgsql
104
+ SECURITY DEFINER
105
+ SET search_path = public, pg_catalog
106
+ AS $$
107
+ DECLARE
108
+ v_enabled TEXT;
109
+ BEGIN
110
+ SELECT value INTO v_enabled FROM cerefox_config WHERE key = 'usage_tracking_enabled';
111
+ IF v_enabled IS NULL OR v_enabled != 'true' THEN
112
+ RETURN; -- tracking disabled; no-op
113
+ END IF;
114
+
115
+ INSERT INTO cerefox_usage_log (
116
+ operation, access_path, reader, document_id, project_id,
117
+ query_text, result_count, extra
118
+ ) VALUES (
119
+ p_operation, p_access_path, p_reader, p_document_id, p_project_id,
120
+ p_query_text, p_result_count, p_extra
121
+ );
122
+ END;
123
+ $$;
124
+
125
+ -- ── 5. List usage log RPC ────────────────────────────────────────────────────
126
+
127
+ CREATE OR REPLACE FUNCTION cerefox_list_usage_log(
128
+ p_start TIMESTAMPTZ DEFAULT NULL,
129
+ p_end TIMESTAMPTZ DEFAULT NULL,
130
+ p_operation TEXT DEFAULT NULL,
131
+ p_access_path TEXT DEFAULT NULL,
132
+ p_reader TEXT DEFAULT NULL,
133
+ p_project_id UUID DEFAULT NULL,
134
+ p_limit INT DEFAULT 100
135
+ )
136
+ RETURNS TABLE (
137
+ id UUID,
138
+ logged_at TIMESTAMPTZ,
139
+ operation TEXT,
140
+ access_path TEXT,
141
+ reader TEXT,
142
+ document_id UUID,
143
+ doc_title TEXT,
144
+ project_id UUID,
145
+ query_text TEXT,
146
+ result_count INT,
147
+ extra JSONB
148
+ )
149
+ LANGUAGE sql
150
+ SECURITY DEFINER
151
+ STABLE
152
+ SET search_path = public, pg_catalog
153
+ AS $$
154
+ SELECT
155
+ ul.id,
156
+ ul.logged_at,
157
+ ul.operation,
158
+ ul.access_path,
159
+ ul.reader,
160
+ ul.document_id,
161
+ d.title AS doc_title,
162
+ ul.project_id,
163
+ ul.query_text,
164
+ ul.result_count,
165
+ ul.extra
166
+ FROM cerefox_usage_log ul
167
+ LEFT JOIN cerefox_documents d ON ul.document_id = d.id
168
+ WHERE (p_start IS NULL OR ul.logged_at >= p_start)
169
+ AND (p_end IS NULL OR ul.logged_at <= p_end)
170
+ AND (p_operation IS NULL OR ul.operation = p_operation)
171
+ AND (p_access_path IS NULL OR ul.access_path = p_access_path)
172
+ AND (p_reader IS NULL OR ul.reader = p_reader)
173
+ AND (p_project_id IS NULL OR ul.project_id = p_project_id)
174
+ ORDER BY ul.logged_at DESC
175
+ LIMIT p_limit;
176
+ $$;
177
+
178
+ -- ── 6. Usage summary RPC ─────────────────────────────────────────────────────
179
+ -- Returns a JSON object with aggregated stats for the analytics page.
180
+
181
+ CREATE OR REPLACE FUNCTION cerefox_usage_summary(
182
+ p_start TIMESTAMPTZ DEFAULT NULL,
183
+ p_end TIMESTAMPTZ DEFAULT NULL,
184
+ p_project_id UUID DEFAULT NULL,
185
+ p_access_path TEXT DEFAULT NULL
186
+ )
187
+ RETURNS JSON
188
+ LANGUAGE plpgsql
189
+ SECURITY DEFINER
190
+ STABLE
191
+ SET search_path = public, pg_catalog
192
+ AS $$
193
+ DECLARE
194
+ v_result JSON;
195
+ BEGIN
196
+ WITH filtered AS (
197
+ SELECT *
198
+ FROM cerefox_usage_log ul
199
+ WHERE (p_start IS NULL OR ul.logged_at >= p_start)
200
+ AND (p_end IS NULL OR ul.logged_at <= p_end)
201
+ AND (p_project_id IS NULL OR ul.project_id = p_project_id)
202
+ AND (p_access_path IS NULL OR ul.access_path = p_access_path)
203
+ ),
204
+ ops_by_day AS (
205
+ SELECT
206
+ DATE(logged_at) AS day,
207
+ COUNT(*) AS count
208
+ FROM filtered
209
+ GROUP BY DATE(logged_at)
210
+ ORDER BY day
211
+ ),
212
+ ops_by_operation AS (
213
+ SELECT operation, COUNT(*) AS count
214
+ FROM filtered
215
+ GROUP BY operation
216
+ ORDER BY count DESC
217
+ ),
218
+ ops_by_access_path AS (
219
+ SELECT access_path, COUNT(*) AS count
220
+ FROM filtered
221
+ GROUP BY access_path
222
+ ORDER BY count DESC
223
+ ),
224
+ top_documents AS (
225
+ SELECT
226
+ f.document_id,
227
+ d.title AS doc_title,
228
+ COUNT(*) AS count
229
+ FROM filtered f
230
+ JOIN cerefox_documents d ON f.document_id = d.id
231
+ WHERE f.document_id IS NOT NULL
232
+ GROUP BY f.document_id, d.title
233
+ ORDER BY count DESC
234
+ LIMIT 10
235
+ ),
236
+ top_readers AS (
237
+ SELECT reader, COUNT(*) AS count
238
+ FROM filtered
239
+ WHERE reader IS NOT NULL
240
+ GROUP BY reader
241
+ ORDER BY count DESC
242
+ LIMIT 10
243
+ )
244
+ SELECT json_build_object(
245
+ 'total_count', (SELECT COUNT(*) FROM filtered),
246
+ 'ops_by_day', COALESCE((SELECT json_agg(json_build_object('day', day, 'count', count)) FROM ops_by_day), '[]'::JSON),
247
+ 'ops_by_operation', COALESCE((SELECT json_agg(json_build_object('operation', operation, 'count', count)) FROM ops_by_operation), '[]'::JSON),
248
+ 'ops_by_access_path', COALESCE((SELECT json_agg(json_build_object('access_path', access_path, 'count', count)) FROM ops_by_access_path), '[]'::JSON),
249
+ 'top_documents', COALESCE((SELECT json_agg(json_build_object('document_id', document_id, 'doc_title', doc_title, 'count', count)) FROM top_documents), '[]'::JSON),
250
+ 'top_readers', COALESCE((SELECT json_agg(json_build_object('reader', reader, 'count', count)) FROM top_readers), '[]'::JSON)
251
+ ) INTO v_result;
252
+
253
+ RETURN v_result;
254
+ END;
255
+ $$;
@@ -0,0 +1,178 @@
1
+ -- Migration 0007: Rename reader to requestor in cerefox_usage_log
2
+ --
3
+ -- The usage log tracks ALL operations (both reads and writes), not just reads.
4
+ -- The "reader" column name implied read-only tracking. "requestor" is a neutral
5
+ -- term that covers both the agent/user performing a search (read) and the
6
+ -- agent/user performing an ingest (write).
7
+
8
+ -- 1. Rename the column
9
+ ALTER TABLE cerefox_usage_log RENAME COLUMN reader TO requestor;
10
+
11
+ -- 2. Rename the index (Postgres doesn't have RENAME INDEX, so drop and recreate)
12
+ DROP INDEX IF EXISTS idx_usage_log_reader;
13
+ CREATE INDEX IF NOT EXISTS idx_usage_log_requestor
14
+ ON cerefox_usage_log (requestor) WHERE requestor IS NOT NULL;
15
+
16
+ -- 3. Drop and recreate cerefox_log_usage with renamed parameter
17
+ DROP FUNCTION IF EXISTS cerefox_log_usage(TEXT, TEXT, TEXT, UUID, UUID, TEXT, INT, JSONB);
18
+
19
+ CREATE OR REPLACE FUNCTION cerefox_log_usage(
20
+ p_operation TEXT,
21
+ p_access_path TEXT,
22
+ p_requestor TEXT DEFAULT NULL,
23
+ p_document_id UUID DEFAULT NULL,
24
+ p_project_id UUID DEFAULT NULL,
25
+ p_query_text TEXT DEFAULT NULL,
26
+ p_result_count INT DEFAULT NULL,
27
+ p_extra JSONB DEFAULT '{}'::JSONB
28
+ )
29
+ RETURNS VOID
30
+ LANGUAGE plpgsql
31
+ SECURITY DEFINER
32
+ SET search_path = public, pg_catalog
33
+ AS $$
34
+ DECLARE
35
+ v_enabled TEXT;
36
+ BEGIN
37
+ SELECT value INTO v_enabled FROM cerefox_config WHERE key = 'usage_tracking_enabled';
38
+ IF v_enabled IS NULL OR v_enabled != 'true' THEN
39
+ RETURN;
40
+ END IF;
41
+
42
+ INSERT INTO cerefox_usage_log (
43
+ operation, access_path, requestor, document_id, project_id,
44
+ query_text, result_count, extra
45
+ ) VALUES (
46
+ p_operation, p_access_path, p_requestor, p_document_id, p_project_id,
47
+ p_query_text, p_result_count, p_extra
48
+ );
49
+ END;
50
+ $$;
51
+
52
+ -- 4. Drop and recreate cerefox_list_usage_log with renamed column
53
+ DROP FUNCTION IF EXISTS cerefox_list_usage_log(TIMESTAMPTZ, TIMESTAMPTZ, TEXT, TEXT, TEXT, UUID, INT);
54
+
55
+ CREATE OR REPLACE FUNCTION cerefox_list_usage_log(
56
+ p_start TIMESTAMPTZ DEFAULT NULL,
57
+ p_end TIMESTAMPTZ DEFAULT NULL,
58
+ p_operation TEXT DEFAULT NULL,
59
+ p_access_path TEXT DEFAULT NULL,
60
+ p_requestor TEXT DEFAULT NULL,
61
+ p_project_id UUID DEFAULT NULL,
62
+ p_limit INT DEFAULT 100
63
+ )
64
+ RETURNS TABLE (
65
+ id UUID,
66
+ logged_at TIMESTAMPTZ,
67
+ operation TEXT,
68
+ access_path TEXT,
69
+ requestor TEXT,
70
+ document_id UUID,
71
+ doc_title TEXT,
72
+ project_id UUID,
73
+ query_text TEXT,
74
+ result_count INT,
75
+ extra JSONB
76
+ )
77
+ LANGUAGE sql
78
+ SECURITY DEFINER
79
+ STABLE
80
+ SET search_path = public, pg_catalog
81
+ AS $$
82
+ SELECT
83
+ ul.id,
84
+ ul.logged_at,
85
+ ul.operation,
86
+ ul.access_path,
87
+ ul.requestor,
88
+ ul.document_id,
89
+ d.title AS doc_title,
90
+ ul.project_id,
91
+ ul.query_text,
92
+ ul.result_count,
93
+ ul.extra
94
+ FROM cerefox_usage_log ul
95
+ LEFT JOIN cerefox_documents d ON ul.document_id = d.id
96
+ WHERE (p_start IS NULL OR ul.logged_at >= p_start)
97
+ AND (p_end IS NULL OR ul.logged_at <= p_end)
98
+ AND (p_operation IS NULL OR ul.operation = p_operation)
99
+ AND (p_access_path IS NULL OR ul.access_path = p_access_path)
100
+ AND (p_requestor IS NULL OR ul.requestor = p_requestor)
101
+ AND (p_project_id IS NULL OR ul.project_id = p_project_id)
102
+ ORDER BY ul.logged_at DESC
103
+ LIMIT p_limit;
104
+ $$;
105
+
106
+ -- 5. Drop and recreate cerefox_usage_summary with renamed field
107
+ DROP FUNCTION IF EXISTS cerefox_usage_summary(TIMESTAMPTZ, TIMESTAMPTZ, UUID, TEXT);
108
+
109
+ CREATE OR REPLACE FUNCTION cerefox_usage_summary(
110
+ p_start TIMESTAMPTZ DEFAULT NULL,
111
+ p_end TIMESTAMPTZ DEFAULT NULL,
112
+ p_project_id UUID DEFAULT NULL,
113
+ p_access_path TEXT DEFAULT NULL
114
+ )
115
+ RETURNS JSON
116
+ LANGUAGE plpgsql
117
+ SECURITY DEFINER
118
+ STABLE
119
+ SET search_path = public, pg_catalog
120
+ AS $$
121
+ DECLARE
122
+ v_result JSON;
123
+ BEGIN
124
+ WITH filtered AS (
125
+ SELECT *
126
+ FROM cerefox_usage_log ul
127
+ WHERE (p_start IS NULL OR ul.logged_at >= p_start)
128
+ AND (p_end IS NULL OR ul.logged_at <= p_end)
129
+ AND (p_project_id IS NULL OR ul.project_id = p_project_id)
130
+ AND (p_access_path IS NULL OR ul.access_path = p_access_path)
131
+ ),
132
+ ops_by_day AS (
133
+ SELECT DATE(logged_at) AS day, COUNT(*) AS count
134
+ FROM filtered
135
+ GROUP BY DATE(logged_at)
136
+ ORDER BY day
137
+ ),
138
+ ops_by_operation AS (
139
+ SELECT operation, COUNT(*) AS count
140
+ FROM filtered
141
+ GROUP BY operation
142
+ ORDER BY count DESC
143
+ ),
144
+ ops_by_access_path AS (
145
+ SELECT access_path, COUNT(*) AS count
146
+ FROM filtered
147
+ GROUP BY access_path
148
+ ORDER BY count DESC
149
+ ),
150
+ top_documents AS (
151
+ SELECT f.document_id, d.title AS doc_title, COUNT(*) AS count
152
+ FROM filtered f
153
+ JOIN cerefox_documents d ON f.document_id = d.id
154
+ WHERE f.document_id IS NOT NULL
155
+ GROUP BY f.document_id, d.title
156
+ ORDER BY count DESC
157
+ LIMIT 10
158
+ ),
159
+ top_requestors AS (
160
+ SELECT requestor, COUNT(*) AS count
161
+ FROM filtered
162
+ WHERE requestor IS NOT NULL
163
+ GROUP BY requestor
164
+ ORDER BY count DESC
165
+ LIMIT 10
166
+ )
167
+ SELECT json_build_object(
168
+ 'total_count', (SELECT COUNT(*) FROM filtered),
169
+ 'ops_by_day', COALESCE((SELECT json_agg(json_build_object('day', day, 'count', count)) FROM ops_by_day), '[]'::JSON),
170
+ 'ops_by_operation', COALESCE((SELECT json_agg(json_build_object('operation', operation, 'count', count)) FROM ops_by_operation), '[]'::JSON),
171
+ 'ops_by_access_path', COALESCE((SELECT json_agg(json_build_object('access_path', access_path, 'count', count)) FROM ops_by_access_path), '[]'::JSON),
172
+ 'top_documents', COALESCE((SELECT json_agg(json_build_object('document_id', document_id, 'doc_title', doc_title, 'count', count)) FROM top_documents), '[]'::JSON),
173
+ 'top_requestors', COALESCE((SELECT json_agg(json_build_object('requestor', requestor, 'count', count)) FROM top_requestors), '[]'::JSON)
174
+ ) INTO v_result;
175
+
176
+ RETURN v_result;
177
+ END;
178
+ $$;
@@ -0,0 +1,130 @@
1
+ -- Migration 0008: Soft delete for documents
2
+ --
3
+ -- Adds deleted_at column to cerefox_documents. "Delete" sets the timestamp
4
+ -- instead of cascade-deleting. Search indexes exclude soft-deleted docs.
5
+ -- A separate "purge" operation does the real cascade delete.
6
+ -- Recovery is just clearing the deleted_at timestamp.
7
+
8
+ -- 1. Add deleted_at column (nullable, default NULL = not deleted)
9
+ ALTER TABLE cerefox_documents ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ DEFAULT NULL;
10
+
11
+ -- 1b. Add 'restore' to the audit log operation CHECK constraint
12
+ ALTER TABLE cerefox_audit_log DROP CONSTRAINT IF EXISTS cerefox_audit_log_operation_check;
13
+ ALTER TABLE cerefox_audit_log ADD CONSTRAINT cerefox_audit_log_operation_check CHECK (
14
+ operation IN ('create', 'update-content', 'update-metadata', 'delete',
15
+ 'status-change', 'archive', 'unarchive', 'restore')
16
+ );
17
+
18
+ -- 2. Index for finding soft-deleted documents efficiently
19
+ CREATE INDEX IF NOT EXISTS idx_documents_deleted_at
20
+ ON cerefox_documents (deleted_at) WHERE deleted_at IS NOT NULL;
21
+
22
+ -- 3. Update cerefox_delete_document to soft-delete instead of cascade-delete.
23
+ -- The old RPC did: audit entry + CASCADE DELETE.
24
+ -- The new RPC does: audit entry + SET deleted_at = NOW().
25
+ -- Drop the old signature first (it takes different params across versions).
26
+
27
+ DROP FUNCTION IF EXISTS cerefox_delete_document(UUID);
28
+
29
+ CREATE OR REPLACE FUNCTION cerefox_delete_document(
30
+ p_document_id UUID
31
+ )
32
+ RETURNS VOID
33
+ LANGUAGE plpgsql
34
+ SECURITY DEFINER
35
+ SET search_path = public, pg_catalog
36
+ AS $$
37
+ DECLARE
38
+ v_title TEXT;
39
+ v_total_chars INT;
40
+ BEGIN
41
+ -- Get document info for audit entry
42
+ SELECT title, total_chars INTO v_title, v_total_chars
43
+ FROM cerefox_documents WHERE id = p_document_id;
44
+
45
+ IF v_title IS NULL THEN
46
+ RETURN; -- Document doesn't exist
47
+ END IF;
48
+
49
+ -- Soft delete: set deleted_at timestamp
50
+ UPDATE cerefox_documents SET deleted_at = NOW() WHERE id = p_document_id;
51
+
52
+ -- Audit entry
53
+ INSERT INTO cerefox_audit_log (
54
+ document_id, operation, author, author_type,
55
+ size_before, size_after, description
56
+ ) VALUES (
57
+ p_document_id, 'delete', 'unknown', 'user',
58
+ v_total_chars, 0,
59
+ format('Soft-deleted document: %s (%s chars)', v_title, v_total_chars)
60
+ );
61
+ END;
62
+ $$;
63
+
64
+ -- 4. New RPC: restore a soft-deleted document
65
+ CREATE OR REPLACE FUNCTION cerefox_restore_document(
66
+ p_document_id UUID
67
+ )
68
+ RETURNS VOID
69
+ LANGUAGE plpgsql
70
+ SECURITY DEFINER
71
+ SET search_path = public, pg_catalog
72
+ AS $$
73
+ DECLARE
74
+ v_title TEXT;
75
+ v_total_chars INT;
76
+ BEGIN
77
+ SELECT title, total_chars INTO v_title, v_total_chars
78
+ FROM cerefox_documents WHERE id = p_document_id AND deleted_at IS NOT NULL;
79
+
80
+ IF v_title IS NULL THEN
81
+ RETURN; -- Not found or not deleted
82
+ END IF;
83
+
84
+ UPDATE cerefox_documents SET deleted_at = NULL WHERE id = p_document_id;
85
+
86
+ INSERT INTO cerefox_audit_log (
87
+ document_id, operation, author, author_type,
88
+ size_before, size_after, description
89
+ ) VALUES (
90
+ p_document_id, 'restore', 'unknown', 'user',
91
+ 0, v_total_chars,
92
+ format('Restored document: %s', v_title)
93
+ );
94
+ END;
95
+ $$;
96
+
97
+ -- 5. New RPC: permanently purge a soft-deleted document
98
+ CREATE OR REPLACE FUNCTION cerefox_purge_document(
99
+ p_document_id UUID
100
+ )
101
+ RETURNS VOID
102
+ LANGUAGE plpgsql
103
+ SECURITY DEFINER
104
+ SET search_path = public, pg_catalog
105
+ AS $$
106
+ DECLARE
107
+ v_title TEXT;
108
+ v_total_chars INT;
109
+ BEGIN
110
+ SELECT title, total_chars INTO v_title, v_total_chars
111
+ FROM cerefox_documents WHERE id = p_document_id AND deleted_at IS NOT NULL;
112
+
113
+ IF v_title IS NULL THEN
114
+ RETURN; -- Not found or not soft-deleted (can only purge soft-deleted docs)
115
+ END IF;
116
+
117
+ -- Audit entry BEFORE delete (FK will SET NULL on the audit entry's document_id)
118
+ INSERT INTO cerefox_audit_log (
119
+ document_id, operation, author, author_type,
120
+ size_before, size_after, description
121
+ ) VALUES (
122
+ p_document_id, 'delete', 'unknown', 'user',
123
+ v_total_chars, 0,
124
+ format('Permanently deleted document: %s (%s chars)', v_title, v_total_chars)
125
+ );
126
+
127
+ -- Real cascade delete
128
+ DELETE FROM cerefox_documents WHERE id = p_document_id;
129
+ END;
130
+ $$;
@@ -0,0 +1,20 @@
1
+ -- Migration 0009: Add 'restore' to audit log operation CHECK constraint
2
+ --
3
+ -- The soft delete feature (0008) used 'unarchive' for restore-from-trash
4
+ -- operations. This was semantically incorrect -- 'unarchive' is for version
5
+ -- archival toggling. This migration adds 'restore' as a proper operation type.
6
+ --
7
+ -- Safe to re-run: DROP CONSTRAINT IF EXISTS handles idempotency.
8
+
9
+ ALTER TABLE cerefox_audit_log DROP CONSTRAINT IF EXISTS cerefox_audit_log_operation_check;
10
+ ALTER TABLE cerefox_audit_log ADD CONSTRAINT cerefox_audit_log_operation_check CHECK (
11
+ operation IN ('create', 'update-content', 'update-metadata', 'delete',
12
+ 'status-change', 'archive', 'unarchive', 'restore')
13
+ );
14
+
15
+ -- Also fix any existing audit entries that used 'unarchive' for restore-from-trash
16
+ -- (they have description starting with 'Restored document:')
17
+ UPDATE cerefox_audit_log
18
+ SET operation = 'restore'
19
+ WHERE operation = 'unarchive'
20
+ AND description LIKE 'Restored document:%';
@@ -0,0 +1,12 @@
1
+ -- Migration 0010: Seed requestor enforcement config defaults
2
+ --
3
+ -- Adds two new config keys for optional requestor identity enforcement
4
+ -- on MCP tool calls. Both default to "off" for backward compatibility.
5
+
6
+ INSERT INTO cerefox_config (key, value)
7
+ VALUES ('require_requestor_identity', 'false')
8
+ ON CONFLICT (key) DO NOTHING;
9
+
10
+ INSERT INTO cerefox_config (key, value)
11
+ VALUES ('requestor_identity_format', '^[a-zA-Z0-9_:.\- ]+$')
12
+ ON CONFLICT (key) DO NOTHING;
@@ -0,0 +1,48 @@
1
+ -- Migration 0011: Title Boosting for FTS and Semantic Search
2
+ --
3
+ -- Changes the fts column on cerefox_chunks from GENERATED ALWAYS AS to a
4
+ -- regular tsvector column. This allows the document title (from
5
+ -- cerefox_documents) to be included at weight A in the FTS index, which
6
+ -- is impossible with a GENERATED column (cannot reference other tables).
7
+ --
8
+ -- After this migration, fts is set by the ingestion pipeline:
9
+ -- setweight(to_tsvector('english', doc_title), 'A') -- document title
10
+ -- || setweight(to_tsvector('english', chunk_title), 'A') -- heading title
11
+ -- || setweight(to_tsvector('english', content), 'B') -- body content
12
+ --
13
+ -- The cerefox_ingest_document RPC computes this automatically for all new
14
+ -- or updated documents. The cerefox_update_chunk_fts RPC handles the
15
+ -- title-change path (title changed but content unchanged).
16
+ --
17
+ -- Existing chunks retain their old tsvectors (computed without doc title).
18
+ -- New ingestion automatically uses the new formula. Running
19
+ -- scripts/reindex_all.py updates all existing chunks (optional but
20
+ -- recommended for complete title-boosted search coverage).
21
+ --
22
+ -- Applied by: uv run python scripts/db_migrate.py
23
+
24
+ -- Step 1: Drop the GENERATED expression on fts.
25
+ -- The column stays as a regular tsvector; existing values are preserved.
26
+ ALTER TABLE cerefox_chunks ALTER COLUMN fts DROP EXPRESSION;
27
+
28
+ -- Step 2: Add cerefox_update_chunk_fts RPC.
29
+ -- Called when a document title changes (content unchanged) to refresh the
30
+ -- FTS index for all current chunks without creating a version snapshot.
31
+ DROP FUNCTION IF EXISTS cerefox_update_chunk_fts(UUID, TEXT);
32
+ CREATE FUNCTION cerefox_update_chunk_fts(
33
+ p_document_id UUID,
34
+ p_new_title TEXT
35
+ )
36
+ RETURNS VOID
37
+ LANGUAGE sql
38
+ SECURITY DEFINER
39
+ SET search_path = public, pg_catalog
40
+ AS $$
41
+ UPDATE cerefox_chunks
42
+ SET fts =
43
+ setweight(to_tsvector('english', COALESCE(p_new_title, '')), 'A') ||
44
+ setweight(to_tsvector('english', COALESCE(title, '')), 'A') ||
45
+ setweight(to_tsvector('english', COALESCE(content, '')), 'B')
46
+ WHERE document_id = p_document_id
47
+ AND version_id IS NULL;
48
+ $$;