@cerefox/memory 1.0.8 → 1.1.0-beta.2
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.
- package/AGENT_QUICK_REFERENCE.md +11 -2
- package/dist/bin/cerefox.js +956 -201
- package/dist/frontend/assets/index-C1JXZA9m.css +1 -0
- package/dist/frontend/assets/index-CMNl_LF9.js +121 -0
- package/dist/frontend/assets/index-CMNl_LF9.js.map +1 -0
- package/dist/frontend/index.html +2 -2
- package/dist/server-assets/_shared/ef-meta/index.ts +2 -2
- package/dist/server-assets/_shared/mcp-tools/_utils.ts +26 -0
- package/dist/server-assets/_shared/mcp-tools/feature-flags.ts +68 -0
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +4 -4
- package/dist/server-assets/_shared/mcp-tools/index.ts +42 -1
- package/dist/server-assets/_shared/mcp-tools/relations.ts +284 -0
- package/dist/server-assets/_shared/mcp-tools/search.ts +14 -8
- package/dist/server-assets/db/migrations/0014_document_relations.sql +64 -0
- package/dist/server-assets/db/rpcs.sql +294 -14
- package/dist/server-assets/db/schema.sql +40 -2
- package/dist/server-assets/supabase/functions/cerefox-mcp/index.ts +25 -8
- package/docs/guides/cli.md +0 -36
- package/docs/guides/configuration.md +21 -0
- package/docs/guides/migration-1.0.md +1 -3
- package/docs/guides/ops-scripts.md +2 -2
- package/docs/guides/setup-local.md +3 -1
- package/docs/guides/setup-supabase.md +35 -4
- package/docs/guides/upgrading.md +1 -1
- package/package.json +3 -3
- package/dist/frontend/assets/index-Asx5wD7g.css +0 -1
- package/dist/frontend/assets/index-VeqA60-v.js +0 -125
- package/dist/frontend/assets/index-VeqA60-v.js.map +0 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
-- 0014_document_relations.sql — typed edges between documents (iteration 29).
|
|
2
|
+
--
|
|
3
|
+
-- Adds the relation graph on top of the existing document model:
|
|
4
|
+
-- * cerefox_document_relations — typed, directed edges (source → target)
|
|
5
|
+
-- * cerefox_documents.lifecycle_status — 'active' | 'superseded' | 'stale' | 'archived'
|
|
6
|
+
--
|
|
7
|
+
-- Design: docs/research/document-relations-and-semantic-graph.md §2.2, §3.
|
|
8
|
+
-- Idempotent: safe to re-run.
|
|
9
|
+
|
|
10
|
+
CREATE TABLE IF NOT EXISTS cerefox_document_relations (
|
|
11
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
12
|
+
source_id UUID NOT NULL REFERENCES cerefox_documents(id) ON DELETE CASCADE,
|
|
13
|
+
target_id UUID NOT NULL REFERENCES cerefox_documents(id) ON DELETE CASCADE,
|
|
14
|
+
-- Free-text by design: agents define new types without a migration. The
|
|
15
|
+
-- type dictionary (in the RPCs) gives known types behaviour; unknown types
|
|
16
|
+
-- are stored and returned, just without special handling.
|
|
17
|
+
rel_type TEXT NOT NULL,
|
|
18
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
19
|
+
author TEXT NOT NULL DEFAULT 'unknown',
|
|
20
|
+
author_type TEXT NOT NULL DEFAULT 'agent',
|
|
21
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
22
|
+
-- One edge of a given type per ordered pair; different types may coexist.
|
|
23
|
+
UNIQUE (source_id, target_id, rel_type),
|
|
24
|
+
-- A document relating to itself is always a mistake, and self-edges would
|
|
25
|
+
-- make traversal loop.
|
|
26
|
+
CONSTRAINT cerefox_relations_no_self_edge CHECK (source_id <> target_id)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_cerefox_relations_source ON cerefox_document_relations(source_id);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_cerefox_relations_target ON cerefox_document_relations(target_id);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_cerefox_relations_type ON cerefox_document_relations(rel_type);
|
|
32
|
+
|
|
33
|
+
-- Lifecycle status: how a document stands relative to the rest of the graph.
|
|
34
|
+
-- Distinct from review_status (editorial state) and deleted_at (existence).
|
|
35
|
+
ALTER TABLE cerefox_documents
|
|
36
|
+
ADD COLUMN IF NOT EXISTS lifecycle_status TEXT NOT NULL DEFAULT 'active';
|
|
37
|
+
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_cerefox_docs_lifecycle
|
|
39
|
+
ON cerefox_documents(lifecycle_status)
|
|
40
|
+
WHERE lifecycle_status <> 'active';
|
|
41
|
+
|
|
42
|
+
-- Data-API grants for the new table (migration 0013 / #26: privileges are
|
|
43
|
+
-- explicit now, and a new table gets none by default).
|
|
44
|
+
DO $$
|
|
45
|
+
BEGIN
|
|
46
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN
|
|
47
|
+
GRANT SELECT, INSERT, UPDATE, DELETE
|
|
48
|
+
ON TABLE public.cerefox_document_relations TO service_role;
|
|
49
|
+
END IF;
|
|
50
|
+
END
|
|
51
|
+
$$;
|
|
52
|
+
|
|
53
|
+
-- Relation writes are auditable operations; widen the audit-log constraint.
|
|
54
|
+
ALTER TABLE cerefox_audit_log DROP CONSTRAINT IF EXISTS cerefox_audit_log_operation_check;
|
|
55
|
+
ALTER TABLE cerefox_audit_log ADD CONSTRAINT cerefox_audit_log_operation_check CHECK (
|
|
56
|
+
operation IN ('create', 'update-content', 'update-metadata', 'delete',
|
|
57
|
+
'status-change', 'archive', 'unarchive', 'restore',
|
|
58
|
+
'relation-set', 'relation-delete')
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
-- Relations ship dormant: the MCP tools stay hidden until a deployment opts in.
|
|
62
|
+
INSERT INTO cerefox_config (key, value)
|
|
63
|
+
VALUES ('relations_enabled', 'false')
|
|
64
|
+
ON CONFLICT (key) DO NOTHING;
|
|
@@ -99,10 +99,13 @@ CREATE OR REPLACE FUNCTION cerefox_hybrid_search(
|
|
|
99
99
|
p_query_text TEXT,
|
|
100
100
|
p_query_embedding VECTOR(768),
|
|
101
101
|
p_match_count INT DEFAULT 10,
|
|
102
|
-
p_alpha FLOAT DEFAULT
|
|
102
|
+
p_alpha FLOAT DEFAULT NULL,
|
|
103
103
|
p_use_upgrade BOOLEAN DEFAULT FALSE,
|
|
104
104
|
p_project_id UUID DEFAULT NULL,
|
|
105
|
-
|
|
105
|
+
-- NULL means "not specified by the caller" (#133): resolve from
|
|
106
|
+
-- cerefox_config, else the built-in default. Callers that pass a value
|
|
107
|
+
-- still win — the chain is per-call > client .env > DB config > default.
|
|
108
|
+
p_min_score FLOAT DEFAULT NULL,
|
|
106
109
|
p_metadata_filter JSONB DEFAULT NULL,
|
|
107
110
|
-- 28I follow-up (v1.0.4): in OR-fallback mode, the unconditional FTS pass
|
|
108
111
|
-- requires at least this fraction of the query's meaningful (non-stopword,
|
|
@@ -110,7 +113,7 @@ CREATE OR REPLACE FUNCTION cerefox_hybrid_search(
|
|
|
110
113
|
-- meant 100% of terms — the pass this gate generalizes. Chunks below the
|
|
111
114
|
-- bar can still pass via the vector threshold, else they are
|
|
112
115
|
-- below-confidence material. 0 restores the pre-gate OR behavior.
|
|
113
|
-
p_min_term_coverage FLOAT DEFAULT
|
|
116
|
+
p_min_term_coverage FLOAT DEFAULT NULL
|
|
114
117
|
)
|
|
115
118
|
RETURNS TABLE (
|
|
116
119
|
chunk_id UUID,
|
|
@@ -164,6 +167,13 @@ DECLARE
|
|
|
164
167
|
seen_tokens TEXT[] := '{}';
|
|
165
168
|
total_tokens INT;
|
|
166
169
|
candidate_count INT := p_match_count * 5;
|
|
170
|
+
-- #133 resolution: caller value, else deployment config, else built-in.
|
|
171
|
+
v_min_score FLOAT := COALESCE(p_min_score,
|
|
172
|
+
cerefox_config_float('min_search_score', 0.5));
|
|
173
|
+
v_alpha FLOAT := COALESCE(p_alpha,
|
|
174
|
+
cerefox_config_float('search_alpha', 0.7));
|
|
175
|
+
v_min_coverage FLOAT := COALESCE(p_min_term_coverage,
|
|
176
|
+
cerefox_config_float('min_term_coverage', 0.5));
|
|
167
177
|
BEGIN
|
|
168
178
|
-- Build the OR-composed query: plainto each whitespace token (so tokens get
|
|
169
179
|
-- the same normalization/stemming as the AND path), skip stopword-only
|
|
@@ -217,7 +227,7 @@ BEGIN
|
|
|
217
227
|
WHEN and_matches OR total_tokens = 0 THEN TRUE
|
|
218
228
|
ELSE (SELECT COUNT(*) FROM unnest(tok_queries) tq
|
|
219
229
|
WHERE c.fts @@ tq)::FLOAT
|
|
220
|
-
>=
|
|
230
|
+
>= v_min_coverage * total_tokens
|
|
221
231
|
END AS coverage_ok
|
|
222
232
|
FROM cerefox_chunks c
|
|
223
233
|
JOIN cerefox_documents d ON c.document_id = d.id
|
|
@@ -261,8 +271,8 @@ BEGIN
|
|
|
261
271
|
combined AS (
|
|
262
272
|
SELECT
|
|
263
273
|
COALESCE(f.id, v.id) AS id,
|
|
264
|
-
(
|
|
265
|
-
(1.0 -
|
|
274
|
+
( v_alpha * COALESCE(v.vec_score, 0.0) +
|
|
275
|
+
(1.0 - v_alpha) * COALESCE(f.fts_score, 0.0)
|
|
266
276
|
) AS score,
|
|
267
277
|
COALESCE(v.vec_score, 0.0) AS vec_score,
|
|
268
278
|
-- TRUE when the chunk matched the @@ FTS operator WITH enough
|
|
@@ -282,7 +292,7 @@ BEGIN
|
|
|
282
292
|
-- results (no FTS match) are filtered by the cosine threshold.
|
|
283
293
|
flagged AS (
|
|
284
294
|
SELECT *,
|
|
285
|
-
(combined.has_fts_match OR combined.vec_score >=
|
|
295
|
+
(combined.has_fts_match OR combined.vec_score >= v_min_score) AS passes
|
|
286
296
|
FROM combined
|
|
287
297
|
),
|
|
288
298
|
any_pass AS (SELECT bool_or(fl.passes) AS ok FROM flagged fl),
|
|
@@ -349,7 +359,7 @@ CREATE OR REPLACE FUNCTION cerefox_fts_search(
|
|
|
349
359
|
p_metadata_filter JSONB DEFAULT NULL,
|
|
350
360
|
-- v1.0.4: see cerefox_hybrid_search. In OR-fallback mode results must
|
|
351
361
|
-- match at least this fraction of the query's meaningful terms.
|
|
352
|
-
p_min_term_coverage FLOAT DEFAULT
|
|
362
|
+
p_min_term_coverage FLOAT DEFAULT NULL
|
|
353
363
|
)
|
|
354
364
|
RETURNS TABLE (
|
|
355
365
|
chunk_id UUID,
|
|
@@ -383,6 +393,8 @@ DECLARE
|
|
|
383
393
|
tok_queries tsquery[] := '{}';
|
|
384
394
|
seen_tokens TEXT[] := '{}';
|
|
385
395
|
total_tokens INT;
|
|
396
|
+
v_min_coverage FLOAT := COALESCE(p_min_term_coverage,
|
|
397
|
+
cerefox_config_float('min_term_coverage', 0.5));
|
|
386
398
|
BEGIN
|
|
387
399
|
FOR tok IN SELECT unnest(regexp_split_to_array(trim(p_query_text), '\s+')) LOOP
|
|
388
400
|
tok_q := plainto_tsquery('english', tok);
|
|
@@ -443,7 +455,7 @@ BEGIN
|
|
|
443
455
|
-- returns only chunks matching enough of the query's terms.
|
|
444
456
|
AND (and_matches OR total_tokens = 0
|
|
445
457
|
OR (SELECT COUNT(*) FROM unnest(tok_queries) tq
|
|
446
|
-
WHERE c.fts @@ tq)::FLOAT >=
|
|
458
|
+
WHERE c.fts @@ tq)::FLOAT >= v_min_coverage * total_tokens)
|
|
447
459
|
AND (p_project_id IS NULL OR EXISTS (
|
|
448
460
|
SELECT 1 FROM cerefox_document_projects dp
|
|
449
461
|
WHERE dp.document_id = d.id AND dp.project_id = p_project_id
|
|
@@ -749,13 +761,15 @@ CREATE OR REPLACE FUNCTION cerefox_search_docs(
|
|
|
749
761
|
p_query_text TEXT,
|
|
750
762
|
p_query_embedding VECTOR(768),
|
|
751
763
|
p_match_count INT DEFAULT 5,
|
|
752
|
-
p_alpha FLOAT DEFAULT
|
|
764
|
+
p_alpha FLOAT DEFAULT NULL,
|
|
753
765
|
p_project_id UUID DEFAULT NULL,
|
|
754
|
-
p_min_score FLOAT DEFAULT
|
|
766
|
+
p_min_score FLOAT DEFAULT NULL,
|
|
755
767
|
p_small_to_big_threshold INT DEFAULT 20000,
|
|
756
768
|
p_context_window INT DEFAULT 1,
|
|
757
769
|
p_metadata_filter JSONB DEFAULT NULL,
|
|
758
|
-
|
|
770
|
+
-- NULL flows through to cerefox_hybrid_search, which resolves the
|
|
771
|
+
-- caller > cerefox_config > built-in chain in one place (#133).
|
|
772
|
+
p_min_term_coverage FLOAT DEFAULT NULL
|
|
759
773
|
)
|
|
760
774
|
RETURNS TABLE (
|
|
761
775
|
document_id UUID,
|
|
@@ -1740,6 +1754,239 @@ BEGIN
|
|
|
1740
1754
|
END;
|
|
1741
1755
|
$$;
|
|
1742
1756
|
|
|
1757
|
+
-- ══ Document relations (iteration 29) ═════════════════════════════════════════
|
|
1758
|
+
-- Typed edges between documents. Design:
|
|
1759
|
+
-- docs/research/document-relations-and-semantic-graph.md
|
|
1760
|
+
--
|
|
1761
|
+
-- Type dictionary: rel_type is free text (any string is accepted and returned),
|
|
1762
|
+
-- but a few KNOWN types carry behaviour. Keeping the dictionary in one place
|
|
1763
|
+
-- here — rather than scattered CASE expressions — is what lets the set/delete
|
|
1764
|
+
-- RPCs stay symmetric with each other.
|
|
1765
|
+
-- symmetric both directions are written/removed together
|
|
1766
|
+
-- supersedes target becomes 'superseded'
|
|
1767
|
+
-- contradicts both documents become 'stale'
|
|
1768
|
+
CREATE OR REPLACE FUNCTION cerefox_relation_is_symmetric(p_rel_type TEXT)
|
|
1769
|
+
RETURNS BOOLEAN
|
|
1770
|
+
LANGUAGE sql
|
|
1771
|
+
IMMUTABLE
|
|
1772
|
+
SET search_path = public, pg_catalog
|
|
1773
|
+
AS $$
|
|
1774
|
+
SELECT p_rel_type IN ('related_to', 'contradicts', 'duplicates');
|
|
1775
|
+
$$;
|
|
1776
|
+
|
|
1777
|
+
-- Create (or update) a relation. Symmetric types write both directions in one
|
|
1778
|
+
-- transaction, so a half-written pair is impossible.
|
|
1779
|
+
CREATE OR REPLACE FUNCTION cerefox_set_relation(
|
|
1780
|
+
p_source_id UUID,
|
|
1781
|
+
p_target_id UUID,
|
|
1782
|
+
p_rel_type TEXT,
|
|
1783
|
+
p_author TEXT DEFAULT 'unknown',
|
|
1784
|
+
p_author_type TEXT DEFAULT 'agent',
|
|
1785
|
+
p_metadata JSONB DEFAULT '{}'::jsonb
|
|
1786
|
+
)
|
|
1787
|
+
RETURNS TABLE (
|
|
1788
|
+
relation_id UUID,
|
|
1789
|
+
source_id UUID,
|
|
1790
|
+
target_id UUID,
|
|
1791
|
+
rel_type TEXT,
|
|
1792
|
+
is_symmetric BOOLEAN
|
|
1793
|
+
)
|
|
1794
|
+
LANGUAGE plpgsql
|
|
1795
|
+
SECURITY DEFINER
|
|
1796
|
+
SET search_path = public, pg_catalog
|
|
1797
|
+
AS $$
|
|
1798
|
+
-- The OUT columns (source_id, target_id, rel_type) share names with the
|
|
1799
|
+
-- table's columns; inside the INSERT/ON CONFLICT below the COLUMN is meant.
|
|
1800
|
+
#variable_conflict use_column
|
|
1801
|
+
DECLARE
|
|
1802
|
+
v_symmetric BOOLEAN := cerefox_relation_is_symmetric(p_rel_type);
|
|
1803
|
+
v_id UUID;
|
|
1804
|
+
BEGIN
|
|
1805
|
+
IF p_source_id = p_target_id THEN
|
|
1806
|
+
RAISE EXCEPTION 'A document cannot relate to itself (%).', p_source_id
|
|
1807
|
+
USING ERRCODE = '22023';
|
|
1808
|
+
END IF;
|
|
1809
|
+
IF p_rel_type IS NULL OR btrim(p_rel_type) = '' THEN
|
|
1810
|
+
RAISE EXCEPTION 'rel_type is required.' USING ERRCODE = '22023';
|
|
1811
|
+
END IF;
|
|
1812
|
+
-- Explicit existence checks give a clear error instead of an FK violation.
|
|
1813
|
+
IF NOT EXISTS (SELECT 1 FROM cerefox_documents d
|
|
1814
|
+
WHERE d.id = p_source_id AND d.deleted_at IS NULL) THEN
|
|
1815
|
+
RAISE EXCEPTION 'Source document % not found (or deleted).', p_source_id
|
|
1816
|
+
USING ERRCODE = '22023';
|
|
1817
|
+
END IF;
|
|
1818
|
+
IF NOT EXISTS (SELECT 1 FROM cerefox_documents d
|
|
1819
|
+
WHERE d.id = p_target_id AND d.deleted_at IS NULL) THEN
|
|
1820
|
+
RAISE EXCEPTION 'Target document % not found (or deleted).', p_target_id
|
|
1821
|
+
USING ERRCODE = '22023';
|
|
1822
|
+
END IF;
|
|
1823
|
+
|
|
1824
|
+
INSERT INTO cerefox_document_relations AS r
|
|
1825
|
+
(source_id, target_id, rel_type, metadata, author, author_type)
|
|
1826
|
+
VALUES (p_source_id, p_target_id, btrim(p_rel_type), COALESCE(p_metadata, '{}'::jsonb),
|
|
1827
|
+
p_author, p_author_type)
|
|
1828
|
+
ON CONFLICT (source_id, target_id, rel_type) DO UPDATE
|
|
1829
|
+
SET metadata = EXCLUDED.metadata,
|
|
1830
|
+
author = EXCLUDED.author,
|
|
1831
|
+
author_type = EXCLUDED.author_type
|
|
1832
|
+
RETURNING r.id INTO v_id;
|
|
1833
|
+
|
|
1834
|
+
IF v_symmetric THEN
|
|
1835
|
+
INSERT INTO cerefox_document_relations
|
|
1836
|
+
(source_id, target_id, rel_type, metadata, author, author_type)
|
|
1837
|
+
VALUES (p_target_id, p_source_id, btrim(p_rel_type),
|
|
1838
|
+
COALESCE(p_metadata, '{}'::jsonb), p_author, p_author_type)
|
|
1839
|
+
ON CONFLICT (source_id, target_id, rel_type) DO UPDATE
|
|
1840
|
+
SET metadata = EXCLUDED.metadata,
|
|
1841
|
+
author = EXCLUDED.author,
|
|
1842
|
+
author_type = EXCLUDED.author_type;
|
|
1843
|
+
END IF;
|
|
1844
|
+
|
|
1845
|
+
-- Lifecycle side effects (type dictionary).
|
|
1846
|
+
IF btrim(p_rel_type) = 'supersedes' THEN
|
|
1847
|
+
UPDATE cerefox_documents SET lifecycle_status = 'superseded'
|
|
1848
|
+
WHERE id = p_target_id;
|
|
1849
|
+
ELSIF btrim(p_rel_type) = 'contradicts' THEN
|
|
1850
|
+
UPDATE cerefox_documents SET lifecycle_status = 'stale'
|
|
1851
|
+
WHERE id IN (p_source_id, p_target_id);
|
|
1852
|
+
END IF;
|
|
1853
|
+
|
|
1854
|
+
INSERT INTO cerefox_audit_log (document_id, operation, author, author_type, description)
|
|
1855
|
+
VALUES (p_source_id, 'relation-set', p_author, p_author_type,
|
|
1856
|
+
format('%s → %s (%s)', p_source_id, p_target_id, btrim(p_rel_type)));
|
|
1857
|
+
|
|
1858
|
+
RETURN QUERY SELECT v_id, p_source_id, p_target_id, btrim(p_rel_type), v_symmetric;
|
|
1859
|
+
END;
|
|
1860
|
+
$$;
|
|
1861
|
+
|
|
1862
|
+
-- Remove a relation. Symmetric types remove both directions. Lifecycle side
|
|
1863
|
+
-- effects are NOT auto-reverted: a document marked superseded may have been
|
|
1864
|
+
-- superseded by something else too, and guessing wrong is worse than leaving
|
|
1865
|
+
-- the operator to set it explicitly.
|
|
1866
|
+
CREATE OR REPLACE FUNCTION cerefox_delete_relation(
|
|
1867
|
+
p_source_id UUID,
|
|
1868
|
+
p_target_id UUID,
|
|
1869
|
+
p_rel_type TEXT,
|
|
1870
|
+
p_author TEXT DEFAULT 'unknown',
|
|
1871
|
+
p_author_type TEXT DEFAULT 'agent'
|
|
1872
|
+
)
|
|
1873
|
+
RETURNS INT
|
|
1874
|
+
LANGUAGE plpgsql
|
|
1875
|
+
SECURITY DEFINER
|
|
1876
|
+
SET search_path = public, pg_catalog
|
|
1877
|
+
AS $$
|
|
1878
|
+
DECLARE
|
|
1879
|
+
v_deleted INT := 0;
|
|
1880
|
+
v_n INT;
|
|
1881
|
+
BEGIN
|
|
1882
|
+
DELETE FROM cerefox_document_relations
|
|
1883
|
+
WHERE source_id = p_source_id AND target_id = p_target_id
|
|
1884
|
+
AND rel_type = btrim(p_rel_type);
|
|
1885
|
+
GET DIAGNOSTICS v_n = ROW_COUNT; v_deleted := v_deleted + v_n;
|
|
1886
|
+
|
|
1887
|
+
IF cerefox_relation_is_symmetric(p_rel_type) THEN
|
|
1888
|
+
DELETE FROM cerefox_document_relations
|
|
1889
|
+
WHERE source_id = p_target_id AND target_id = p_source_id
|
|
1890
|
+
AND rel_type = btrim(p_rel_type);
|
|
1891
|
+
GET DIAGNOSTICS v_n = ROW_COUNT; v_deleted := v_deleted + v_n;
|
|
1892
|
+
END IF;
|
|
1893
|
+
|
|
1894
|
+
IF v_deleted > 0 THEN
|
|
1895
|
+
INSERT INTO cerefox_audit_log (document_id, operation, author, author_type, description)
|
|
1896
|
+
VALUES (p_source_id, 'relation-delete', p_author, p_author_type,
|
|
1897
|
+
format('%s → %s (%s)', p_source_id, p_target_id, btrim(p_rel_type)));
|
|
1898
|
+
END IF;
|
|
1899
|
+
RETURN v_deleted;
|
|
1900
|
+
END;
|
|
1901
|
+
$$;
|
|
1902
|
+
|
|
1903
|
+
-- All edges touching a document, in both directions. `direction` tells the
|
|
1904
|
+
-- caller which way each edge points relative to the document asked about.
|
|
1905
|
+
CREATE OR REPLACE FUNCTION cerefox_get_relations(p_document_id UUID)
|
|
1906
|
+
RETURNS TABLE (
|
|
1907
|
+
relation_id UUID,
|
|
1908
|
+
direction TEXT,
|
|
1909
|
+
rel_type TEXT,
|
|
1910
|
+
other_id UUID,
|
|
1911
|
+
other_title TEXT,
|
|
1912
|
+
other_lifecycle TEXT,
|
|
1913
|
+
metadata JSONB,
|
|
1914
|
+
author TEXT,
|
|
1915
|
+
created_at TIMESTAMPTZ
|
|
1916
|
+
)
|
|
1917
|
+
LANGUAGE sql
|
|
1918
|
+
SECURITY DEFINER
|
|
1919
|
+
STABLE
|
|
1920
|
+
SET search_path = public, pg_catalog
|
|
1921
|
+
AS $$
|
|
1922
|
+
SELECT r.id, 'outbound'::TEXT, r.rel_type, r.target_id, d.title,
|
|
1923
|
+
d.lifecycle_status, r.metadata, r.author, r.created_at
|
|
1924
|
+
FROM cerefox_document_relations r
|
|
1925
|
+
JOIN cerefox_documents d ON d.id = r.target_id
|
|
1926
|
+
WHERE r.source_id = p_document_id AND d.deleted_at IS NULL
|
|
1927
|
+
UNION ALL
|
|
1928
|
+
SELECT r.id, 'inbound'::TEXT, r.rel_type, r.source_id, d.title,
|
|
1929
|
+
d.lifecycle_status, r.metadata, r.author, r.created_at
|
|
1930
|
+
FROM cerefox_document_relations r
|
|
1931
|
+
JOIN cerefox_documents d ON d.id = r.source_id
|
|
1932
|
+
WHERE r.target_id = p_document_id AND d.deleted_at IS NULL
|
|
1933
|
+
ORDER BY 3, 9 DESC;
|
|
1934
|
+
$$;
|
|
1935
|
+
|
|
1936
|
+
-- Walk the graph from a document along ONE relation type. Depth > 1 follows
|
|
1937
|
+
-- chains (meaningful for e.g. `follows` / `reply_to`); a visited-set stops
|
|
1938
|
+
-- cycles, which free-text types make possible.
|
|
1939
|
+
CREATE OR REPLACE FUNCTION cerefox_get_neighbors(
|
|
1940
|
+
p_document_id UUID,
|
|
1941
|
+
p_rel_type TEXT,
|
|
1942
|
+
p_depth INT DEFAULT 1,
|
|
1943
|
+
p_from_time TIMESTAMPTZ DEFAULT NULL,
|
|
1944
|
+
p_to_time TIMESTAMPTZ DEFAULT NULL,
|
|
1945
|
+
p_limit INT DEFAULT 50
|
|
1946
|
+
)
|
|
1947
|
+
RETURNS TABLE (
|
|
1948
|
+
document_id UUID,
|
|
1949
|
+
title TEXT,
|
|
1950
|
+
lifecycle_status TEXT,
|
|
1951
|
+
depth INT,
|
|
1952
|
+
direction TEXT,
|
|
1953
|
+
doc_created_at TIMESTAMPTZ
|
|
1954
|
+
)
|
|
1955
|
+
LANGUAGE sql
|
|
1956
|
+
SECURITY DEFINER
|
|
1957
|
+
STABLE
|
|
1958
|
+
SET search_path = public, pg_catalog
|
|
1959
|
+
AS $$
|
|
1960
|
+
WITH RECURSIVE walk AS (
|
|
1961
|
+
SELECT p_document_id AS id, 0 AS depth, 'self'::TEXT AS direction,
|
|
1962
|
+
ARRAY[p_document_id] AS seen
|
|
1963
|
+
UNION ALL
|
|
1964
|
+
SELECT nxt.id, w.depth + 1, nxt.direction, w.seen || nxt.id
|
|
1965
|
+
FROM walk w
|
|
1966
|
+
CROSS JOIN LATERAL (
|
|
1967
|
+
SELECT r.target_id AS id, 'outbound'::TEXT AS direction
|
|
1968
|
+
FROM cerefox_document_relations r
|
|
1969
|
+
WHERE r.source_id = w.id AND r.rel_type = btrim(p_rel_type)
|
|
1970
|
+
UNION ALL
|
|
1971
|
+
SELECT r.source_id, 'inbound'::TEXT
|
|
1972
|
+
FROM cerefox_document_relations r
|
|
1973
|
+
WHERE r.target_id = w.id AND r.rel_type = btrim(p_rel_type)
|
|
1974
|
+
) nxt
|
|
1975
|
+
WHERE w.depth < GREATEST(p_depth, 1)
|
|
1976
|
+
AND NOT (nxt.id = ANY(w.seen)) -- cycle guard
|
|
1977
|
+
)
|
|
1978
|
+
SELECT DISTINCT ON (w.id)
|
|
1979
|
+
w.id, d.title, d.lifecycle_status, w.depth, w.direction, d.created_at
|
|
1980
|
+
FROM walk w
|
|
1981
|
+
JOIN cerefox_documents d ON d.id = w.id
|
|
1982
|
+
WHERE w.depth > 0
|
|
1983
|
+
AND d.deleted_at IS NULL
|
|
1984
|
+
AND (p_from_time IS NULL OR d.created_at >= p_from_time)
|
|
1985
|
+
AND (p_to_time IS NULL OR d.created_at <= p_to_time)
|
|
1986
|
+
ORDER BY w.id, w.depth
|
|
1987
|
+
LIMIT GREATEST(p_limit, 1);
|
|
1988
|
+
$$;
|
|
1989
|
+
|
|
1743
1990
|
-- ── cerefox_get_config / cerefox_set_config ──────────────────────────────────
|
|
1744
1991
|
-- Read/write key-value config from cerefox_config table.
|
|
1745
1992
|
|
|
@@ -1753,6 +2000,31 @@ AS $$
|
|
|
1753
2000
|
SELECT value FROM cerefox_config WHERE key = p_key;
|
|
1754
2001
|
$$;
|
|
1755
2002
|
|
|
2003
|
+
-- Numeric config reader with a fallback (v1.1.0, #133). Returns p_fallback
|
|
2004
|
+
-- when the key is unset or unparseable, so a malformed row can never break
|
|
2005
|
+
-- search — it just reverts to the built-in default.
|
|
2006
|
+
CREATE OR REPLACE FUNCTION cerefox_config_float(p_key TEXT, p_fallback FLOAT)
|
|
2007
|
+
RETURNS FLOAT
|
|
2008
|
+
LANGUAGE plpgsql
|
|
2009
|
+
SECURITY DEFINER
|
|
2010
|
+
STABLE
|
|
2011
|
+
SET search_path = public, pg_catalog
|
|
2012
|
+
AS $$
|
|
2013
|
+
DECLARE
|
|
2014
|
+
v_raw TEXT;
|
|
2015
|
+
v_num FLOAT;
|
|
2016
|
+
BEGIN
|
|
2017
|
+
SELECT value INTO v_raw FROM cerefox_config WHERE key = p_key;
|
|
2018
|
+
IF v_raw IS NULL OR v_raw = '' THEN RETURN p_fallback; END IF;
|
|
2019
|
+
BEGIN
|
|
2020
|
+
v_num := v_raw::FLOAT;
|
|
2021
|
+
EXCEPTION WHEN others THEN
|
|
2022
|
+
RETURN p_fallback;
|
|
2023
|
+
END;
|
|
2024
|
+
RETURN v_num;
|
|
2025
|
+
END;
|
|
2026
|
+
$$;
|
|
2027
|
+
|
|
1756
2028
|
CREATE OR REPLACE FUNCTION cerefox_set_config(p_key TEXT, p_value TEXT)
|
|
1757
2029
|
RETURNS VOID
|
|
1758
2030
|
LANGUAGE plpgsql
|
|
@@ -1760,7 +2032,15 @@ SECURITY DEFINER
|
|
|
1760
2032
|
SET search_path = public, pg_catalog
|
|
1761
2033
|
AS $$
|
|
1762
2034
|
DECLARE
|
|
1763
|
-
|
|
2035
|
+
-- Retrieval tunables (#133) join the governance keys: setting one here
|
|
2036
|
+
-- governs EVERY access path (CLI, local + remote MCP, Edge Functions, web),
|
|
2037
|
+
-- because they all resolve through these RPCs.
|
|
2038
|
+
v_allowed TEXT[] := ARRAY[
|
|
2039
|
+
'usage_tracking_enabled', 'require_requestor_identity', 'requestor_identity_format',
|
|
2040
|
+
'min_search_score', 'min_term_coverage', 'search_alpha',
|
|
2041
|
+
-- Optional features, off by default (iteration 29).
|
|
2042
|
+
'relations_enabled'
|
|
2043
|
+
];
|
|
1764
2044
|
BEGIN
|
|
1765
2045
|
IF NOT (p_key = ANY(v_allowed)) THEN
|
|
1766
2046
|
RAISE EXCEPTION 'Unknown config key: %. Allowed keys: %', p_key, v_allowed;
|
|
@@ -1954,7 +2234,7 @@ SET search_path = public, pg_catalog
|
|
|
1954
2234
|
AS $$
|
|
1955
2235
|
-- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
|
|
1956
2236
|
-- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
|
|
1957
|
-
SELECT '0.
|
|
2237
|
+
SELECT '0.10.1'::TEXT;
|
|
1958
2238
|
$$;
|
|
1959
2239
|
|
|
1960
2240
|
-- ── cerefox_content_format_stats ─────────────────────────────────────────────
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
-- Requires extensions: vector (pgvector), uuid-ossp
|
|
6
6
|
-- These are enabled at the top of db_deploy.py before this file is applied.
|
|
7
7
|
--
|
|
8
|
-
-- @version: 0.
|
|
8
|
+
-- @version: 0.10.1
|
|
9
9
|
-- The `@version` marker above is read by the schema-version-mismatch banner
|
|
10
10
|
-- (see /api/v1/schema-version). Bump it whenever schema.sql OR rpcs.sql
|
|
11
11
|
-- changes in a way that requires `cerefox server deploy` to be re-run —
|
|
@@ -49,6 +49,11 @@ CREATE TABLE IF NOT EXISTS cerefox_documents (
|
|
|
49
49
|
-- 'pending_review' = modified by agent, not yet reviewed.
|
|
50
50
|
-- Content is searchable in both states.
|
|
51
51
|
review_status TEXT NOT NULL DEFAULT 'approved',
|
|
52
|
+
-- lifecycle_status: where this document stands relative to the graph —
|
|
53
|
+
-- 'active' | 'superseded' | 'stale' | 'archived'. Distinct from
|
|
54
|
+
-- review_status (editorial state) and deleted_at (existence). Maintained by
|
|
55
|
+
-- the relation RPCs (e.g. `supersedes` marks its target superseded).
|
|
56
|
+
lifecycle_status TEXT NOT NULL DEFAULT 'active',
|
|
52
57
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
53
58
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
54
59
|
-- Soft delete: NULL = active, timestamp = deleted (recoverable).
|
|
@@ -118,7 +123,9 @@ CREATE TABLE IF NOT EXISTS cerefox_audit_log (
|
|
|
118
123
|
|
|
119
124
|
CONSTRAINT cerefox_audit_log_operation_check CHECK (
|
|
120
125
|
operation IN ('create', 'update-content', 'update-metadata', 'delete',
|
|
121
|
-
'status-change', 'archive', 'unarchive', 'restore'
|
|
126
|
+
'status-change', 'archive', 'unarchive', 'restore',
|
|
127
|
+
-- iteration 29: graph edges are auditable writes too
|
|
128
|
+
'relation-set', 'relation-delete')
|
|
122
129
|
),
|
|
123
130
|
CONSTRAINT cerefox_audit_log_author_type_check CHECK (author_type IN ('user', 'agent'))
|
|
124
131
|
);
|
|
@@ -132,6 +139,32 @@ CREATE TABLE IF NOT EXISTS cerefox_document_projects (
|
|
|
132
139
|
PRIMARY KEY (document_id, project_id)
|
|
133
140
|
);
|
|
134
141
|
|
|
142
|
+
-- ── Document relations (iteration 29) ─────────────────────────────────────────
|
|
143
|
+
-- Typed, directed edges between documents. rel_type is free text by design so
|
|
144
|
+
-- agents can define new types without a migration; the type dictionary lives in
|
|
145
|
+
-- the RPCs and gives KNOWN types behaviour (symmetry, lifecycle side effects).
|
|
146
|
+
-- Design: docs/research/document-relations-and-semantic-graph.md §2.2.
|
|
147
|
+
|
|
148
|
+
CREATE TABLE IF NOT EXISTS cerefox_document_relations (
|
|
149
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
150
|
+
source_id UUID NOT NULL REFERENCES cerefox_documents(id) ON DELETE CASCADE,
|
|
151
|
+
target_id UUID NOT NULL REFERENCES cerefox_documents(id) ON DELETE CASCADE,
|
|
152
|
+
rel_type TEXT NOT NULL,
|
|
153
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
154
|
+
author TEXT NOT NULL DEFAULT 'unknown',
|
|
155
|
+
author_type TEXT NOT NULL DEFAULT 'agent',
|
|
156
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
157
|
+
UNIQUE (source_id, target_id, rel_type),
|
|
158
|
+
CONSTRAINT cerefox_relations_no_self_edge CHECK (source_id <> target_id)
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
CREATE INDEX IF NOT EXISTS idx_cerefox_relations_source ON cerefox_document_relations(source_id);
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_cerefox_relations_target ON cerefox_document_relations(target_id);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_cerefox_relations_type ON cerefox_document_relations(rel_type);
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_cerefox_docs_lifecycle
|
|
165
|
+
ON cerefox_documents(lifecycle_status)
|
|
166
|
+
WHERE lifecycle_status <> 'active';
|
|
167
|
+
|
|
135
168
|
-- ── Chunks ────────────────────────────────────────────────────────────────────
|
|
136
169
|
-- One row per chunk of a document. Embeddings and FTS live here.
|
|
137
170
|
--
|
|
@@ -335,6 +368,11 @@ ON CONFLICT (key) DO NOTHING;
|
|
|
335
368
|
INSERT INTO cerefox_config (key, value)
|
|
336
369
|
VALUES ('requestor_identity_format', '^[a-zA-Z0-9_:.\- ]+$')
|
|
337
370
|
ON CONFLICT (key) DO NOTHING;
|
|
371
|
+
-- Document relations ship dormant: the tools stay hidden from agents until a
|
|
372
|
+
-- deployment opts in (iteration 29).
|
|
373
|
+
INSERT INTO cerefox_config (key, value)
|
|
374
|
+
VALUES ('relations_enabled', 'false')
|
|
375
|
+
ON CONFLICT (key) DO NOTHING;
|
|
338
376
|
|
|
339
377
|
|
|
340
378
|
-- ── Usage log ────────────────────────────────────────────────────────────────
|
|
@@ -34,12 +34,15 @@ import {
|
|
|
34
34
|
unauthorizedChallenge,
|
|
35
35
|
} from "./oauth.ts";
|
|
36
36
|
import type { AuthResult, McpAuthenticator } from "../../../_shared/mcp-auth/index.ts";
|
|
37
|
+
import type { MCPSupabaseClient } from "../../../_shared/mcp-tools/types.ts";
|
|
37
38
|
import { checkAccessToken, parseAccessTokens } from "../../../_shared/ef-auth/index.ts";
|
|
38
39
|
import {
|
|
39
40
|
ALL_TOOLS,
|
|
40
41
|
McpInvalidParams,
|
|
41
42
|
TOOLS_BY_NAME,
|
|
42
43
|
type ToolContext,
|
|
44
|
+
assertToolEnabled,
|
|
45
|
+
listEnabledTools,
|
|
43
46
|
} from "../../../_shared/mcp-tools/index.ts";
|
|
44
47
|
import {
|
|
45
48
|
type AggregatedVersions,
|
|
@@ -57,11 +60,16 @@ const SERVER_VERSION = "0.4.0";
|
|
|
57
60
|
|
|
58
61
|
// ── Tool list (derived from _shared/mcp-tools/) ─────────────────────────────
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// Built per request rather than at module load: the tool surface depends on
|
|
64
|
+
// deployment config (optional features are hidden until enabled), and an
|
|
65
|
+
// isolate can outlive a config change.
|
|
66
|
+
async function buildToolList(supabase: MCPSupabaseClient) {
|
|
67
|
+
return (await listEnabledTools(supabase)).map((t) => ({
|
|
68
|
+
name: t.name,
|
|
69
|
+
description: t.description,
|
|
70
|
+
inputSchema: t.inputSchema,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
65
73
|
|
|
66
74
|
// ── Method handlers ──────────────────────────────────────────────────────────
|
|
67
75
|
|
|
@@ -77,8 +85,8 @@ function handleInitialize(id: unknown): Response {
|
|
|
77
85
|
});
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
function handleToolsList(id: unknown): Response {
|
|
81
|
-
return jsonResponse({ jsonrpc: "2.0", id, result: { tools:
|
|
88
|
+
async function handleToolsList(id: unknown, supabase: MCPSupabaseClient): Promise<Response> {
|
|
89
|
+
return jsonResponse({ jsonrpc: "2.0", id, result: { tools: await buildToolList(supabase) } });
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
async function handleToolsCall(
|
|
@@ -105,6 +113,14 @@ async function handleToolsCall(
|
|
|
105
113
|
// deno-lint-ignore no-explicit-any
|
|
106
114
|
const supabase: any = makeSupabaseClient();
|
|
107
115
|
|
|
116
|
+
// Optional features: a session that listed tools before the flag changed can
|
|
117
|
+
// still name a gated tool.
|
|
118
|
+
try {
|
|
119
|
+
await assertToolEnabled(supabase, toolName);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return errorResponse(id, -32602, err instanceof Error ? err.message : String(err));
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
try {
|
|
109
125
|
const { data: requireConfig } = await supabase.rpc("cerefox_get_config", {
|
|
110
126
|
p_key: "require_requestor_identity",
|
|
@@ -336,7 +352,8 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
|
|
336
352
|
case "ping":
|
|
337
353
|
return jsonResponse({ jsonrpc: "2.0", id, result: {} });
|
|
338
354
|
case "tools/list":
|
|
339
|
-
|
|
355
|
+
// deno-lint-ignore no-explicit-any
|
|
356
|
+
return await handleToolsList(id, makeSupabaseClient() as any);
|
|
340
357
|
case "tools/call":
|
|
341
358
|
return await handleToolsCall(
|
|
342
359
|
id,
|
package/docs/guides/cli.md
CHANGED
|
@@ -544,42 +544,6 @@ Detects fresh vs. existing databases: a fresh DB gets schema + RPCs + migration
|
|
|
544
544
|
| `--all` | flag | off | Reindex every chunk regardless of embedder. |
|
|
545
545
|
| `--dry-run` | flag | off | Count what would be re-embedded; do nothing. |
|
|
546
546
|
|
|
547
|
-
> **Reindex refreshes embeddings only.** It rewrites `embedding_primary` on
|
|
548
|
-
> existing chunk rows and never re-chunks, so it cannot change a document's
|
|
549
|
-
> stored `content_format`. Use `server migrate-format` for that (#164).
|
|
550
|
-
|
|
551
|
-
---
|
|
552
|
-
|
|
553
|
-
### `cerefox server migrate-format`
|
|
554
|
-
|
|
555
|
-
**Purpose**: convert documents still stored under the legacy chunk-reconstruction
|
|
556
|
-
format to the current one, by re-ingesting them through the normal pipeline
|
|
557
|
-
(re-chunk → re-embed → stamp the current format). `cerefox doctor` reports how
|
|
558
|
-
many documents are affected.
|
|
559
|
-
|
|
560
|
-
**Synopsis**: `cerefox server migrate-format [OPTIONS]`
|
|
561
|
-
|
|
562
|
-
**Options**:
|
|
563
|
-
|
|
564
|
-
| Flag | Type | Default | Description |
|
|
565
|
-
|---|---|---|---|
|
|
566
|
-
| `--dry-run` | flag | off | Report how many documents would convert; write nothing. |
|
|
567
|
-
| `-l, --limit <n>` | int | all | Convert at most N documents (re-run to continue). |
|
|
568
|
-
| `--document-id <uuid>` | string | — | Convert a single document. |
|
|
569
|
-
| `--author <name>` | string | `CEREFOX_AUTHOR_NAME` | Recorded in the audit log per conversion. |
|
|
570
|
-
|
|
571
|
-
**This costs embedding spend** — every converted document is re-embedded — so it
|
|
572
|
-
is opt-in and never runs automatically. Legacy-format documents are *not*
|
|
573
|
-
broken: they reconstruct exactly as they always have, and convert on their own
|
|
574
|
-
the next time they are edited. Each conversion runs under optimistic
|
|
575
|
-
concurrency, so a document edited mid-run is skipped rather than overwritten,
|
|
576
|
-
and documents whose content is byte-identical to another document cannot be
|
|
577
|
-
converted (the ingestion pipeline's dedup check rejects them) — those are
|
|
578
|
-
reported rather than failing the run.
|
|
579
|
-
|
|
580
|
-
**Take a backup first** (`cerefox backup create`). The previous chunks are
|
|
581
|
-
archived as a document version, but version retention defaults to 48 hours.
|
|
582
|
-
|
|
583
547
|
---
|
|
584
548
|
|
|
585
549
|
### `cerefox token generate` / `cerefox token rotate` / `cerefox token list`
|