@cerefox/memory 1.7.0 → 1.8.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 +4 -4
- package/dist/bin/cerefox.js +213 -96
- package/dist/server-assets/_shared/ef-meta/index.ts +2 -2
- package/dist/server-assets/db/migrations/0025_drop_orphaned_overloads.sql +16 -0
- package/dist/server-assets/db/migrations/0026_metadata_guard_and_dead_links.sql +46 -0
- package/dist/server-assets/db/migrations/0027_drop_archived_search_artifacts.sql +172 -0
- package/dist/server-assets/db/rpcs.sql +185 -23
- package/dist/server-assets/db/schema.sql +20 -4
- package/docs/guides/cli.md +21 -0
- package/docs/guides/upgrading.md +9 -0
- package/package.json +2 -2
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* doesn't touch `supabase/functions/` leaves it alone).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
export const EF_VERSION = "1.
|
|
21
|
+
export const EF_VERSION = "1.8.0";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* The Cerefox RELEASE version — what `cerefox --version` reports and what npm
|
|
@@ -36,7 +36,7 @@ export const EF_VERSION = "1.7.0";
|
|
|
36
36
|
* is imported by the Deno Edge Functions, which cannot reach into the npm
|
|
37
37
|
* package.
|
|
38
38
|
*/
|
|
39
|
-
export const CEREFOX_VERSION = "1.
|
|
39
|
+
export const CEREFOX_VERSION = "1.8.0";
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* The most recent version whose EF-side SOURCE actually changed (#127).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- 0025_drop_orphaned_overloads.sql — remove pre-author-era 1-arg overloads.
|
|
2
|
+
--
|
|
3
|
+
-- cerefox_purge_document(UUID) and cerefox_restore_document(UUID) survived
|
|
4
|
+
-- every CREATE OR REPLACE since their signatures grew (OR REPLACE only
|
|
5
|
+
-- replaces the SAME signature), leaving long-lived databases with BOTH
|
|
6
|
+
-- overloads. A named 1-arg call is then ambiguous — PostgREST PGRST203
|
|
7
|
+
-- ("could not choose the best candidate") — which is how the first
|
|
8
|
+
-- acceptance run against a long-lived database failed to purge its
|
|
9
|
+
-- fixtures (v1.7.0).
|
|
10
|
+
-- Fresh databases never had the old signatures and are unaffected.
|
|
11
|
+
--
|
|
12
|
+
-- Schema version 0.12.0 → 0.12.1. The DROPs also run from rpcs.sql on every
|
|
13
|
+
-- deploy; this migration makes the version advance signal the redeploy.
|
|
14
|
+
|
|
15
|
+
DROP FUNCTION IF EXISTS cerefox_purge_document(UUID);
|
|
16
|
+
DROP FUNCTION IF EXISTS cerefox_restore_document(UUID);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- 0026_metadata_guard_and_dead_links.sql — #212 metadata type guards +
|
|
2
|
+
-- #214 phase-2 dead-link sweep.
|
|
3
|
+
--
|
|
4
|
+
-- RPC changes ship via rpcs.sql on this deploy: cerefox_ingest_document
|
|
5
|
+
-- rejects non-object p_metadata (the MCP layer always did; now every write
|
|
6
|
+
-- path agrees), cerefox_set_document_metadata refuses to MERGE onto a
|
|
7
|
+
-- non-object stored value (|| would produce an array; only replace=true
|
|
8
|
+
-- repairs), and two read-only RPCs arrive: cerefox_find_dead_links (whole-KB
|
|
9
|
+
-- [Text](uuid) sweep) and cerefox_metadata_health (rows with non-object
|
|
10
|
+
-- metadata, surfaced by doctor).
|
|
11
|
+
--
|
|
12
|
+
-- Schema version 0.12.1 → 0.12.2. This migration also REPORTS (not repairs)
|
|
13
|
+
-- any rows already in the non-object state, so the operator sees them at
|
|
14
|
+
-- upgrade time; repair is `cerefox document set-metadata <id> --replace`.
|
|
15
|
+
|
|
16
|
+
-- Table-level backstop (round-5 review): closes every current and future
|
|
17
|
+
-- direct writer at once, not only the RPC/CLI/web paths patched in code.
|
|
18
|
+
-- NOT VALID: legacy non-object rows survive (reported below) until repaired
|
|
19
|
+
-- with `document set-metadata --replace`, which the constraint then checks.
|
|
20
|
+
DO $$
|
|
21
|
+
BEGIN
|
|
22
|
+
IF NOT EXISTS (
|
|
23
|
+
SELECT 1 FROM pg_constraint
|
|
24
|
+
WHERE conname = 'cerefox_documents_metadata_object'
|
|
25
|
+
) THEN
|
|
26
|
+
ALTER TABLE cerefox_documents
|
|
27
|
+
ADD CONSTRAINT cerefox_documents_metadata_object
|
|
28
|
+
CHECK (jsonb_typeof(metadata) = 'object') NOT VALID;
|
|
29
|
+
END IF;
|
|
30
|
+
END $$;
|
|
31
|
+
|
|
32
|
+
DO $$
|
|
33
|
+
DECLARE
|
|
34
|
+
v_count INT;
|
|
35
|
+
BEGIN
|
|
36
|
+
SELECT count(*) INTO v_count
|
|
37
|
+
FROM cerefox_documents
|
|
38
|
+
WHERE metadata IS NOT NULL AND jsonb_typeof(metadata) <> 'object';
|
|
39
|
+
IF v_count > 0 THEN
|
|
40
|
+
RAISE NOTICE
|
|
41
|
+
'Migration 0026: % document(s) hold NON-OBJECT metadata (legacy #212 state). List them with cerefox doctor (or SELECT * FROM cerefox_metadata_health()); repair each with cerefox document set-metadata <id> --replace --json ''<object>''.',
|
|
42
|
+
v_count;
|
|
43
|
+
ELSE
|
|
44
|
+
RAISE NOTICE 'Migration 0026: metadata guards + dead-link sweep arrive with rpcs.sql. No non-object metadata rows found. Schema 0.12.2.';
|
|
45
|
+
END IF;
|
|
46
|
+
END $$;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
-- 0027_drop_archived_search_artifacts.sql — archived chunks carry no search
|
|
2
|
+
-- artifacts (#216). THE CANONICAL RATIONALE LIVES HERE; code comments
|
|
3
|
+
-- reference it.
|
|
4
|
+
--
|
|
5
|
+
-- Search is current-chunks-only by design (every search index is partial on
|
|
6
|
+
-- version_id IS NULL), version reconstruction and diffs read `content`, and
|
|
7
|
+
-- restoring an old version is deliberately manual re-ingest (which
|
|
8
|
+
-- re-embeds). Embeddings and fts on archived chunks were therefore never
|
|
9
|
+
-- readable by anything — pure storage cost, measured at ~30-45% of the chunk
|
|
10
|
+
-- relation on long-lived stores — and after a reindex they are stale for the
|
|
11
|
+
-- current embedder besides. There is no config and no maintenance command:
|
|
12
|
+
-- with nothing able to read the artifacts, this is an invariant, not a
|
|
13
|
+
-- policy. The archived content — the actual safety copy — is untouched.
|
|
14
|
+
--
|
|
15
|
+
-- Four parts:
|
|
16
|
+
-- 1. embedding_primary becomes nullable, WITH a replacement guard: a CHECK
|
|
17
|
+
-- that a CURRENT chunk always carries an embedding. Without it, a short
|
|
18
|
+
-- embedding-API response could insert a silently search-invisible chunk
|
|
19
|
+
-- where the old NOT NULL failed loudly (review round 6).
|
|
20
|
+
-- 2. The NEW cerefox_snapshot_version ships INSIDE this migration (repo
|
|
21
|
+
-- precedent: 0005-0008, 0011, 0025 carry function bodies). This closes
|
|
22
|
+
-- two windows where the OLD snapshot could keep archiving WITH
|
|
23
|
+
-- artifacts after 0027 was stamped: `bun scripts/db_migrate.ts` (which
|
|
24
|
+
-- never refreshes rpcs.sql), and a `server deploy` that failed between
|
|
25
|
+
-- the migration step and the RPC refresh.
|
|
26
|
+
-- 3. Back-fill: strip artifacts from existing archived rows, reporting
|
|
27
|
+
-- rows and bytes. embedder_upgrade is nulled with its vector;
|
|
28
|
+
-- embedder_primary is deliberately kept (NOT NULL, harmless provenance).
|
|
29
|
+
-- 4. Space note: Postgres frees the bytes for REUSE via autovacuum rather
|
|
30
|
+
-- than shrinking files immediately — growth stops even if the reported
|
|
31
|
+
-- database size does not drop the same day.
|
|
32
|
+
--
|
|
33
|
+
-- Schema version 0.12.2 → 0.13.0.
|
|
34
|
+
|
|
35
|
+
ALTER TABLE cerefox_chunks ALTER COLUMN embedding_primary DROP NOT NULL;
|
|
36
|
+
|
|
37
|
+
DO $$
|
|
38
|
+
BEGIN
|
|
39
|
+
IF NOT EXISTS (
|
|
40
|
+
SELECT 1 FROM pg_constraint WHERE conname = 'cerefox_chunks_current_has_embedding'
|
|
41
|
+
) THEN
|
|
42
|
+
ALTER TABLE cerefox_chunks
|
|
43
|
+
ADD CONSTRAINT cerefox_chunks_current_has_embedding
|
|
44
|
+
CHECK (version_id IS NOT NULL OR embedding_primary IS NOT NULL);
|
|
45
|
+
END IF;
|
|
46
|
+
END $$;
|
|
47
|
+
|
|
48
|
+
-- The new snapshot (identical to the rpcs.sql copy this release ships):
|
|
49
|
+
|
|
50
|
+
DROP FUNCTION IF EXISTS cerefox_snapshot_version(UUID, TEXT, INT);
|
|
51
|
+
DROP FUNCTION IF EXISTS cerefox_snapshot_version(UUID, TEXT, INT, BOOLEAN);
|
|
52
|
+
CREATE FUNCTION cerefox_snapshot_version(
|
|
53
|
+
p_document_id UUID,
|
|
54
|
+
p_source TEXT DEFAULT 'manual',
|
|
55
|
+
-- NULL (the new default) means "use the store's policy from
|
|
56
|
+
-- cerefox_config". Passing a value still overrides, for deliberate one-off
|
|
57
|
+
-- admin operations — but callers no longer supply one by accident.
|
|
58
|
+
p_retention_hours INT DEFAULT NULL,
|
|
59
|
+
p_cleanup_enabled BOOLEAN DEFAULT NULL
|
|
60
|
+
)
|
|
61
|
+
RETURNS TABLE (
|
|
62
|
+
version_id UUID,
|
|
63
|
+
version_number INT,
|
|
64
|
+
chunk_count INT,
|
|
65
|
+
total_chars INT
|
|
66
|
+
)
|
|
67
|
+
LANGUAGE plpgsql
|
|
68
|
+
SECURITY DEFINER
|
|
69
|
+
SET search_path = public, pg_catalog
|
|
70
|
+
AS $$
|
|
71
|
+
DECLARE
|
|
72
|
+
v_version_id UUID;
|
|
73
|
+
v_version_number INT;
|
|
74
|
+
v_chunk_count INT;
|
|
75
|
+
v_total_chars INT;
|
|
76
|
+
-- Resolve the retention policy from the STORE, not the caller.
|
|
77
|
+
--
|
|
78
|
+
-- These used to arrive as parameters filled from each client's own env, so
|
|
79
|
+
-- the surviving version history depended on which client wrote last: an
|
|
80
|
+
-- agent running defaults would prune versions that an operator had
|
|
81
|
+
-- configured to keep. Retention describes the data, so it belongs to the
|
|
82
|
+
-- data. Same COALESCE(param, config, default) shape the retrieval tunables
|
|
83
|
+
-- already use.
|
|
84
|
+
v_retention INT := COALESCE(p_retention_hours,
|
|
85
|
+
cerefox_config_int('version_retention_hours', 120));
|
|
86
|
+
v_cleanup BOOLEAN := COALESCE(p_cleanup_enabled,
|
|
87
|
+
cerefox_config_bool('version_cleanup_enabled', TRUE));
|
|
88
|
+
BEGIN
|
|
89
|
+
-- Count current chunks to record in the version metadata
|
|
90
|
+
SELECT COUNT(*), COALESCE(SUM(char_count), 0)
|
|
91
|
+
INTO v_chunk_count, v_total_chars
|
|
92
|
+
FROM cerefox_chunks c
|
|
93
|
+
WHERE c.document_id = p_document_id
|
|
94
|
+
AND c.version_id IS NULL;
|
|
95
|
+
|
|
96
|
+
-- Compute the next version number (sequential per document)
|
|
97
|
+
SELECT COALESCE(MAX(dv.version_number), 0) + 1
|
|
98
|
+
INTO v_version_number
|
|
99
|
+
FROM cerefox_document_versions dv
|
|
100
|
+
WHERE dv.document_id = p_document_id;
|
|
101
|
+
|
|
102
|
+
-- Create the version row
|
|
103
|
+
INSERT INTO cerefox_document_versions (
|
|
104
|
+
document_id, version_number, source, chunk_count, total_chars
|
|
105
|
+
) VALUES (
|
|
106
|
+
p_document_id, v_version_number, p_source, v_chunk_count, v_total_chars
|
|
107
|
+
)
|
|
108
|
+
RETURNING id INTO v_version_id;
|
|
109
|
+
|
|
110
|
+
-- Archive all current chunks by pointing them at the new version, and
|
|
111
|
+
-- NULL their search artifacts in the same write (0.13.0, #216 — full
|
|
112
|
+
-- rationale in migration 0027). The content — the actual safety copy —
|
|
113
|
+
-- is untouched. embedder_upgrade is nulled with its vector;
|
|
114
|
+
-- embedder_primary is deliberately KEPT (it is NOT NULL, and the label
|
|
115
|
+
-- is harmless provenance for a vector that no longer exists — nothing
|
|
116
|
+
-- reads embedder columns without a version_id IS NULL filter).
|
|
117
|
+
UPDATE cerefox_chunks c
|
|
118
|
+
SET version_id = v_version_id,
|
|
119
|
+
embedding_primary = NULL,
|
|
120
|
+
embedding_upgrade = NULL,
|
|
121
|
+
embedder_upgrade = NULL,
|
|
122
|
+
fts = NULL
|
|
123
|
+
WHERE c.document_id = p_document_id
|
|
124
|
+
AND c.version_id IS NULL;
|
|
125
|
+
|
|
126
|
+
-- Lazy retention: delete versions outside the retention window,
|
|
127
|
+
-- but always keep the most recently created version (the one we just made).
|
|
128
|
+
-- Skip archived versions (archived=true) -- they are protected from cleanup.
|
|
129
|
+
-- Skip cleanup entirely if p_cleanup_enabled is false (immutable mode).
|
|
130
|
+
IF v_cleanup THEN
|
|
131
|
+
DELETE FROM cerefox_document_versions dv
|
|
132
|
+
WHERE dv.document_id = p_document_id
|
|
133
|
+
AND dv.archived IS NOT TRUE
|
|
134
|
+
AND dv.created_at < NOW() - (v_retention || ' hours')::INTERVAL
|
|
135
|
+
AND dv.id != (
|
|
136
|
+
SELECT id FROM cerefox_document_versions
|
|
137
|
+
WHERE document_id = p_document_id
|
|
138
|
+
ORDER BY created_at DESC
|
|
139
|
+
LIMIT 1
|
|
140
|
+
);
|
|
141
|
+
END IF;
|
|
142
|
+
|
|
143
|
+
RETURN QUERY SELECT v_version_id, v_version_number, v_chunk_count, v_total_chars;
|
|
144
|
+
END;
|
|
145
|
+
$$;
|
|
146
|
+
|
|
147
|
+
DO $$
|
|
148
|
+
DECLARE
|
|
149
|
+
v_rows INT;
|
|
150
|
+
v_bytes BIGINT;
|
|
151
|
+
BEGIN
|
|
152
|
+
SELECT count(*),
|
|
153
|
+
COALESCE(SUM(COALESCE(pg_column_size(embedding_primary), 0))
|
|
154
|
+
+ SUM(COALESCE(pg_column_size(embedding_upgrade), 0))
|
|
155
|
+
+ SUM(COALESCE(pg_column_size(fts), 0)), 0)
|
|
156
|
+
INTO v_rows, v_bytes
|
|
157
|
+
FROM cerefox_chunks
|
|
158
|
+
WHERE version_id IS NOT NULL
|
|
159
|
+
AND (embedding_primary IS NOT NULL OR embedding_upgrade IS NOT NULL OR fts IS NOT NULL);
|
|
160
|
+
|
|
161
|
+
UPDATE cerefox_chunks
|
|
162
|
+
SET embedding_primary = NULL,
|
|
163
|
+
embedding_upgrade = NULL,
|
|
164
|
+
embedder_upgrade = NULL,
|
|
165
|
+
fts = NULL
|
|
166
|
+
WHERE version_id IS NOT NULL
|
|
167
|
+
AND (embedding_primary IS NOT NULL OR embedding_upgrade IS NOT NULL OR fts IS NOT NULL);
|
|
168
|
+
|
|
169
|
+
RAISE NOTICE
|
|
170
|
+
'Migration 0027: stripped search artifacts from % archived chunk row(s), freeing ~% for reuse. Archived content is untouched; current chunks keep their embeddings.',
|
|
171
|
+
v_rows, pg_size_pretty(v_bytes);
|
|
172
|
+
END $$;
|
|
@@ -1009,9 +1009,19 @@ BEGIN
|
|
|
1009
1009
|
)
|
|
1010
1010
|
RETURNING id INTO v_version_id;
|
|
1011
1011
|
|
|
1012
|
-
-- Archive all current chunks by pointing them at the new version
|
|
1012
|
+
-- Archive all current chunks by pointing them at the new version, and
|
|
1013
|
+
-- NULL their search artifacts in the same write (0.13.0, #216 — full
|
|
1014
|
+
-- rationale in migration 0027). The content — the actual safety copy —
|
|
1015
|
+
-- is untouched. embedder_upgrade is nulled with its vector;
|
|
1016
|
+
-- embedder_primary is deliberately KEPT (it is NOT NULL, and the label
|
|
1017
|
+
-- is harmless provenance for a vector that no longer exists — nothing
|
|
1018
|
+
-- reads embedder columns without a version_id IS NULL filter).
|
|
1013
1019
|
UPDATE cerefox_chunks c
|
|
1014
|
-
SET version_id = v_version_id
|
|
1020
|
+
SET version_id = v_version_id,
|
|
1021
|
+
embedding_primary = NULL,
|
|
1022
|
+
embedding_upgrade = NULL,
|
|
1023
|
+
embedder_upgrade = NULL,
|
|
1024
|
+
fts = NULL
|
|
1015
1025
|
WHERE c.document_id = p_document_id
|
|
1016
1026
|
AND c.version_id IS NULL;
|
|
1017
1027
|
|
|
@@ -1247,6 +1257,9 @@ $$;
|
|
|
1247
1257
|
|
|
1248
1258
|
DROP FUNCTION IF EXISTS cerefox_restore_document(UUID, TEXT, TEXT, TEXT);
|
|
1249
1259
|
DROP FUNCTION IF EXISTS cerefox_restore_document(UUID, TEXT, TEXT);
|
|
1260
|
+
-- 0.12.1: the pre-author 1-arg overload survived every CREATE OR REPLACE
|
|
1261
|
+
-- since the signature grew (same orphan class as purge; found live on a long-lived database).
|
|
1262
|
+
DROP FUNCTION IF EXISTS cerefox_restore_document(UUID);
|
|
1250
1263
|
CREATE FUNCTION cerefox_restore_document(
|
|
1251
1264
|
p_document_id UUID,
|
|
1252
1265
|
p_author TEXT DEFAULT 'unknown',
|
|
@@ -1309,7 +1322,14 @@ $$;
|
|
|
1309
1322
|
-- ── cerefox_purge_document ───────────────────────────────────────────────────
|
|
1310
1323
|
-- Permanently deletes a soft-deleted document (CASCADE). Only works on
|
|
1311
1324
|
-- documents that are already soft-deleted (deleted_at IS NOT NULL).
|
|
1325
|
+
--
|
|
1326
|
+
-- 0.12.1: drop the pre-author 1-arg overload. CREATE OR REPLACE never removed
|
|
1327
|
+
-- it when the signature grew, so long-lived databases carried BOTH — and a
|
|
1328
|
+
-- 1-arg named call was ambiguous there (PGRST203), which is how the first
|
|
1329
|
+
-- prod acceptance run failed to purge its fixtures. Same cleanup for
|
|
1330
|
+
-- cerefox_restore_document below.
|
|
1312
1331
|
|
|
1332
|
+
DROP FUNCTION IF EXISTS cerefox_purge_document(UUID);
|
|
1313
1333
|
CREATE OR REPLACE FUNCTION cerefox_purge_document(
|
|
1314
1334
|
p_document_id UUID,
|
|
1315
1335
|
p_author TEXT DEFAULT 'unknown',
|
|
@@ -1347,6 +1367,43 @@ END;
|
|
|
1347
1367
|
$$;
|
|
1348
1368
|
|
|
1349
1369
|
|
|
1370
|
+
-- ── cerefox_extract_doc_link_ids ─────────────────────────────────────────────
|
|
1371
|
+
-- The ONE implementation of [Text](uuid) link scanning (#214), shared by the
|
|
1372
|
+
-- write-time guard in cerefox_ingest_document and the cerefox_find_dead_links
|
|
1373
|
+
-- sweep — "same scanning rules" is enforced by this being the only copy.
|
|
1374
|
+
--
|
|
1375
|
+
-- Fences are LINE-ANCHORED and handled by SPLITTING, not by a paired-fence
|
|
1376
|
+
-- regex: Postgres AREs give the whole RE the greediness of their first
|
|
1377
|
+
-- quantified atom, which silently overrode a later .*? and made a closed
|
|
1378
|
+
-- fence strip everything to end-of-string (round-5 review, verified live) —
|
|
1379
|
+
-- blinding the scan to every link after any code block. Odd-numbered split
|
|
1380
|
+
-- segments are outside fences; an unterminated fence leaves its tail inside
|
|
1381
|
+
-- an even segment, dropped (under-validate, never false-reject). Inline code
|
|
1382
|
+
-- spans are stripped after (their regex has no greediness hazard).
|
|
1383
|
+
|
|
1384
|
+
CREATE OR REPLACE FUNCTION cerefox_extract_doc_link_ids(p_content TEXT)
|
|
1385
|
+
RETURNS TABLE (link_id TEXT)
|
|
1386
|
+
LANGUAGE sql
|
|
1387
|
+
IMMUTABLE
|
|
1388
|
+
SET search_path = public, pg_catalog
|
|
1389
|
+
AS $$
|
|
1390
|
+
WITH segments AS (
|
|
1391
|
+
SELECT seg, row_number() OVER () AS rn
|
|
1392
|
+
FROM regexp_split_to_table(COALESCE(p_content, ''), '^[ \t]*```.*$', 'n') AS seg
|
|
1393
|
+
),
|
|
1394
|
+
outside AS (
|
|
1395
|
+
SELECT string_agg(regexp_replace(seg, '`[^`]*`', ' ', 'g'), ' ') AS s
|
|
1396
|
+
FROM segments
|
|
1397
|
+
WHERE rn % 2 = 1
|
|
1398
|
+
)
|
|
1399
|
+
SELECT lower(m[1])
|
|
1400
|
+
FROM outside,
|
|
1401
|
+
LATERAL regexp_matches(
|
|
1402
|
+
outside.s,
|
|
1403
|
+
'\]\(([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\)',
|
|
1404
|
+
'g') AS m;
|
|
1405
|
+
$$;
|
|
1406
|
+
|
|
1350
1407
|
-- ── cerefox_ingest_document ──────────────────────────────────────────────────
|
|
1351
1408
|
-- Single RPC for ingesting a document (create or update). Handles:
|
|
1352
1409
|
-- - Create: insert document row, insert chunks, set review_status, create audit entry
|
|
@@ -1494,6 +1551,19 @@ BEGIN
|
|
|
1494
1551
|
USING ERRCODE = '22023'; -- invalid_parameter_value
|
|
1495
1552
|
END IF;
|
|
1496
1553
|
|
|
1554
|
+
-- ── Metadata type guard (#212, 0.12.2) ───────────────────────────────
|
|
1555
|
+
-- metadata is jsonb and accepts ANY JSON value, but every reader assumes
|
|
1556
|
+
-- an object — and `document edit`'s JS spread DECOMPOSED a stored string
|
|
1557
|
+
-- into per-character keys (13 real documents were in the vulnerable
|
|
1558
|
+
-- state). The MCP layer always validated this; the RPC now does too, so
|
|
1559
|
+
-- every write path agrees (CLI, scripts, direct PostgREST included).
|
|
1560
|
+
IF p_metadata IS NOT NULL AND jsonb_typeof(p_metadata) <> 'object' THEN
|
|
1561
|
+
RAISE EXCEPTION
|
|
1562
|
+
'cerefox_ingest_document: metadata must be a JSON object, got %. Wrap scalar values in a key ({"value": ...}).',
|
|
1563
|
+
jsonb_typeof(p_metadata)
|
|
1564
|
+
USING ERRCODE = '22023'; -- invalid_parameter_value
|
|
1565
|
+
END IF;
|
|
1566
|
+
|
|
1497
1567
|
-- ── Link integrity (#214, 0.12.0) ────────────────────────────────────
|
|
1498
1568
|
-- Validate [Text](uuid) document links against the store: agents mangle
|
|
1499
1569
|
-- long random ids when regenerating text, and a mangled id silently
|
|
@@ -1506,23 +1576,14 @@ BEGIN
|
|
|
1506
1576
|
-- See docs/specs/link-integrity-design.md.
|
|
1507
1577
|
SELECT string_agg(c->>'content', E'\n') INTO v_scannable
|
|
1508
1578
|
FROM jsonb_array_elements(p_chunks) c;
|
|
1509
|
-
|
|
1510
|
-
--
|
|
1511
|
-
--
|
|
1512
|
-
--
|
|
1513
|
-
--
|
|
1514
|
-
|
|
1515
|
-
v_scannable
|
|
1516
|
-
|
|
1517
|
-
E'(^|\\n)[ \\t]*```.*?(\\n[ \\t]*```[^\\n]*|$)', ' ', 'g');
|
|
1518
|
-
v_scannable := regexp_replace(v_scannable, '`[^`]*`', ' ', 'g');
|
|
1519
|
-
|
|
1520
|
-
SELECT array_agg(DISTINCT m[1]) INTO v_missing
|
|
1521
|
-
FROM regexp_matches(
|
|
1522
|
-
v_scannable,
|
|
1523
|
-
'\]\(([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\)',
|
|
1524
|
-
'g') m
|
|
1525
|
-
WHERE NOT EXISTS (SELECT 1 FROM cerefox_documents d WHERE d.id = m[1]::uuid);
|
|
1579
|
+
|
|
1580
|
+
-- Scanning delegated to cerefox_extract_doc_link_ids — the one copy of
|
|
1581
|
+
-- the fence/inline-code/uuid rules, shared with the dead-link sweep
|
|
1582
|
+
-- (0.12.2; the previous inline regex was defeated by ARE whole-RE
|
|
1583
|
+
-- greediness — see the helper's header).
|
|
1584
|
+
SELECT array_agg(DISTINCT l.link_id) INTO v_missing
|
|
1585
|
+
FROM cerefox_extract_doc_link_ids(v_scannable) l
|
|
1586
|
+
WHERE NOT EXISTS (SELECT 1 FROM cerefox_documents d WHERE d.id = l.link_id::uuid);
|
|
1526
1587
|
|
|
1527
1588
|
-- On UPDATE, tolerate dead links the document ALREADY carries: a target
|
|
1528
1589
|
-- purged after linking must not make the document unwritable — sync
|
|
@@ -1778,9 +1839,9 @@ $$;
|
|
|
1778
1839
|
-- Reads chunk title and content directly from the DB -- caller only needs to
|
|
1779
1840
|
-- supply the new document title.
|
|
1780
1841
|
--
|
|
1781
|
-
-- Only affects current chunks (version_id IS NULL). Archived chunks
|
|
1782
|
-
--
|
|
1783
|
-
-- re-
|
|
1842
|
+
-- Only affects current chunks (version_id IS NULL). Archived chunks carry NO
|
|
1843
|
+
-- tsvector at all since 0.13.0 (#216) — fts is nulled at archive time, and a
|
|
1844
|
+
-- restore is a re-ingest that recomputes everything.
|
|
1784
1845
|
|
|
1785
1846
|
DROP FUNCTION IF EXISTS cerefox_update_chunk_fts(UUID, TEXT);
|
|
1786
1847
|
CREATE FUNCTION cerefox_update_chunk_fts(
|
|
@@ -2662,6 +2723,30 @@ BEGIN
|
|
|
2662
2723
|
USING ERRCODE = 'P0002';
|
|
2663
2724
|
END IF;
|
|
2664
2725
|
|
|
2726
|
+
-- #212: a legacy row can hold NON-OBJECT metadata (the ingest RPC did not
|
|
2727
|
+
-- validate its input until 0.12.2). Merging onto it with || would produce
|
|
2728
|
+
-- an ARRAY — Postgres treats both sides as arrays — burying the stored
|
|
2729
|
+
-- value one level deeper and leaving the row still corrupt. Only replace
|
|
2730
|
+
-- actually repairs such a row, so refuse the merge and say so. jsonb
|
|
2731
|
+
-- 'null' is treated like SQL NULL (empty), matching the CLI.
|
|
2732
|
+
IF NOT p_replace AND v_before IS NOT NULL
|
|
2733
|
+
AND jsonb_typeof(v_before) NOT IN ('object', 'null') THEN
|
|
2734
|
+
RAISE EXCEPTION
|
|
2735
|
+
'CEREFOX_BAD_METADATA: stored metadata on document % is not an object (%). A merge cannot repair it — retry with replace (CLI: --replace; MCP: replace: true), passing the full intended object.',
|
|
2736
|
+
p_document_id, jsonb_typeof(v_before)
|
|
2737
|
+
USING ERRCODE = '22023';
|
|
2738
|
+
END IF;
|
|
2739
|
+
|
|
2740
|
+
-- Normalized BEFORE value for merge and reporting: everything below must
|
|
2741
|
+
-- work when the stored value is a scalar/array/'null' and p_replace is
|
|
2742
|
+
-- true — that is the REPAIR path, and jsonb_object_keys on a scalar
|
|
2743
|
+
-- errors, which would roll back the repair itself (round-5 review,
|
|
2744
|
+
-- verified live).
|
|
2745
|
+
v_before := CASE
|
|
2746
|
+
WHEN v_before IS NULL OR jsonb_typeof(v_before) <> 'object' THEN '{}'::jsonb
|
|
2747
|
+
ELSE v_before
|
|
2748
|
+
END;
|
|
2749
|
+
|
|
2665
2750
|
-- Keys explicitly set to null are removals, never stored values.
|
|
2666
2751
|
SELECT COALESCE(array_agg(key), ARRAY[]::TEXT[]) INTO v_null_keys
|
|
2667
2752
|
FROM jsonb_each(p_metadata)
|
|
@@ -2713,6 +2798,15 @@ SET search_path = public, pg_catalog
|
|
|
2713
2798
|
AS $$
|
|
2714
2799
|
-- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
|
|
2715
2800
|
-- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
|
|
2801
|
+
-- 0.13.0 (#216): archived chunks carry no search artifacts —
|
|
2802
|
+
-- cerefox_snapshot_version nulls embedding_primary/embedding_upgrade/fts
|
|
2803
|
+
-- at archive time; embedding_primary becomes nullable; migration 0027
|
|
2804
|
+
-- strips existing archived rows.
|
|
2805
|
+
-- 0.12.2 (#212, #214): metadata must be a JSON object (ingest input guard
|
|
2806
|
+
-- + set_document_metadata stored-state merge guard); cerefox_find_dead_links
|
|
2807
|
+
-- (link-integrity phase-2 sweep); cerefox_metadata_health (doctor check).
|
|
2808
|
+
-- 0.12.1: drop orphaned 1-arg overloads of purge/restore (pre-author era;
|
|
2809
|
+
-- CREATE OR REPLACE never removed them; ambiguous PGRST203 on 1-arg calls).
|
|
2716
2810
|
-- 0.12.0 (#208, #210): cerefox_delete_document — CAS
|
|
2717
2811
|
-- (p_expected_content_hash), p_reason in audit description, JSONB return,
|
|
2718
2812
|
-- idempotent re-delete; cerefox_restore_document — same rework (JSONB,
|
|
@@ -2726,7 +2820,75 @@ AS $$
|
|
|
2726
2820
|
-- 0.11.0 supersedes 0.10.6 (v1.2.1, #191): this branch carries that fix plus
|
|
2727
2821
|
-- the partial-edit surface, and both migrations (0019, 0020) are in the
|
|
2728
2822
|
-- sequence, so a store deploying this gets everything from both lines.
|
|
2729
|
-
SELECT '0.
|
|
2823
|
+
SELECT '0.13.0'::TEXT;
|
|
2824
|
+
$$;
|
|
2825
|
+
|
|
2826
|
+
-- ── cerefox_find_dead_links ──────────────────────────────────────────────────
|
|
2827
|
+
-- Phase 2 of link integrity (#214): a read-only whole-KB sweep for
|
|
2828
|
+
-- [Text](uuid) links whose target document no longer EXISTS (purged after
|
|
2829
|
+
-- linking). Complements the write-time guard, which validates only
|
|
2830
|
+
-- newly-introduced links on updates. Same scanning rules as the guard:
|
|
2831
|
+
-- line-anchored fences and inline code spans are stripped (examples are not
|
|
2832
|
+
-- links); a trashed target still exists and is NOT a dead link.
|
|
2833
|
+
-- Full chunk scan — run on demand (CLI `cerefox document dead-links`), not on
|
|
2834
|
+
-- every doctor.
|
|
2835
|
+
|
|
2836
|
+
CREATE OR REPLACE FUNCTION cerefox_find_dead_links()
|
|
2837
|
+
RETURNS TABLE (
|
|
2838
|
+
document_id UUID,
|
|
2839
|
+
document_title TEXT,
|
|
2840
|
+
dead_link_id UUID,
|
|
2841
|
+
occurrences BIGINT
|
|
2842
|
+
)
|
|
2843
|
+
LANGUAGE sql
|
|
2844
|
+
STABLE
|
|
2845
|
+
SECURITY DEFINER
|
|
2846
|
+
SET search_path = public, pg_catalog
|
|
2847
|
+
AS $$
|
|
2848
|
+
-- Scanning delegated to cerefox_extract_doc_link_ids — the one copy of
|
|
2849
|
+
-- the fence/inline-code/uuid rules, shared with the write-time guard.
|
|
2850
|
+
-- Deliberate scope: trashed LINKER documents are excluded (they are
|
|
2851
|
+
-- inert until restored; restoring one re-enters it into the next sweep).
|
|
2852
|
+
WITH doc_content AS (
|
|
2853
|
+
SELECT d.id, d.title, string_agg(c.content, E'\n' ORDER BY c.chunk_index) AS content
|
|
2854
|
+
FROM cerefox_documents d
|
|
2855
|
+
JOIN cerefox_chunks c ON c.document_id = d.id AND c.version_id IS NULL
|
|
2856
|
+
WHERE d.deleted_at IS NULL
|
|
2857
|
+
GROUP BY d.id, d.title
|
|
2858
|
+
),
|
|
2859
|
+
links AS (
|
|
2860
|
+
SELECT dc.id, dc.title, l.link_id AS target
|
|
2861
|
+
FROM doc_content dc,
|
|
2862
|
+
LATERAL cerefox_extract_doc_link_ids(dc.content) l
|
|
2863
|
+
)
|
|
2864
|
+
SELECT l.id, l.title, l.target::uuid, count(*)
|
|
2865
|
+
FROM links l
|
|
2866
|
+
WHERE NOT EXISTS (SELECT 1 FROM cerefox_documents t WHERE t.id = l.target::uuid)
|
|
2867
|
+
GROUP BY l.id, l.title, l.target
|
|
2868
|
+
ORDER BY l.title, l.target;
|
|
2869
|
+
$$;
|
|
2870
|
+
|
|
2871
|
+
-- ── cerefox_metadata_health ──────────────────────────────────────────────────
|
|
2872
|
+
-- #212: rows whose stored metadata is not a JSON object — the state the
|
|
2873
|
+
-- 0.12.2 write guards now prevent, but which legacy rows may still be in.
|
|
2874
|
+
-- Cheap (documents table only); surfaced by `cerefox doctor`. Repair:
|
|
2875
|
+
-- `cerefox document set-metadata <id> --replace --json '<object>'`.
|
|
2876
|
+
|
|
2877
|
+
CREATE OR REPLACE FUNCTION cerefox_metadata_health()
|
|
2878
|
+
RETURNS TABLE (
|
|
2879
|
+
document_id UUID,
|
|
2880
|
+
document_title TEXT,
|
|
2881
|
+
metadata_type TEXT
|
|
2882
|
+
)
|
|
2883
|
+
LANGUAGE sql
|
|
2884
|
+
STABLE
|
|
2885
|
+
SECURITY DEFINER
|
|
2886
|
+
SET search_path = public, pg_catalog
|
|
2887
|
+
AS $$
|
|
2888
|
+
SELECT d.id, d.title, jsonb_typeof(d.metadata)
|
|
2889
|
+
FROM cerefox_documents d
|
|
2890
|
+
WHERE d.metadata IS NOT NULL AND jsonb_typeof(d.metadata) <> 'object'
|
|
2891
|
+
ORDER BY d.title;
|
|
2730
2892
|
$$;
|
|
2731
2893
|
|
|
2732
2894
|
-- ── 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.13.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 —
|
|
@@ -42,7 +42,15 @@ CREATE TABLE IF NOT EXISTS cerefox_documents (
|
|
|
42
42
|
source_path TEXT,
|
|
43
43
|
-- SHA-256 of raw markdown content; used for deduplication
|
|
44
44
|
content_hash TEXT NOT NULL,
|
|
45
|
-
|
|
45
|
+
-- #212 (0.12.2): must be a JSON OBJECT — jsonb accepts any JSON value,
|
|
46
|
+
-- but every reader assumes an object, and non-object values were silently
|
|
47
|
+
-- destroyed by read-modify-write paths. The table-level CHECK closes all
|
|
48
|
+
-- current AND future direct writers at once; existing databases get it as
|
|
49
|
+
-- NOT VALID via migration 0026 (legacy rows survive until repaired with
|
|
50
|
+
-- `document set-metadata --replace`, which the constraint then validates).
|
|
51
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb
|
|
52
|
+
CONSTRAINT cerefox_documents_metadata_object
|
|
53
|
+
CHECK (jsonb_typeof(metadata) = 'object'),
|
|
46
54
|
chunk_count INT NOT NULL DEFAULT 0,
|
|
47
55
|
total_chars INT NOT NULL DEFAULT 0,
|
|
48
56
|
-- review_status: human governance flag. 'approved' = validated by human,
|
|
@@ -204,8 +212,16 @@ CREATE TABLE IF NOT EXISTS cerefox_chunks (
|
|
|
204
212
|
-- reconstructs with its OWN format. See docs/guides/content-format.md.
|
|
205
213
|
content_format SMALLINT NOT NULL DEFAULT 1,
|
|
206
214
|
|
|
207
|
-
-- Primary embedding: always computed, cloud API (default: OpenAI
|
|
208
|
-
|
|
215
|
+
-- Primary embedding: always computed on write, cloud API (default: OpenAI
|
|
216
|
+
-- text-embedding-3-small). NULL only on ARCHIVED rows (0.13.0, #216 —
|
|
217
|
+
-- rationale in migration 0027): archiving nulls the search artifacts.
|
|
218
|
+
-- The CHECK below preserves the pre-0.13.0 guarantee that a CURRENT
|
|
219
|
+
-- chunk always has an embedding — without it, a short embedding-API
|
|
220
|
+
-- response could insert a silently search-invisible chunk where the old
|
|
221
|
+
-- NOT NULL failed loudly.
|
|
222
|
+
embedding_primary VECTOR(768)
|
|
223
|
+
CONSTRAINT cerefox_chunks_current_has_embedding
|
|
224
|
+
CHECK (version_id IS NOT NULL OR embedding_primary IS NOT NULL),
|
|
209
225
|
-- Upgrade embedding: optional, alternative model (Fireworks, Vertex, etc.)
|
|
210
226
|
embedding_upgrade VECTOR(768),
|
|
211
227
|
|
package/docs/guides/cli.md
CHANGED
|
@@ -226,6 +226,12 @@ cerefox document list [OPTIONS]
|
|
|
226
226
|
|
|
227
227
|
**Purpose**: update a document's title and/or metadata in place, without re-ingesting content.
|
|
228
228
|
|
|
229
|
+
> **v1.7.1 (#212)**: a title-only edit no longer writes `metadata` at all, and
|
|
230
|
+
> the command refuses to patch a document whose stored metadata is not a JSON
|
|
231
|
+
> object (a legacy state older writes could create) instead of destroying it —
|
|
232
|
+
> repair such a row with `cerefox document set-metadata <id> --replace --json
|
|
233
|
+
> '<object>'`. `cerefox doctor` lists any rows in that state.
|
|
234
|
+
|
|
229
235
|
**Synopsis**:
|
|
230
236
|
```
|
|
231
237
|
cerefox document edit [OPTIONS] DOCUMENT_ID
|
|
@@ -341,6 +347,20 @@ cerefox document set-projects <doc-id> --clear
|
|
|
341
347
|
|
|
342
348
|
---
|
|
343
349
|
|
|
350
|
+
### `cerefox document dead-links`
|
|
351
|
+
|
|
352
|
+
**Purpose**: whole-KB sweep for `[Text](uuid)` links whose target document no longer exists (phase 2 of link integrity, #214; v1.7.1, needs schema 0.12.2).
|
|
353
|
+
|
|
354
|
+
**Synopsis**: `cerefox document dead-links [--json]`
|
|
355
|
+
|
|
356
|
+
The write-time guard (v1.7.0) validates only links a write *introduces* — deliberately, so a target purged after linking cannot make its linkers unwritable. This command finds those legacy dead links on demand. A trashed target still exists and is **not** reported; trashed **linker** documents are also excluded (inert until restored — a restore re-enters them into the next sweep). Full chunk scan server-side (one RPC call); run on demand, not part of `doctor`.
|
|
357
|
+
|
|
358
|
+
**Fix each hit** by editing the linking document: correct the id, remove the link, or backtick it as an example. Full linking overview: [`linking.md`](linking.md).
|
|
359
|
+
|
|
360
|
+
**MCP equivalent**: none (maintenance verb).
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
344
364
|
### `cerefox document restore`
|
|
345
365
|
|
|
346
366
|
**Purpose**: restore a soft-deleted (trashed) document back to active.
|
|
@@ -806,6 +826,7 @@ surface).
|
|
|
806
826
|
| `document get` | `cerefox_get_document` | ✅ |
|
|
807
827
|
| `document list` | `cerefox_metadata_search` (scope by `project_name` / metadata / time) | ✅ as of this change. Unscoped whole-KB listing has no MCP path by design (scope it) |
|
|
808
828
|
| `document edit` (title / metadata in place) | — | 🔒 intentional: a human/web-parity convenience. Agents update title+metadata deterministically via `cerefox_ingest` (with `document_id`); a metadata-only edit isn't a needed agent primitive |
|
|
829
|
+
| `document dead-links` | — | 🔒 intentional: a maintenance sweep for the operator (v1.7.1, #214 phase 2); agents encounter dead links through the write-time guard instead |
|
|
809
830
|
| `document delete` (soft-delete) | `cerefox_delete_document` | ✅ v1.7.0 (#208). MCP requires the caller's read-hash; the CLI confirms interactively instead |
|
|
810
831
|
| `document restore` | `cerefox_restore_document` | ✅ v1.7.0 (#210). Permanent purge remains web-UI-only |
|
|
811
832
|
| `document version list` | `cerefox_list_versions` | ✅ |
|
package/docs/guides/upgrading.md
CHANGED
|
@@ -150,6 +150,15 @@ knowing about:
|
|
|
150
150
|
you're coming from a pre-installer 0.1.x clone, see the "old pre-installer
|
|
151
151
|
clone" note above: install the package and run `cerefox init`.
|
|
152
152
|
|
|
153
|
+
## Notable: v1.8.0 storage reclaim (migration 0027)
|
|
154
|
+
|
|
155
|
+
Upgrading to v1.8.0 strips never-read search artifacts from archived version
|
|
156
|
+
chunks (rationale: #216 / the migration's own header) and the migration
|
|
157
|
+
prints what it freed. Postgres releases the bytes for **reuse** via
|
|
158
|
+
autovacuum rather than shrinking files immediately, so expect growth to stop
|
|
159
|
+
rather than the reported database size to drop the same day. Archived
|
|
160
|
+
version *content* is untouched.
|
|
161
|
+
|
|
153
162
|
## After upgrading: AI agents
|
|
154
163
|
|
|
155
164
|
New tools and updated tool signatures are picked up by MCP clients in **new
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cerefox/memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
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",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"typescript": "^6.0.3"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
|
-
"build": "bun build src/bin/cerefox.ts --outdir dist/bin --target node --format esm --external @huggingface/transformers --external onnxruntime-node",
|
|
68
|
+
"build": "bun run bundle-server-assets && bun build src/bin/cerefox.ts --outdir dist/bin --target node --format esm --external @huggingface/transformers --external onnxruntime-node",
|
|
69
69
|
"clean": "rm -rf dist docs AGENT_GUIDE.md AGENT_QUICK_REFERENCE.md",
|
|
70
70
|
"bundle-docs": "bun run ../../scripts/bundle_package_docs.ts",
|
|
71
71
|
"bundle-server-assets": "bun run ../../scripts/bundle_server_assets.ts",
|