@cerefox/memory 1.7.1 → 1.9.0
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_GUIDE.md +13 -0
- package/AGENT_QUICK_REFERENCE.md +1 -1
- package/README.md +4 -0
- package/dist/bin/cerefox.js +325 -99
- package/dist/frontend/assets/index-DWT7wZMR.js +121 -0
- package/dist/frontend/assets/index-DWT7wZMR.js.map +1 -0
- package/dist/frontend/index.html +1 -1
- package/dist/server-assets/_shared/ef-meta/index.ts +3 -3
- package/dist/server-assets/_shared/mcp-tools/_projects.ts +76 -51
- package/dist/server-assets/_shared/mcp-tools/_utils.ts +45 -0
- package/dist/server-assets/_shared/mcp-tools/audit-log.ts +3 -5
- package/dist/server-assets/_shared/mcp-tools/audit-ops.ts +46 -0
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +1 -1
- package/dist/server-assets/_shared/mcp-tools/ingest.ts +6 -6
- package/dist/server-assets/db/migrations/0025_drop_orphaned_overloads.sql +2 -1
- package/dist/server-assets/db/migrations/0027_drop_archived_search_artifacts.sql +172 -0
- package/dist/server-assets/db/migrations/0028_audit_store_level_writes.sql +349 -0
- package/dist/server-assets/db/rpcs.sql +270 -64
- package/dist/server-assets/db/schema.sql +18 -4
- package/dist/server-assets/supabase/functions/cerefox-ingest/index.ts +14 -97
- package/docs/guides/cli.md +13 -2
- package/docs/guides/connect-agents.md +4 -19
- package/docs/guides/ops-scripts.md +4 -4
- package/docs/guides/upgrading.md +14 -1
- package/package.json +2 -2
- package/dist/frontend/assets/index-D8E0mTnp.js +0 -121
- package/dist/frontend/assets/index-D8E0mTnp.js.map +0 -1
|
@@ -598,63 +598,18 @@ AS $$
|
|
|
598
598
|
GROUP BY d.id, d.title, d.source, d.metadata;
|
|
599
599
|
$$;
|
|
600
600
|
|
|
601
|
-
-- ──
|
|
602
|
-
--
|
|
603
|
-
--
|
|
604
|
-
--
|
|
605
|
-
--
|
|
601
|
+
-- ── retired V1 RPC (dropped in 0.14.0) ──────────────────────────────────────
|
|
602
|
+
-- cerefox_save_note was a Python-era V1 tool with zero callers since the TS
|
|
603
|
+
-- rewrite — nothing in the CLI, MCP tools, Edge Functions, web app, OR the
|
|
604
|
+
-- SQL layer ever called it. The DROP stays here so re-applying rpcs.sql also
|
|
605
|
+
-- cleans long-lived databases (migration 0028 carries the same).
|
|
606
606
|
--
|
|
607
|
-
--
|
|
608
|
-
--
|
|
609
|
-
--
|
|
610
|
-
--
|
|
611
|
-
--
|
|
612
|
-
|
|
613
|
-
--
|
|
614
|
-
-- Returns: the created document row (id, title, created_at)
|
|
615
|
-
|
|
616
|
-
CREATE OR REPLACE FUNCTION cerefox_save_note(
|
|
617
|
-
p_title TEXT,
|
|
618
|
-
p_content TEXT,
|
|
619
|
-
p_source TEXT DEFAULT 'agent',
|
|
620
|
-
p_project_id UUID DEFAULT NULL,
|
|
621
|
-
p_metadata JSONB DEFAULT '{}'::JSONB
|
|
622
|
-
)
|
|
623
|
-
RETURNS TABLE (
|
|
624
|
-
id UUID,
|
|
625
|
-
title TEXT,
|
|
626
|
-
created_at TIMESTAMPTZ
|
|
627
|
-
)
|
|
628
|
-
LANGUAGE plpgsql
|
|
629
|
-
SECURITY DEFINER
|
|
630
|
-
SET search_path = public, pg_catalog
|
|
631
|
-
AS $$
|
|
632
|
-
DECLARE
|
|
633
|
-
v_hash TEXT;
|
|
634
|
-
v_doc_id UUID;
|
|
635
|
-
v_created_at TIMESTAMPTZ;
|
|
636
|
-
BEGIN
|
|
637
|
-
-- Compute content hash to support deduplication on the caller side.
|
|
638
|
-
v_hash := encode(sha256(p_content::BYTEA), 'hex');
|
|
639
|
-
|
|
640
|
-
INSERT INTO cerefox_documents (
|
|
641
|
-
title, source, content_hash, metadata, chunk_count, total_chars
|
|
642
|
-
) VALUES (
|
|
643
|
-
p_title, p_source, v_hash, p_metadata, 0, length(p_content)
|
|
644
|
-
)
|
|
645
|
-
RETURNING cerefox_documents.id, cerefox_documents.created_at
|
|
646
|
-
INTO v_doc_id, v_created_at;
|
|
647
|
-
|
|
648
|
-
-- Assign to project if provided (many-to-many junction).
|
|
649
|
-
IF p_project_id IS NOT NULL THEN
|
|
650
|
-
INSERT INTO cerefox_document_projects (document_id, project_id)
|
|
651
|
-
VALUES (v_doc_id, p_project_id)
|
|
652
|
-
ON CONFLICT DO NOTHING;
|
|
653
|
-
END IF;
|
|
654
|
-
|
|
655
|
-
RETURN QUERY SELECT v_doc_id, p_title, v_created_at;
|
|
656
|
-
END;
|
|
657
|
-
$$;
|
|
607
|
+
-- cerefox_context_expand (below) looked like the same class but is NOT dead:
|
|
608
|
+
-- cerefox_search_docs calls it for small-to-big retrieval. TS-side grep alone
|
|
609
|
+
-- cannot establish an RPC is unused — SQL functions have SQL callers. It was
|
|
610
|
+
-- briefly slated for removal in this release; the sandbox validation caught
|
|
611
|
+
-- the breakage (42883 from search_docs) before anything shipped.
|
|
612
|
+
DROP FUNCTION IF EXISTS cerefox_save_note(TEXT, TEXT, TEXT, UUID, JSONB);
|
|
658
613
|
|
|
659
614
|
-- ── cerefox_context_expand ────────────────────────────────────────────────────
|
|
660
615
|
-- Small-to-big retrieval: given a set of chunk IDs from a search result,
|
|
@@ -1009,9 +964,19 @@ BEGIN
|
|
|
1009
964
|
)
|
|
1010
965
|
RETURNING id INTO v_version_id;
|
|
1011
966
|
|
|
1012
|
-
-- Archive all current chunks by pointing them at the new version
|
|
967
|
+
-- Archive all current chunks by pointing them at the new version, and
|
|
968
|
+
-- NULL their search artifacts in the same write (0.13.0, #216 — full
|
|
969
|
+
-- rationale in migration 0027). The content — the actual safety copy —
|
|
970
|
+
-- is untouched. embedder_upgrade is nulled with its vector;
|
|
971
|
+
-- embedder_primary is deliberately KEPT (it is NOT NULL, and the label
|
|
972
|
+
-- is harmless provenance for a vector that no longer exists — nothing
|
|
973
|
+
-- reads embedder columns without a version_id IS NULL filter).
|
|
1013
974
|
UPDATE cerefox_chunks c
|
|
1014
|
-
SET version_id = v_version_id
|
|
975
|
+
SET version_id = v_version_id,
|
|
976
|
+
embedding_primary = NULL,
|
|
977
|
+
embedding_upgrade = NULL,
|
|
978
|
+
embedder_upgrade = NULL,
|
|
979
|
+
fts = NULL
|
|
1015
980
|
WHERE c.document_id = p_document_id
|
|
1016
981
|
AND c.version_id IS NULL;
|
|
1017
982
|
|
|
@@ -1248,7 +1213,7 @@ $$;
|
|
|
1248
1213
|
DROP FUNCTION IF EXISTS cerefox_restore_document(UUID, TEXT, TEXT, TEXT);
|
|
1249
1214
|
DROP FUNCTION IF EXISTS cerefox_restore_document(UUID, TEXT, TEXT);
|
|
1250
1215
|
-- 0.12.1: the pre-author 1-arg overload survived every CREATE OR REPLACE
|
|
1251
|
-
-- since the signature grew (same orphan class as purge; found live on
|
|
1216
|
+
-- since the signature grew (same orphan class as purge; found live on a long-lived database).
|
|
1252
1217
|
DROP FUNCTION IF EXISTS cerefox_restore_document(UUID);
|
|
1253
1218
|
CREATE FUNCTION cerefox_restore_document(
|
|
1254
1219
|
p_document_id UUID,
|
|
@@ -1829,9 +1794,9 @@ $$;
|
|
|
1829
1794
|
-- Reads chunk title and content directly from the DB -- caller only needs to
|
|
1830
1795
|
-- supply the new document title.
|
|
1831
1796
|
--
|
|
1832
|
-
-- Only affects current chunks (version_id IS NULL). Archived chunks
|
|
1833
|
-
--
|
|
1834
|
-
-- re-
|
|
1797
|
+
-- Only affects current chunks (version_id IS NULL). Archived chunks carry NO
|
|
1798
|
+
-- tsvector at all since 0.13.0 (#216) — fts is nulled at archive time, and a
|
|
1799
|
+
-- restore is a re-ingest that recomputes everything.
|
|
1835
1800
|
|
|
1836
1801
|
DROP FUNCTION IF EXISTS cerefox_update_chunk_fts(UUID, TEXT);
|
|
1837
1802
|
CREATE FUNCTION cerefox_update_chunk_fts(
|
|
@@ -2010,6 +1975,211 @@ AS $$
|
|
|
2010
1975
|
ORDER BY p.name;
|
|
2011
1976
|
$$;
|
|
2012
1977
|
|
|
1978
|
+
-- ── Project write RPCs (0.14.0, #147/#219) ──────────────────────────────────
|
|
1979
|
+
-- Project writes carried no business logic until the audit trail arrived —
|
|
1980
|
+
-- then every caller (CLI, web, shared helpers, ingestion pipeline, the ingest
|
|
1981
|
+
-- EF) grew its own follow-up cerefox_create_audit_entry call: non-atomic,
|
|
1982
|
+
-- warn-on-failure, and empirically forgettable (review round 1 caught two
|
|
1983
|
+
-- unwired paths in the release that introduced the convention). Single
|
|
1984
|
+
-- Implementation Principle applies the moment a write has a side effect: the
|
|
1985
|
+
-- write AND its audit entry live here, in one transaction. Callers are thin.
|
|
1986
|
+
|
|
1987
|
+
-- Return shape changed in review round 4 (full row: the web routes were
|
|
1988
|
+
-- re-reading the row the RPC had just written). A changed RETURNS TABLE
|
|
1989
|
+
-- cannot go through CREATE OR REPLACE — drop first (house pattern).
|
|
1990
|
+
DROP FUNCTION IF EXISTS cerefox_create_project(TEXT, TEXT, TEXT, TEXT, TEXT);
|
|
1991
|
+
|
|
1992
|
+
CREATE FUNCTION cerefox_create_project(
|
|
1993
|
+
p_name TEXT,
|
|
1994
|
+
p_description TEXT DEFAULT '',
|
|
1995
|
+
p_author TEXT DEFAULT 'unknown',
|
|
1996
|
+
p_author_type TEXT DEFAULT 'user',
|
|
1997
|
+
-- 'error' → explicit creation (CLI/web): duplicate name raises.
|
|
1998
|
+
-- 'return' → get-or-create (implicit creation during document
|
|
1999
|
+
-- assignment): an existing project is returned untouched and
|
|
2000
|
+
-- NOT audited — only an actual create writes an entry.
|
|
2001
|
+
p_if_exists TEXT DEFAULT 'error'
|
|
2002
|
+
)
|
|
2003
|
+
RETURNS TABLE (
|
|
2004
|
+
project_id UUID,
|
|
2005
|
+
project_name TEXT,
|
|
2006
|
+
project_description TEXT,
|
|
2007
|
+
created BOOLEAN,
|
|
2008
|
+
created_at TIMESTAMPTZ,
|
|
2009
|
+
updated_at TIMESTAMPTZ
|
|
2010
|
+
)
|
|
2011
|
+
LANGUAGE plpgsql
|
|
2012
|
+
SECURITY DEFINER
|
|
2013
|
+
SET search_path = public, pg_catalog
|
|
2014
|
+
AS $$
|
|
2015
|
+
DECLARE
|
|
2016
|
+
v_row cerefox_projects%ROWTYPE;
|
|
2017
|
+
BEGIN
|
|
2018
|
+
IF NULLIF(BTRIM(p_name), '') IS NULL THEN
|
|
2019
|
+
RAISE EXCEPTION 'Project name is required' USING ERRCODE = '22023';
|
|
2020
|
+
END IF;
|
|
2021
|
+
IF p_if_exists NOT IN ('error', 'return') THEN
|
|
2022
|
+
RAISE EXCEPTION 'p_if_exists must be ''error'' or ''return''' USING ERRCODE = '22023';
|
|
2023
|
+
END IF;
|
|
2024
|
+
|
|
2025
|
+
IF p_if_exists = 'return' THEN
|
|
2026
|
+
-- Case-insensitive, matching the name resolution the assignment
|
|
2027
|
+
-- paths have always used. (A case-colliding pair created directly is
|
|
2028
|
+
-- pre-existing behavior: the unique constraint is exact-match; this
|
|
2029
|
+
-- resolver returns the first match.)
|
|
2030
|
+
SELECT p.* INTO v_row
|
|
2031
|
+
FROM cerefox_projects p WHERE lower(p.name) = lower(BTRIM(p_name)) LIMIT 1;
|
|
2032
|
+
IF v_row.id IS NOT NULL THEN
|
|
2033
|
+
RETURN QUERY SELECT v_row.id, v_row.name, v_row.description, FALSE,
|
|
2034
|
+
v_row.created_at, v_row.updated_at;
|
|
2035
|
+
RETURN;
|
|
2036
|
+
END IF;
|
|
2037
|
+
END IF;
|
|
2038
|
+
|
|
2039
|
+
BEGIN
|
|
2040
|
+
INSERT INTO cerefox_projects (name, description)
|
|
2041
|
+
VALUES (BTRIM(p_name), COALESCE(p_description, ''))
|
|
2042
|
+
RETURNING * INTO v_row;
|
|
2043
|
+
EXCEPTION WHEN unique_violation THEN
|
|
2044
|
+
-- TOCTOU (round 4): a concurrent create won the race between the
|
|
2045
|
+
-- resolve above and this insert. In 'return' mode that is exactly
|
|
2046
|
+
-- the get-or-create contract — hand back the winner, no audit entry
|
|
2047
|
+
-- (this call created nothing). In 'error' mode a duplicate is the
|
|
2048
|
+
-- caller's error, exactly as if there had been no race.
|
|
2049
|
+
IF p_if_exists = 'return' THEN
|
|
2050
|
+
SELECT p.* INTO v_row
|
|
2051
|
+
FROM cerefox_projects p WHERE lower(p.name) = lower(BTRIM(p_name)) LIMIT 1;
|
|
2052
|
+
IF v_row.id IS NOT NULL THEN
|
|
2053
|
+
RETURN QUERY SELECT v_row.id, v_row.name, v_row.description, FALSE,
|
|
2054
|
+
v_row.created_at, v_row.updated_at;
|
|
2055
|
+
RETURN;
|
|
2056
|
+
END IF;
|
|
2057
|
+
END IF;
|
|
2058
|
+
RAISE;
|
|
2059
|
+
END;
|
|
2060
|
+
|
|
2061
|
+
PERFORM cerefox_create_audit_entry(
|
|
2062
|
+
p_operation := 'project-create',
|
|
2063
|
+
p_author := p_author,
|
|
2064
|
+
p_author_type := p_author_type,
|
|
2065
|
+
p_description := 'Project ''' || v_row.name || ''' created'
|
|
2066
|
+
|| CASE WHEN p_if_exists = 'return'
|
|
2067
|
+
THEN ' implicitly (document assignment)' ELSE '' END
|
|
2068
|
+
);
|
|
2069
|
+
RETURN QUERY SELECT v_row.id, v_row.name, v_row.description, TRUE,
|
|
2070
|
+
v_row.created_at, v_row.updated_at;
|
|
2071
|
+
END;
|
|
2072
|
+
$$;
|
|
2073
|
+
|
|
2074
|
+
DROP FUNCTION IF EXISTS cerefox_update_project(UUID, TEXT, TEXT, TEXT, TEXT);
|
|
2075
|
+
|
|
2076
|
+
CREATE FUNCTION cerefox_update_project(
|
|
2077
|
+
p_project_id UUID,
|
|
2078
|
+
-- NULL = keep the current value (the #191/#204 "NULL means not provided"
|
|
2079
|
+
-- convention). An explicit empty name is rejected.
|
|
2080
|
+
p_name TEXT DEFAULT NULL,
|
|
2081
|
+
p_description TEXT DEFAULT NULL,
|
|
2082
|
+
p_author TEXT DEFAULT 'unknown',
|
|
2083
|
+
p_author_type TEXT DEFAULT 'user'
|
|
2084
|
+
)
|
|
2085
|
+
RETURNS TABLE (
|
|
2086
|
+
project_id UUID,
|
|
2087
|
+
project_name TEXT,
|
|
2088
|
+
project_description TEXT,
|
|
2089
|
+
created_at TIMESTAMPTZ,
|
|
2090
|
+
updated_at TIMESTAMPTZ
|
|
2091
|
+
)
|
|
2092
|
+
LANGUAGE plpgsql
|
|
2093
|
+
SECURITY DEFINER
|
|
2094
|
+
SET search_path = public, pg_catalog
|
|
2095
|
+
AS $$
|
|
2096
|
+
DECLARE
|
|
2097
|
+
v_old cerefox_projects%ROWTYPE;
|
|
2098
|
+
v_new cerefox_projects%ROWTYPE;
|
|
2099
|
+
v_changes TEXT[] := '{}';
|
|
2100
|
+
BEGIN
|
|
2101
|
+
IF p_name IS NULL AND p_description IS NULL THEN
|
|
2102
|
+
RAISE EXCEPTION 'Nothing to update: pass p_name and/or p_description' USING ERRCODE = '22023';
|
|
2103
|
+
END IF;
|
|
2104
|
+
IF p_name IS NOT NULL AND NULLIF(BTRIM(p_name), '') IS NULL THEN
|
|
2105
|
+
RAISE EXCEPTION 'Project name cannot be empty' USING ERRCODE = '22023';
|
|
2106
|
+
END IF;
|
|
2107
|
+
|
|
2108
|
+
SELECT p.* INTO v_old FROM cerefox_projects p WHERE p.id = p_project_id FOR UPDATE;
|
|
2109
|
+
IF NOT FOUND THEN
|
|
2110
|
+
RAISE EXCEPTION 'Project not found: %', p_project_id USING ERRCODE = '22023';
|
|
2111
|
+
END IF;
|
|
2112
|
+
|
|
2113
|
+
UPDATE cerefox_projects SET
|
|
2114
|
+
name = COALESCE(BTRIM(p_name), name),
|
|
2115
|
+
description = COALESCE(p_description, description),
|
|
2116
|
+
updated_at = NOW()
|
|
2117
|
+
WHERE id = p_project_id
|
|
2118
|
+
RETURNING * INTO v_new;
|
|
2119
|
+
|
|
2120
|
+
-- The trail records what actually changed, not which arguments arrived.
|
|
2121
|
+
IF v_new.name <> v_old.name THEN
|
|
2122
|
+
v_changes := v_changes || ('renamed ''' || v_old.name || ''' → ''' || v_new.name || '''');
|
|
2123
|
+
END IF;
|
|
2124
|
+
IF COALESCE(v_new.description, '') <> COALESCE(v_old.description, '') THEN
|
|
2125
|
+
v_changes := v_changes || 'description changed';
|
|
2126
|
+
END IF;
|
|
2127
|
+
|
|
2128
|
+
PERFORM cerefox_create_audit_entry(
|
|
2129
|
+
p_operation := 'project-edit',
|
|
2130
|
+
p_author := p_author,
|
|
2131
|
+
p_author_type := p_author_type,
|
|
2132
|
+
p_description := 'Project ''' || v_new.name || ''' edited ('
|
|
2133
|
+
|| COALESCE(NULLIF(array_to_string(v_changes, '; '), ''), 'no-op') || ')'
|
|
2134
|
+
);
|
|
2135
|
+
RETURN QUERY SELECT v_new.id, v_new.name, v_new.description,
|
|
2136
|
+
v_new.created_at, v_new.updated_at;
|
|
2137
|
+
END;
|
|
2138
|
+
$$;
|
|
2139
|
+
|
|
2140
|
+
CREATE OR REPLACE FUNCTION cerefox_delete_project(
|
|
2141
|
+
p_project_id UUID,
|
|
2142
|
+
p_author TEXT DEFAULT 'unknown',
|
|
2143
|
+
p_author_type TEXT DEFAULT 'user'
|
|
2144
|
+
)
|
|
2145
|
+
RETURNS TABLE (deleted BOOLEAN, project_name TEXT)
|
|
2146
|
+
LANGUAGE plpgsql
|
|
2147
|
+
SECURITY DEFINER
|
|
2148
|
+
SET search_path = public, pg_catalog
|
|
2149
|
+
AS $$
|
|
2150
|
+
DECLARE
|
|
2151
|
+
v_name TEXT;
|
|
2152
|
+
v_links INT;
|
|
2153
|
+
BEGIN
|
|
2154
|
+
-- Lock + read name and link count in one pass: the memberships CASCADE
|
|
2155
|
+
-- with the row, so the count must be read pre-DELETE — but the zero-row
|
|
2156
|
+
-- path (repeat delete) pays for nothing (round 4).
|
|
2157
|
+
SELECT p.name, (SELECT COUNT(*) FROM cerefox_document_projects dp
|
|
2158
|
+
WHERE dp.project_id = p.id)
|
|
2159
|
+
INTO v_name, v_links
|
|
2160
|
+
FROM cerefox_projects p WHERE p.id = p_project_id FOR UPDATE;
|
|
2161
|
+
|
|
2162
|
+
IF v_name IS NULL THEN
|
|
2163
|
+
-- Zero rows: nothing happened, so nothing is audited — the trail
|
|
2164
|
+
-- must never assert an event that did not occur. Callers decide
|
|
2165
|
+
-- whether "already gone" is an error (CLI) or a 404 (web).
|
|
2166
|
+
RETURN QUERY SELECT FALSE, NULL::TEXT;
|
|
2167
|
+
RETURN;
|
|
2168
|
+
END IF;
|
|
2169
|
+
|
|
2170
|
+
DELETE FROM cerefox_projects WHERE id = p_project_id;
|
|
2171
|
+
|
|
2172
|
+
PERFORM cerefox_create_audit_entry(
|
|
2173
|
+
p_operation := 'project-delete',
|
|
2174
|
+
p_author := p_author,
|
|
2175
|
+
p_author_type := p_author_type,
|
|
2176
|
+
p_description := 'Project ''' || v_name || ''' deleted ('
|
|
2177
|
+
|| v_links || ' document link(s) removed)'
|
|
2178
|
+
);
|
|
2179
|
+
RETURN QUERY SELECT TRUE, v_name;
|
|
2180
|
+
END;
|
|
2181
|
+
$$;
|
|
2182
|
+
|
|
2013
2183
|
-- ── cerefox_metadata_search ──────────────────────────────────────────────────
|
|
2014
2184
|
-- Query documents by metadata key-value criteria without a text search term.
|
|
2015
2185
|
-- Uses JSONB containment (@>) which leverages the existing GIN index on
|
|
@@ -2432,7 +2602,23 @@ BEGIN
|
|
|
2432
2602
|
END;
|
|
2433
2603
|
$$;
|
|
2434
2604
|
|
|
2435
|
-
|
|
2605
|
+
-- The 2-arg signature grew to 4 in 0.14.0 (audit trail). CREATE OR REPLACE
|
|
2606
|
+
-- never removes the old overload, and a surviving one makes named calls
|
|
2607
|
+
-- ambiguous under PostgREST (PGRST203) — the v1.7.0 purge/restore lesson.
|
|
2608
|
+
-- Both signatures dropped (house pattern, cf. delete_document): the old one
|
|
2609
|
+
-- so it cannot linger, the current one so re-applying this file stays
|
|
2610
|
+
-- idempotent after migration 0028 has already created it.
|
|
2611
|
+
DROP FUNCTION IF EXISTS cerefox_set_config(TEXT, TEXT);
|
|
2612
|
+
DROP FUNCTION IF EXISTS cerefox_set_config(TEXT, TEXT, TEXT, TEXT);
|
|
2613
|
+
|
|
2614
|
+
CREATE FUNCTION cerefox_set_config(
|
|
2615
|
+
p_key TEXT,
|
|
2616
|
+
p_value TEXT,
|
|
2617
|
+
-- 0.14.0: config changes are governance decisions ("who turned retention
|
|
2618
|
+
-- off, and when?") and belong in the audit trail like any other write.
|
|
2619
|
+
p_author TEXT DEFAULT 'unknown',
|
|
2620
|
+
p_author_type TEXT DEFAULT 'user'
|
|
2621
|
+
)
|
|
2436
2622
|
RETURNS VOID
|
|
2437
2623
|
LANGUAGE plpgsql
|
|
2438
2624
|
SECURITY DEFINER
|
|
@@ -2457,14 +2643,30 @@ DECLARE
|
|
|
2457
2643
|
-- point. A signal in the write's response, never a refusal.
|
|
2458
2644
|
'document_size_warning_chars'
|
|
2459
2645
|
];
|
|
2646
|
+
v_old TEXT;
|
|
2460
2647
|
BEGIN
|
|
2461
2648
|
IF NOT (p_key = ANY(v_allowed)) THEN
|
|
2462
2649
|
RAISE EXCEPTION 'Unknown config key: %. Allowed keys: %', p_key, v_allowed;
|
|
2463
2650
|
END IF;
|
|
2464
2651
|
|
|
2652
|
+
SELECT value INTO v_old FROM cerefox_config WHERE key = p_key;
|
|
2653
|
+
|
|
2465
2654
|
INSERT INTO cerefox_config (key, value)
|
|
2466
2655
|
VALUES (p_key, p_value)
|
|
2467
2656
|
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
|
|
2657
|
+
|
|
2658
|
+
-- Same-transaction audit entry (document_id NULL — a store-level write).
|
|
2659
|
+
-- A no-op set (same value) is still recorded: "checked and confirmed" is
|
|
2660
|
+
-- itself a governance action, and skipping it would make the trail lie by
|
|
2661
|
+
-- omission when someone re-asserts a setting.
|
|
2662
|
+
PERFORM cerefox_create_audit_entry(
|
|
2663
|
+
p_operation := 'config-change',
|
|
2664
|
+
p_author := p_author,
|
|
2665
|
+
p_author_type := p_author_type,
|
|
2666
|
+
p_description := 'config: ' || p_key || ': '
|
|
2667
|
+
|| COALESCE('''' || v_old || '''', '(unset)')
|
|
2668
|
+
|| ' → ''' || p_value || ''''
|
|
2669
|
+
);
|
|
2468
2670
|
END;
|
|
2469
2671
|
$$;
|
|
2470
2672
|
|
|
@@ -2788,6 +2990,10 @@ SET search_path = public, pg_catalog
|
|
|
2788
2990
|
AS $$
|
|
2789
2991
|
-- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
|
|
2790
2992
|
-- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
|
|
2993
|
+
-- 0.13.0 (#216): archived chunks carry no search artifacts —
|
|
2994
|
+
-- cerefox_snapshot_version nulls embedding_primary/embedding_upgrade/fts
|
|
2995
|
+
-- at archive time; embedding_primary becomes nullable; migration 0027
|
|
2996
|
+
-- strips existing archived rows.
|
|
2791
2997
|
-- 0.12.2 (#212, #214): metadata must be a JSON object (ingest input guard
|
|
2792
2998
|
-- + set_document_metadata stored-state merge guard); cerefox_find_dead_links
|
|
2793
2999
|
-- (link-integrity phase-2 sweep); cerefox_metadata_health (doctor check).
|
|
@@ -2806,7 +3012,7 @@ AS $$
|
|
|
2806
3012
|
-- 0.11.0 supersedes 0.10.6 (v1.2.1, #191): this branch carries that fix plus
|
|
2807
3013
|
-- the partial-edit surface, and both migrations (0019, 0020) are in the
|
|
2808
3014
|
-- sequence, so a store deploying this gets everything from both lines.
|
|
2809
|
-
SELECT '0.
|
|
3015
|
+
SELECT '0.14.0'::TEXT;
|
|
2810
3016
|
$$;
|
|
2811
3017
|
|
|
2812
3018
|
-- ── cerefox_find_dead_links ──────────────────────────────────────────────────
|
|
@@ -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.14.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 —
|
|
@@ -140,7 +140,13 @@ CREATE TABLE IF NOT EXISTS cerefox_audit_log (
|
|
|
140
140
|
'insert', 'replace-section', 'delete-section',
|
|
141
141
|
-- iteration 35 (#197): renaming a heading is neither a
|
|
142
142
|
-- rewrite nor a removal, and the trail should say so.
|
|
143
|
-
'rename-section'
|
|
143
|
+
'rename-section',
|
|
144
|
+
-- 0.14.0 (iteration 38, #147): store-level writes join the
|
|
145
|
+
-- trail. These entries carry document_id NULL — like the
|
|
146
|
+
-- rows a purge cascade orphans, so every reader already
|
|
147
|
+
-- tolerates them.
|
|
148
|
+
'config-change', 'project-create', 'project-edit',
|
|
149
|
+
'project-delete')
|
|
144
150
|
),
|
|
145
151
|
CONSTRAINT cerefox_audit_log_author_type_check CHECK (author_type IN ('user', 'agent'))
|
|
146
152
|
);
|
|
@@ -212,8 +218,16 @@ CREATE TABLE IF NOT EXISTS cerefox_chunks (
|
|
|
212
218
|
-- reconstructs with its OWN format. See docs/guides/content-format.md.
|
|
213
219
|
content_format SMALLINT NOT NULL DEFAULT 1,
|
|
214
220
|
|
|
215
|
-
-- Primary embedding: always computed, cloud API (default: OpenAI
|
|
216
|
-
|
|
221
|
+
-- Primary embedding: always computed on write, cloud API (default: OpenAI
|
|
222
|
+
-- text-embedding-3-small). NULL only on ARCHIVED rows (0.13.0, #216 —
|
|
223
|
+
-- rationale in migration 0027): archiving nulls the search artifacts.
|
|
224
|
+
-- The CHECK below preserves the pre-0.13.0 guarantee that a CURRENT
|
|
225
|
+
-- chunk always has an embedding — without it, a short embedding-API
|
|
226
|
+
-- response could insert a silently search-invisible chunk where the old
|
|
227
|
+
-- NOT NULL failed loudly.
|
|
228
|
+
embedding_primary VECTOR(768)
|
|
229
|
+
CONSTRAINT cerefox_chunks_current_has_embedding
|
|
230
|
+
CHECK (version_id IS NOT NULL OR embedding_primary IS NOT NULL),
|
|
217
231
|
-- Upgrade embedding: optional, alternative model (Fireworks, Vertex, etc.)
|
|
218
232
|
embedding_upgrade VECTOR(768),
|
|
219
233
|
|
|
@@ -3,6 +3,10 @@ import { createClient } from "jsr:@supabase/supabase-js@2";
|
|
|
3
3
|
import { isVersionRequest, versionResponse } from "../../../_shared/ef-meta/index.ts";
|
|
4
4
|
import { efAuthGate } from "../../../_shared/ef-auth/index.ts";
|
|
5
5
|
import { capEmbeddingInput } from "../../../_shared/embeddings/index.ts";
|
|
6
|
+
import {
|
|
7
|
+
ensureDocumentInProject,
|
|
8
|
+
setDocumentProjectsByName,
|
|
9
|
+
} from "../../../_shared/mcp-tools/_projects.ts";
|
|
6
10
|
import {
|
|
7
11
|
chunkMarkdown,
|
|
8
12
|
embeddingInputFor,
|
|
@@ -220,97 +224,10 @@ async function sha256hex(text: string): Promise<string> {
|
|
|
220
224
|
//
|
|
221
225
|
// Used by both update branches AND the create path so resolution is consistent.
|
|
222
226
|
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
documentId: string,
|
|
228
|
-
projectName: string,
|
|
229
|
-
): Promise<string | null> {
|
|
230
|
-
// Resolve project name → id (look up; create if absent).
|
|
231
|
-
let projectId: string | null = null;
|
|
232
|
-
const { data: proj } = await supabase
|
|
233
|
-
.from("cerefox_projects")
|
|
234
|
-
.select("id")
|
|
235
|
-
.ilike("name", projectName)
|
|
236
|
-
.limit(1);
|
|
237
|
-
if (proj?.length) {
|
|
238
|
-
projectId = proj[0].id;
|
|
239
|
-
} else {
|
|
240
|
-
const { data: newProj } = await supabase
|
|
241
|
-
.from("cerefox_projects")
|
|
242
|
-
.insert({ name: projectName })
|
|
243
|
-
.select("id");
|
|
244
|
-
projectId = newProj?.[0]?.id ?? null;
|
|
245
|
-
}
|
|
246
|
-
if (!projectId) return null;
|
|
247
|
-
|
|
248
|
-
// Check membership; INSERT only if missing. PRIMARY KEY (document_id, project_id)
|
|
249
|
-
// guarantees uniqueness, so this is safe under concurrent calls (worst case:
|
|
250
|
-
// one of two concurrent inserts fails with 23505 unique_violation — we log
|
|
251
|
-
// and treat as "already a member"; outcome is identical).
|
|
252
|
-
const { data: existing } = await supabase
|
|
253
|
-
.from("cerefox_document_projects")
|
|
254
|
-
.select("document_id")
|
|
255
|
-
.eq("document_id", documentId)
|
|
256
|
-
.eq("project_id", projectId)
|
|
257
|
-
.limit(1);
|
|
258
|
-
if (existing?.length) return projectId; // Already a member — non-destructive
|
|
259
|
-
|
|
260
|
-
const { error: insertErr } = await supabase
|
|
261
|
-
.from("cerefox_document_projects")
|
|
262
|
-
.insert({ document_id: documentId, project_id: projectId });
|
|
263
|
-
if (insertErr && !String(insertErr.message ?? "").includes("duplicate key")) {
|
|
264
|
-
console.warn("ensureDocumentInProject: insert failed", insertErr);
|
|
265
|
-
}
|
|
266
|
-
return projectId;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// ── Destructive set-the-full-list helper (project_names list form) ─────────
|
|
270
|
-
//
|
|
271
|
-
// Resolves each name to a project_id (creating if absent), then REPLACES the
|
|
272
|
-
// document's project memberships with exactly that set. Used by the
|
|
273
|
-
// project_names: string[] form on cerefox_ingest (full-set semantics).
|
|
274
|
-
//
|
|
275
|
-
// Empty list = remove from all projects.
|
|
276
|
-
|
|
277
|
-
// deno-lint-ignore no-explicit-any
|
|
278
|
-
async function setDocumentProjectsByName(
|
|
279
|
-
// deno-lint-ignore no-explicit-any
|
|
280
|
-
supabase: any,
|
|
281
|
-
documentId: string,
|
|
282
|
-
projectNames: string[],
|
|
283
|
-
): Promise<string[]> {
|
|
284
|
-
const projectIds: string[] = [];
|
|
285
|
-
for (const name of projectNames) {
|
|
286
|
-
if (!name) continue;
|
|
287
|
-
const { data: proj } = await supabase
|
|
288
|
-
.from("cerefox_projects")
|
|
289
|
-
.select("id")
|
|
290
|
-
.ilike("name", name)
|
|
291
|
-
.limit(1);
|
|
292
|
-
if (proj?.length) {
|
|
293
|
-
projectIds.push(proj[0].id);
|
|
294
|
-
} else {
|
|
295
|
-
const { data: newProj } = await supabase
|
|
296
|
-
.from("cerefox_projects")
|
|
297
|
-
.insert({ name })
|
|
298
|
-
.select("id");
|
|
299
|
-
if (newProj?.[0]?.id) projectIds.push(newProj[0].id);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// DELETE-then-INSERT replace (matches Python assign_document_projects).
|
|
304
|
-
await supabase
|
|
305
|
-
.from("cerefox_document_projects")
|
|
306
|
-
.delete()
|
|
307
|
-
.eq("document_id", documentId);
|
|
308
|
-
if (projectIds.length > 0) {
|
|
309
|
-
const rows = projectIds.map((pid) => ({ document_id: documentId, project_id: pid }));
|
|
310
|
-
await supabase.from("cerefox_document_projects").insert(rows);
|
|
311
|
-
}
|
|
312
|
-
return projectIds;
|
|
313
|
-
}
|
|
227
|
+
// Project resolution + membership helpers are the SHARED implementations
|
|
228
|
+
// (_shared/mcp-tools/_projects.ts) — the EF carried textual clones until
|
|
229
|
+
// v1.9.0, which is exactly how forks drift (round-3 review). Both route
|
|
230
|
+
// project creation through cerefox_create_project (audits in-transaction).
|
|
314
231
|
|
|
315
232
|
// ── Main handler ───────────────────────────────────────────────────────────
|
|
316
233
|
|
|
@@ -535,9 +452,9 @@ Deno.serve(async (req: Request) => {
|
|
|
535
452
|
// - project_names (list) → destructive replace (full-set semantics)
|
|
536
453
|
// - project_name (singular) → non-destructive add (only if project_names absent)
|
|
537
454
|
if (project_names !== null) {
|
|
538
|
-
await setDocumentProjectsByName(supabase, existingDoc.id, project_names);
|
|
455
|
+
await setDocumentProjectsByName(supabase, existingDoc.id, project_names, { author, authorType: author_type });
|
|
539
456
|
} else if (project_name) {
|
|
540
|
-
await ensureDocumentInProject(supabase, existingDoc.id, project_name);
|
|
457
|
+
await ensureDocumentInProject(supabase, existingDoc.id, project_name, { author, authorType: author_type });
|
|
541
458
|
}
|
|
542
459
|
|
|
543
460
|
const note = update_if_exists ? undefined : "update_if_exists flag was overridden by document_id";
|
|
@@ -658,9 +575,9 @@ Deno.serve(async (req: Request) => {
|
|
|
658
575
|
// - project_names (list) → destructive replace (full-set semantics)
|
|
659
576
|
// - project_name (singular) → non-destructive add (only if project_names absent)
|
|
660
577
|
if (project_names !== null) {
|
|
661
|
-
await setDocumentProjectsByName(supabase, existingDoc.id, project_names);
|
|
578
|
+
await setDocumentProjectsByName(supabase, existingDoc.id, project_names, { author, authorType: author_type });
|
|
662
579
|
} else if (project_name) {
|
|
663
|
-
await ensureDocumentInProject(supabase, existingDoc.id, project_name);
|
|
580
|
+
await ensureDocumentInProject(supabase, existingDoc.id, project_name, { author, authorType: author_type });
|
|
664
581
|
}
|
|
665
582
|
|
|
666
583
|
return new Response(
|
|
@@ -759,9 +676,9 @@ Deno.serve(async (req: Request) => {
|
|
|
759
676
|
// - project_name (singular) → assign one via the non-destructive helper
|
|
760
677
|
let projectId: string | null = null;
|
|
761
678
|
if (project_names !== null && project_names.length > 0) {
|
|
762
|
-
await setDocumentProjectsByName(supabase, documentId, project_names);
|
|
679
|
+
await setDocumentProjectsByName(supabase, documentId, project_names, { author, authorType: author_type });
|
|
763
680
|
} else if (project_name) {
|
|
764
|
-
projectId = await ensureDocumentInProject(supabase, documentId, project_name);
|
|
681
|
+
projectId = await ensureDocumentInProject(supabase, documentId, project_name, { author, authorType: author_type });
|
|
765
682
|
}
|
|
766
683
|
|
|
767
684
|
// Fire-and-forget usage logging for ingest
|
package/docs/guides/cli.md
CHANGED
|
@@ -453,6 +453,8 @@ cerefox project delete [OPTIONS] PROJECT
|
|
|
453
453
|
| `--description TEXT` | str | _none_ | Project description (`create` / `edit`). |
|
|
454
454
|
| `--name TEXT` | str | _unchanged_ | New name (`edit`). |
|
|
455
455
|
| `-y, --yes` | flag | off | Skip confirmation (`delete`; required for non-interactive use). |
|
|
456
|
+
| `-a, --author TEXT` | str | `CEREFOX_AUTHOR_NAME` or `unknown` | Identity recorded in the audit log (v1.9.0 — project writes are audited in-transaction by the server). |
|
|
457
|
+
| `--author-type TEXT` | user\|agent | `user` | Recorded alongside the author. |
|
|
456
458
|
|
|
457
459
|
**Examples**:
|
|
458
460
|
```bash
|
|
@@ -461,6 +463,10 @@ cerefox project edit research --name research-archive
|
|
|
461
463
|
cerefox project delete research-archive --yes
|
|
462
464
|
```
|
|
463
465
|
|
|
466
|
+
Since v1.9.0 every project mutation is recorded in the audit log by the
|
|
467
|
+
server itself (`project-create` / `project-edit` / `project-delete`), with
|
|
468
|
+
the audit entry written in the same transaction as the change.
|
|
469
|
+
|
|
464
470
|
---
|
|
465
471
|
|
|
466
472
|
### `cerefox metadata keys`
|
|
@@ -519,7 +525,7 @@ cerefox audit list [OPTIONS]
|
|
|
519
525
|
|---|---|---|---|
|
|
520
526
|
| `--document-id TEXT` | UUID | _none_ | Filter to a single document. |
|
|
521
527
|
| `--author TEXT` | str | _none_ | Filter by author name (exact match). |
|
|
522
|
-
| `--operation
|
|
528
|
+
| `--operation TEXT` | choice | _none_ | Filter by operation type: `create`, `update-content`, `update-metadata`, `insert`, `replace-section`, `delete-section`, `rename-section`, `delete`, `restore`, `status-change`, `archive`, `unarchive`, `config-change`, `project-create`, `project-edit`, `project-delete`. |
|
|
523
529
|
| `--since TEXT` | ISO-8601 | _none_ | Lower bound on `created_at`. |
|
|
524
530
|
| `--until TEXT` | ISO-8601 | _none_ | Upper bound on `created_at`. |
|
|
525
531
|
| `--limit INTEGER` | int | `50` | Max rows. |
|
|
@@ -698,9 +704,14 @@ Agents see 15 tools with the flag off and 19 with it on. See
|
|
|
698
704
|
```
|
|
699
705
|
cerefox config list # all current key/value pairs
|
|
700
706
|
cerefox config get KEY
|
|
701
|
-
cerefox config set KEY VALUE
|
|
707
|
+
cerefox config set KEY VALUE [--author NAME] [--author-type user|agent]
|
|
702
708
|
```
|
|
703
709
|
|
|
710
|
+
Since v1.9.0 every `config set` is recorded in the audit log by the server
|
|
711
|
+
itself (`config-change`, with the old → new value), in the same transaction
|
|
712
|
+
as the write — pass `--author` (or set `CEREFOX_AUTHOR_NAME`) so the entry
|
|
713
|
+
is attributed; it records `unknown` otherwise, with a warning.
|
|
714
|
+
|
|
704
715
|
Used for toggling features at runtime without a redeploy — see the "Decision Log Q1 Part 2 — usage tracking opt-in" entry (stored in the Cerefox knowledge base).
|
|
705
716
|
|
|
706
717
|
---
|