@cerefox/memory 1.0.6 → 1.1.0-beta.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.
@@ -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 0.7,
102
+ p_alpha FLOAT DEFAULT NULL,
103
103
  p_use_upgrade BOOLEAN DEFAULT FALSE,
104
104
  p_project_id UUID DEFAULT NULL,
105
- p_min_score FLOAT DEFAULT 0.0,
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 0.5
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
- >= p_min_term_coverage * total_tokens
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
- ( p_alpha * COALESCE(v.vec_score, 0.0) +
265
- (1.0 - p_alpha) * COALESCE(f.fts_score, 0.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 >= p_min_score) AS passes
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 0.5
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 >= p_min_term_coverage * total_tokens)
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 0.7,
764
+ p_alpha FLOAT DEFAULT NULL,
753
765
  p_project_id UUID DEFAULT NULL,
754
- p_min_score FLOAT DEFAULT 0.0,
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
- p_min_term_coverage FLOAT DEFAULT 0.5
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,13 @@ SECURITY DEFINER
1760
2032
  SET search_path = public, pg_catalog
1761
2033
  AS $$
1762
2034
  DECLARE
1763
- v_allowed TEXT[] := ARRAY['usage_tracking_enabled', 'require_requestor_identity', 'requestor_identity_format'];
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
+ ];
1764
2042
  BEGIN
1765
2043
  IF NOT (p_key = ANY(v_allowed)) THEN
1766
2044
  RAISE EXCEPTION 'Unknown config key: %. Allowed keys: %', p_key, v_allowed;
@@ -1954,7 +2232,7 @@ SET search_path = public, pg_catalog
1954
2232
  AS $$
1955
2233
  -- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
1956
2234
  -- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
1957
- SELECT '0.9.2'::TEXT;
2235
+ SELECT '0.10.0'::TEXT;
1958
2236
  $$;
1959
2237
 
1960
2238
  -- ── 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.9.2
8
+ -- @version: 0.10.0
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
  --
@@ -117,6 +117,21 @@ This handles intermittent OpenAI API errors (500s) that would otherwise cause se
117
117
 
118
118
  ## Retrieval
119
119
 
120
+ > **Deployment-wide defaults (v1.1.0+).** `min_search_score`,
121
+ > `min_term_coverage`, and `search_alpha` can also be set **once, in the
122
+ > database**, and every access path obeys — CLI, local and remote MCP, Edge
123
+ > Functions, web — because they all resolve through the same search RPCs:
124
+ >
125
+ > ```bash
126
+ > cerefox config set min_search_score 0.6
127
+ > cerefox config list # shows every settable key
128
+ > ```
129
+ >
130
+ > Resolution order, highest first: **per-call argument** (`--min-score`, the
131
+ > `min_score` MCP parameter) → **client env var** below → **`cerefox_config`**
132
+ > → built-in default. A malformed stored value is ignored in favour of the
133
+ > built-in, so a bad setting can never break search.
134
+
120
135
  > **Which paths read these?** Client-side tunables in this section are read
121
136
  > from *your* `.env` by the **CLI**, the **local MCP server**, and `cerefox
122
137
  > web`. Since **v1.0.6** the same values are also honored on the **remote MCP /
@@ -143,7 +143,9 @@ cerefox-local start # start a stopped container
143
143
  cerefox-local stop # stop it (your data persists in the Docker volume)
144
144
  cerefox-local restart
145
145
  cerefox-local logs -f # follow the logs
146
- cerefox-local upgrade # pull the latest image + recreate (keeps data + OPENAI key)
146
+ cerefox-local upgrade # upgrade to the newest release + recreate (keeps data + OPENAI key)
147
+ cerefox-local upgrade v1.2.3 # pin an exact version (also how you downgrade)
148
+ cerefox-local upgrade --latest # follow the moving :latest tag from now on
147
149
  cerefox-local uninstall # remove the container, KEEP the data volume
148
150
  cerefox-local uninstall --purge # remove the container AND delete the data volume
149
151
  ```
@@ -10,7 +10,7 @@ to re-run.
10
10
  |---|---|
11
11
  | **Installer / npm** (end user, no repo clone) | `cerefox self-update` (or re-run the [installer](quickstart.md#1-install), or `bun/npm update -g @cerefox/memory`). Then `cerefox server deploy` **if the release notes flag a server-side change**. `cerefox doctor` verifies. |
12
12
  | **Source checkout** (`git clone`, contributor) | `git pull`, then `cerefox server deploy` (or the lower-level `bun scripts/db_*.ts` + `npx supabase functions deploy`). Rebuild the SPA if you run `cerefox web` from source. |
13
- | **Local / self-hosted (Docker, World B)** | `cerefox-local upgrade` — pulls the new image and recreates the container (data persists in the volume; OpenAI key + tuning overrides preserved). **No separate `server deploy`/`reindex`**: the CLI, web, PostgREST, and schema all ship together in one versioned image, so they can't drift. See [`setup-local.md`](setup-local.md). |
13
+ | **Local / self-hosted (Docker, World B)** | `cerefox-local upgrade` — resolves the newest release, pulls it, and recreates the container (`upgrade <tag>` pins an exact version; `upgrade --latest` follows the moving tag). Data persists in the volume; OpenAI key + tuning overrides preserved. **No separate `server deploy`/`reindex`**: the CLI, web, PostgREST, and schema all ship together in one versioned image, so they can't drift. See [`setup-local.md`](setup-local.md). |
14
14
 
15
15
  > **On an old pre-installer clone (0.1.x)?** The cleanest upgrade is to stop
16
16
  > running from the repo and install the package: follow the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cerefox/memory",
3
- "version": "1.0.6",
3
+ "version": "1.1.0-beta.1",
4
4
  "description": "Cerefox — user-owned shared memory for AI agents. CLI + stdio MCP server + web UI + ingestion for a knowledge base on your own Supabase project (or fully self-hosted with Cerefox Local).",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/fstamatelopoulos/cerefox",
@@ -43,7 +43,7 @@
43
43
  "@modelcontextprotocol/sdk": "^1.30.0",
44
44
  "@supabase/supabase-js": "^2.45.0",
45
45
  "cli-progress": "^3.12.0",
46
- "commander": "^12.1.0",
46
+ "commander": "^14.0.3",
47
47
  "hono": "^4.12.34",
48
48
  "mammoth": "^1.9.0",
49
49
  "ora": "^9.4.0",
@@ -54,7 +54,7 @@
54
54
  "zod": "^3.23.0"
55
55
  },
56
56
  "optionalDependencies": {
57
- "@huggingface/transformers": "^3.5.0",
57
+ "@huggingface/transformers": "^4.2.0",
58
58
  "onnxruntime-node": "^1.21.0"
59
59
  },
60
60
  "devDependencies": {