@cerefox/memory 1.17.4 → 1.19.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 +8 -3
- package/AGENT_QUICK_REFERENCE.md +3 -3
- package/README.md +1 -1
- package/dist/bin/cerefox.js +1542 -1123
- package/dist/frontend/assets/index-01k5C3sG.css +1 -0
- package/dist/frontend/assets/index-HNs8rgY1.js +121 -0
- package/dist/frontend/assets/index-HNs8rgY1.js.map +1 -0
- package/dist/frontend/index.html +2 -2
- package/dist/server-assets/_shared/ef-meta/index.ts +3 -3
- package/dist/server-assets/_shared/embeddings/onnx-embedder.ts +25 -9
- package/dist/server-assets/_shared/mcp-tools/_document-meta.ts +24 -2
- package/dist/server-assets/_shared/mcp-tools/_utils.ts +92 -0
- package/dist/server-assets/_shared/mcp-tools/audit-log.ts +6 -2
- package/dist/server-assets/_shared/mcp-tools/audit-ops.ts +27 -0
- package/dist/server-assets/_shared/mcp-tools/get-document.ts +11 -1
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +3 -3
- package/dist/server-assets/_shared/mcp-tools/ingest.ts +55 -17
- package/dist/server-assets/_shared/mcp-tools/metadata-search.ts +3 -1
- package/dist/server-assets/_shared/mcp-tools/partial-edits.ts +8 -13
- package/dist/server-assets/_shared/mcp-tools/search.ts +11 -2
- package/dist/server-assets/_shared/mcp-tools/set-document-metadata.ts +31 -5
- package/dist/server-assets/db/migrations/0012_content_format.sql +1 -1
- package/dist/server-assets/db/migrations/0014_document_relations.sql +1 -1
- package/dist/server-assets/db/migrations/0020_ingest_preserves_source.sql +1 -1
- package/dist/server-assets/db/migrations/0022_rls_on_document_relations.sql +1 -1
- package/dist/server-assets/db/migrations/0032_derive_grants.sql +1 -1
- package/dist/server-assets/db/migrations/0034_drop_vector_indexes.sql +22 -0
- package/dist/server-assets/db/migrations/0035_drop_content_hash_unique.sql +21 -0
- package/dist/server-assets/db/migrations/0036_metadata_revision.sql +43 -0
- package/dist/server-assets/db/rpcs.sql +260 -58
- package/dist/server-assets/db/schema.sql +49 -18
- package/dist/server-assets/supabase/functions/cerefox-get-document/index.ts +8 -1
- package/dist/server-assets/supabase/functions/cerefox-ingest/index.ts +42 -6
- package/docs/api/openapi.json +77 -3
- package/docs/guides/access-paths.md +7 -3
- package/docs/guides/api.md +1 -1
- package/docs/guides/cli.md +5 -3
- package/docs/guides/configuration.md +23 -2
- package/docs/guides/connect-agents.md +60 -25
- package/docs/guides/ops-scripts.md +1 -1
- package/docs/guides/quickstart.md +7 -3
- package/docs/guides/setup-local.md +30 -6
- package/docs/guides/setup-supabase.md +5 -0
- package/docs/guides/upgrading.md +16 -1
- package/package.json +7 -7
- package/dist/frontend/assets/index-B0H_N4if.css +0 -1
- package/dist/frontend/assets/index-D1kdLLr8.js +0 -121
- package/dist/frontend/assets/index-D1kdLLr8.js.map +0 -1
|
@@ -81,6 +81,11 @@ DROP FUNCTION IF EXISTS cerefox_search_docs(TEXT, VECTOR(768), INT, FLOAT, UUID,
|
|
|
81
81
|
DROP FUNCTION IF EXISTS cerefox_hybrid_search(TEXT, VECTOR(768), INT, FLOAT, BOOLEAN, UUID, FLOAT, JSONB, FLOAT);
|
|
82
82
|
DROP FUNCTION IF EXISTS cerefox_search_docs(TEXT, VECTOR(768), INT, FLOAT, UUID, FLOAT, INT, INT, JSONB, FLOAT);
|
|
83
83
|
|
|
84
|
+
-- #333 (0.21.0): metadata_revision added to the return types of the
|
|
85
|
+
-- document-shaped reads, next to content_hash. Drop the current signatures.
|
|
86
|
+
DROP FUNCTION IF EXISTS cerefox_search_docs(TEXT, VECTOR(768), INT, FLOAT, UUID, FLOAT, INT, INT, JSONB, FLOAT, TEXT);
|
|
87
|
+
DROP FUNCTION IF EXISTS cerefox_metadata_search(JSONB, UUID, TIMESTAMPTZ, TIMESTAMPTZ, INT, BOOLEAN, INT);
|
|
88
|
+
|
|
84
89
|
-- ── Shared return type note ────────────────────────────────────────────────────
|
|
85
90
|
-- All chunk-level search RPCs return the same shape for consistency:
|
|
86
91
|
-- chunk_id, document_id, chunk_index, title, content, heading_path,
|
|
@@ -236,7 +241,7 @@ BEGIN
|
|
|
236
241
|
fts_results AS (
|
|
237
242
|
SELECT
|
|
238
243
|
c.id,
|
|
239
|
-
-- 0.18.0:
|
|
244
|
+
-- 0.18.0: normalization 32 bounds the keyword score to
|
|
240
245
|
-- rank/(rank+1), in [0, 1) like the cosine it is fused with.
|
|
241
246
|
-- Unbounded (up to ~4 when every term matches), one keyword
|
|
242
247
|
-- hit outweighed any semantic evidence, and the same document
|
|
@@ -305,7 +310,8 @@ BEGIN
|
|
|
305
310
|
-- hnsw.ef_search (40) rows: on a store with heavy version churn it
|
|
306
311
|
-- returned far fewer candidates than asked for, mostly not the
|
|
307
312
|
-- nearest ones. An exact scan is also faster at knowledge-base
|
|
308
|
-
-- scale (thousands of chunks).
|
|
313
|
+
-- scale (thousands of chunks). 0.19.0 dropped the HNSW indexes
|
|
314
|
+
-- altogether (migration 0034).
|
|
309
315
|
ORDER BY vec_score DESC
|
|
310
316
|
LIMIT candidate_count
|
|
311
317
|
),
|
|
@@ -787,7 +793,10 @@ RETURNS TABLE (
|
|
|
787
793
|
content_hash TEXT,
|
|
788
794
|
-- 28I: TRUE when this is a below-confidence fallback response (nothing
|
|
789
795
|
-- cleared the hybrid pass-filter). See cerefox_hybrid_search.
|
|
790
|
-
below_confidence BOOLEAN
|
|
796
|
+
below_confidence BOOLEAN,
|
|
797
|
+
-- #333: the metadata's concurrency token, to pass back as
|
|
798
|
+
-- expected_metadata_revision on a metadata write.
|
|
799
|
+
metadata_revision INT
|
|
791
800
|
)
|
|
792
801
|
LANGUAGE sql
|
|
793
802
|
SECURITY DEFINER
|
|
@@ -824,7 +833,8 @@ AS $$
|
|
|
824
833
|
cr.version_count,
|
|
825
834
|
cr.below_confidence,
|
|
826
835
|
d.updated_at AS doc_updated_at,
|
|
827
|
-
d.content_hash
|
|
836
|
+
d.content_hash,
|
|
837
|
+
d.metadata_revision
|
|
828
838
|
FROM chunk_results cr
|
|
829
839
|
JOIN cerefox_documents d ON d.id = cr.document_id
|
|
830
840
|
ORDER BY cr.document_id, cr.score DESC
|
|
@@ -909,7 +919,8 @@ AS $$
|
|
|
909
919
|
td.version_count,
|
|
910
920
|
ac.is_partial,
|
|
911
921
|
td.content_hash,
|
|
912
|
-
td.below_confidence
|
|
922
|
+
td.below_confidence,
|
|
923
|
+
td.metadata_revision
|
|
913
924
|
FROM top_docs td
|
|
914
925
|
JOIN doc_sizes ds ON ds.document_id = td.document_id
|
|
915
926
|
JOIN all_content ac ON ac.document_id = td.document_id
|
|
@@ -1068,7 +1079,9 @@ RETURNS TABLE (
|
|
|
1068
1079
|
-- Current content_hash of the document — the optimistic-concurrency token
|
|
1069
1080
|
-- to pass back as expected_content_hash on update (iter-32). Note: always
|
|
1070
1081
|
-- the CURRENT hash, even when an archived version is being retrieved.
|
|
1071
|
-
content_hash TEXT
|
|
1082
|
+
content_hash TEXT,
|
|
1083
|
+
-- #333: the metadata's concurrency token (always the current one).
|
|
1084
|
+
metadata_revision INT
|
|
1072
1085
|
)
|
|
1073
1086
|
LANGUAGE sql
|
|
1074
1087
|
SECURITY DEFINER
|
|
@@ -1090,7 +1103,8 @@ AS $$
|
|
|
1090
1103
|
COUNT(*)::INT AS chunk_count,
|
|
1091
1104
|
SUM(c.char_count)::INT AS total_chars,
|
|
1092
1105
|
d.created_at,
|
|
1093
|
-
d.content_hash
|
|
1106
|
+
d.content_hash,
|
|
1107
|
+
d.metadata_revision
|
|
1094
1108
|
FROM cerefox_documents d
|
|
1095
1109
|
JOIN cerefox_chunks c ON c.document_id = d.id
|
|
1096
1110
|
WHERE d.id = p_document_id
|
|
@@ -1098,7 +1112,8 @@ AS $$
|
|
|
1098
1112
|
(p_version_id IS NULL AND c.version_id IS NULL) OR
|
|
1099
1113
|
(p_version_id IS NOT NULL AND c.version_id = p_version_id)
|
|
1100
1114
|
)
|
|
1101
|
-
GROUP BY d.id, d.title, d.source, d.metadata, d.created_at, d.content_hash
|
|
1115
|
+
GROUP BY d.id, d.title, d.source, d.metadata, d.created_at, d.content_hash,
|
|
1116
|
+
d.metadata_revision;
|
|
1102
1117
|
$$;
|
|
1103
1118
|
|
|
1104
1119
|
-- ── cerefox_list_document_versions ────────────────────────────────────────────
|
|
@@ -1511,8 +1526,11 @@ $$;
|
|
|
1511
1526
|
-- p_source_label, which records how THIS write was
|
|
1512
1527
|
-- triggered and is stored on the version row.
|
|
1513
1528
|
-- p_metadata : JSONB metadata. NULL = "not provided" → create uses '{}',
|
|
1514
|
-
-- update keeps the existing metadata (v0.11.1).
|
|
1515
|
-
--
|
|
1529
|
+
-- update keeps the existing metadata (v0.11.1). On update
|
|
1530
|
+
-- it MERGES (JSON null removes a key) unless
|
|
1531
|
+
-- p_replace_metadata (#333, 0.21.0); '{}' is a no-op merge.
|
|
1532
|
+
-- p_expected_metadata_revision, p_replace_metadata : the metadata token and
|
|
1533
|
+
-- the replace switch (#333); see cerefox_check_metadata_revision.
|
|
1516
1534
|
-- p_review_status : ACCEPTED AND IGNORED since 0.16.0 (#241). The status is
|
|
1517
1535
|
-- decided here from p_author_type alone (agent →
|
|
1518
1536
|
-- pending_review, user → approved), so every transport
|
|
@@ -1538,8 +1556,67 @@ $$;
|
|
|
1538
1556
|
-- flows where an external source of truth makes conflicts
|
|
1539
1557
|
-- meaningless). Recorded in the audit description when used.
|
|
1540
1558
|
--
|
|
1541
|
-
-- Returns: document_id, chunk_count, total_chars, operation ('create' or
|
|
1542
|
-
-- version_id (UUID of snapshot, null on create)
|
|
1559
|
+
-- Returns: document_id, chunk_count, total_chars, operation ('create' or
|
|
1560
|
+
-- 'update-content'), version_id (UUID of snapshot, null on create).
|
|
1561
|
+
-- A create whose content_hash matches a LIVE document raises
|
|
1562
|
+
-- CEREFOX_DUPLICATE_CONTENT (23505) naming that document; nothing is
|
|
1563
|
+
-- written (0.20.0 #332; raised rather than returned since 0.21.1)
|
|
1564
|
+
|
|
1565
|
+
-- ── Metadata helpers (#333, 0.21.0) ──────────────────────────────────────────
|
|
1566
|
+
-- One copy of the merge/replace rule and of the token rule, shared by
|
|
1567
|
+
-- cerefox_set_document_metadata and cerefox_ingest_document.
|
|
1568
|
+
--
|
|
1569
|
+
-- cerefox_metadata_apply: RFC 7386-style. Merge = stored || patch; replace =
|
|
1570
|
+
-- the patch alone. Either way, keys whose value is JSON null are REMOVED, never
|
|
1571
|
+
-- stored (Cerefox metadata values are strings by convention, so a null is
|
|
1572
|
+
-- never a legitimate value).
|
|
1573
|
+
CREATE OR REPLACE FUNCTION cerefox_metadata_apply(
|
|
1574
|
+
p_stored JSONB,
|
|
1575
|
+
p_patch JSONB,
|
|
1576
|
+
p_replace BOOLEAN
|
|
1577
|
+
)
|
|
1578
|
+
RETURNS JSONB
|
|
1579
|
+
LANGUAGE sql
|
|
1580
|
+
IMMUTABLE
|
|
1581
|
+
SET search_path = public, pg_catalog
|
|
1582
|
+
AS $$
|
|
1583
|
+
SELECT (CASE WHEN p_replace THEN p_patch
|
|
1584
|
+
ELSE COALESCE(p_stored, '{}'::jsonb) || p_patch END)
|
|
1585
|
+
- COALESCE((SELECT array_agg(key) FROM jsonb_each(p_patch)
|
|
1586
|
+
WHERE value = 'null'::jsonb), ARRAY[]::TEXT[]);
|
|
1587
|
+
$$;
|
|
1588
|
+
|
|
1589
|
+
-- cerefox_check_metadata_revision: raise unless the write may proceed.
|
|
1590
|
+
-- expected passed and stale → CEREFOX_METADATA_CONFLICT (PT409)
|
|
1591
|
+
-- replace without token or LWW → CEREFOX_METADATA_TOKEN_REQUIRED (22023)
|
|
1592
|
+
-- PT409, never a retryable SQLSTATE: a stale token fails identically forever
|
|
1593
|
+
-- (see the CEREFOX_CONFLICT note in cerefox_ingest_document).
|
|
1594
|
+
CREATE OR REPLACE FUNCTION cerefox_check_metadata_revision(
|
|
1595
|
+
p_document_id UUID,
|
|
1596
|
+
p_current INT,
|
|
1597
|
+
p_expected INT,
|
|
1598
|
+
p_replace BOOLEAN,
|
|
1599
|
+
p_last_write_wins BOOLEAN
|
|
1600
|
+
)
|
|
1601
|
+
RETURNS VOID
|
|
1602
|
+
LANGUAGE plpgsql
|
|
1603
|
+
SET search_path = public, pg_catalog
|
|
1604
|
+
AS $$
|
|
1605
|
+
BEGIN
|
|
1606
|
+
IF p_expected IS NOT NULL AND p_expected <> p_current THEN
|
|
1607
|
+
RAISE EXCEPTION
|
|
1608
|
+
'CEREFOX_METADATA_CONFLICT: the metadata of document % changed since it was read (expected metadata_revision %, current %). Re-read the document, reapply your change, and retry with the new metadata_revision.',
|
|
1609
|
+
p_document_id, p_expected, p_current
|
|
1610
|
+
USING ERRCODE = 'PT409';
|
|
1611
|
+
END IF;
|
|
1612
|
+
IF p_replace AND p_expected IS NULL AND NOT COALESCE(p_last_write_wins, FALSE) THEN
|
|
1613
|
+
RAISE EXCEPTION
|
|
1614
|
+
'CEREFOX_METADATA_TOKEN_REQUIRED: replacing all metadata deletes keys, so it requires expected_metadata_revision (the metadata_revision you read) or last_write_wins=true. Current metadata_revision: %',
|
|
1615
|
+
p_current
|
|
1616
|
+
USING ERRCODE = '22023';
|
|
1617
|
+
END IF;
|
|
1618
|
+
END;
|
|
1619
|
+
$$;
|
|
1543
1620
|
|
|
1544
1621
|
DROP FUNCTION IF EXISTS cerefox_ingest_document(UUID, TEXT, TEXT, TEXT, TEXT, JSONB, TEXT, JSONB, TEXT, TEXT, TEXT, INT, BOOLEAN);
|
|
1545
1622
|
DROP FUNCTION IF EXISTS cerefox_ingest_document(UUID, TEXT, TEXT, TEXT, TEXT, JSONB, TEXT, JSONB, TEXT, TEXT, TEXT, INT, BOOLEAN, TEXT, BOOLEAN);
|
|
@@ -1548,6 +1625,9 @@ DROP FUNCTION IF EXISTS cerefox_ingest_document(UUID, TEXT, TEXT, TEXT, TEXT, JS
|
|
|
1548
1625
|
-- must be dropped before the 17-arg CREATE below (Postgres will not replace a
|
|
1549
1626
|
-- function whose RETURNS TABLE changed).
|
|
1550
1627
|
DROP FUNCTION IF EXISTS cerefox_ingest_document(UUID, TEXT, TEXT, TEXT, TEXT, JSONB, TEXT, JSONB, TEXT, TEXT, TEXT, INT, BOOLEAN, TEXT, BOOLEAN, SMALLINT, JSONB);
|
|
1628
|
+
-- #333 (0.21.0): + p_expected_metadata_revision, p_replace_metadata; returns
|
|
1629
|
+
-- metadata_revision.
|
|
1630
|
+
DROP FUNCTION IF EXISTS cerefox_ingest_document(UUID, TEXT, TEXT, TEXT, TEXT, JSONB, TEXT, JSONB, TEXT, TEXT, TEXT, INT, BOOLEAN, TEXT, BOOLEAN, SMALLINT, JSONB, INT, BOOLEAN);
|
|
1551
1631
|
CREATE FUNCTION cerefox_ingest_document(
|
|
1552
1632
|
p_document_id UUID DEFAULT NULL,
|
|
1553
1633
|
p_title TEXT DEFAULT 'Untitled',
|
|
@@ -1559,7 +1639,9 @@ CREATE FUNCTION cerefox_ingest_document(
|
|
|
1559
1639
|
p_content_hash TEXT DEFAULT '',
|
|
1560
1640
|
-- NULL = "not provided": create uses '{}', update KEEPS existing metadata
|
|
1561
1641
|
-- (v0.11.1 fix — content updates without metadata used to wipe tags).
|
|
1562
|
-
--
|
|
1642
|
+
-- #333 (0.21.0): on update this MERGES (a JSON null removes a key), unless
|
|
1643
|
+
-- p_replace_metadata. It used to replace the whole object, checked only
|
|
1644
|
+
-- against the content token, which could wipe another writer's change.
|
|
1563
1645
|
p_metadata JSONB DEFAULT NULL,
|
|
1564
1646
|
p_review_status TEXT DEFAULT 'approved',
|
|
1565
1647
|
p_chunks JSONB DEFAULT '[]',
|
|
@@ -1587,7 +1669,15 @@ CREATE FUNCTION cerefox_ingest_document(
|
|
|
1587
1669
|
-- flattening to 'update-content'. NULL-means-today, per the #183 lesson:
|
|
1588
1670
|
-- a parameter that substitutes its own concrete default is how store policy
|
|
1589
1671
|
-- got silently overridden for a whole release.
|
|
1590
|
-
p_operations JSONB DEFAULT NULL
|
|
1672
|
+
p_operations JSONB DEFAULT NULL,
|
|
1673
|
+
-- #333: the metadata's concurrency token. Checked on update when
|
|
1674
|
+
-- p_metadata is passed: strict when given; required (or p_last_write_wins)
|
|
1675
|
+
-- when p_replace_metadata. Ignored on create.
|
|
1676
|
+
p_expected_metadata_revision INT DEFAULT NULL,
|
|
1677
|
+
-- #333: replace the whole metadata object instead of merging. For callers
|
|
1678
|
+
-- whose metadata has an external source of truth (front matter synced
|
|
1679
|
+
-- with last_write_wins); everyone else merges.
|
|
1680
|
+
p_replace_metadata BOOLEAN DEFAULT FALSE
|
|
1591
1681
|
)
|
|
1592
1682
|
RETURNS TABLE (
|
|
1593
1683
|
document_id UUID,
|
|
@@ -1602,7 +1692,9 @@ RETURNS TABLE (
|
|
|
1602
1692
|
-- iter-33: true when the new size crosses `document_size_warning_chars`
|
|
1603
1693
|
-- (dormant when that config is unset/0). A signal, never a refusal: an
|
|
1604
1694
|
-- insert-only workflow otherwise never sees a document grow.
|
|
1605
|
-
size_warning BOOLEAN
|
|
1695
|
+
size_warning BOOLEAN,
|
|
1696
|
+
-- #333: the metadata's token after this write.
|
|
1697
|
+
metadata_revision INT
|
|
1606
1698
|
)
|
|
1607
1699
|
LANGUAGE plpgsql
|
|
1608
1700
|
SECURITY DEFINER
|
|
@@ -1610,6 +1702,7 @@ SET search_path = public, pg_catalog
|
|
|
1610
1702
|
AS $$
|
|
1611
1703
|
DECLARE
|
|
1612
1704
|
v_doc_id UUID;
|
|
1705
|
+
v_duplicate_id UUID;
|
|
1613
1706
|
v_chunk_count INT;
|
|
1614
1707
|
v_total_chars INT;
|
|
1615
1708
|
v_operation TEXT;
|
|
@@ -1625,6 +1718,9 @@ DECLARE
|
|
|
1625
1718
|
v_doc_deleted TIMESTAMPTZ;
|
|
1626
1719
|
v_scannable TEXT;
|
|
1627
1720
|
v_missing TEXT[];
|
|
1721
|
+
v_meta_before JSONB;
|
|
1722
|
+
v_meta_after JSONB;
|
|
1723
|
+
v_meta_rev INT;
|
|
1628
1724
|
BEGIN
|
|
1629
1725
|
-- ── Zero-chunk guard (v0.3.1) ────────────────────────────────────────
|
|
1630
1726
|
-- Refuse to create or update a document with no chunks. Three reasons:
|
|
@@ -1735,8 +1831,9 @@ BEGIN
|
|
|
1735
1831
|
-- updaters serialize here, and the second one sees the first one's
|
|
1736
1832
|
-- hash — the race window (chunk + embed latency) is closed at the
|
|
1737
1833
|
-- only place all transports share (iter-32).
|
|
1738
|
-
SELECT COALESCE(d.total_chars, 0), d.content_hash, d.deleted_at
|
|
1739
|
-
|
|
1834
|
+
SELECT COALESCE(d.total_chars, 0), d.content_hash, d.deleted_at,
|
|
1835
|
+
d.metadata, d.metadata_revision
|
|
1836
|
+
INTO v_old_chars, v_current_hash, v_doc_deleted, v_meta_before, v_meta_rev
|
|
1740
1837
|
FROM cerefox_documents d WHERE d.id = v_doc_id
|
|
1741
1838
|
FOR UPDATE;
|
|
1742
1839
|
|
|
@@ -1810,6 +1907,21 @@ BEGIN
|
|
|
1810
1907
|
END IF;
|
|
1811
1908
|
END IF;
|
|
1812
1909
|
|
|
1910
|
+
-- ── Metadata token (#333) ─────────────────────────────────────
|
|
1911
|
+
-- Only when this write touches metadata. Checked after the content
|
|
1912
|
+
-- token, so a caller holding a stale body learns that first.
|
|
1913
|
+
IF p_metadata IS NOT NULL THEN
|
|
1914
|
+
PERFORM cerefox_check_metadata_revision(
|
|
1915
|
+
v_doc_id, v_meta_rev, p_expected_metadata_revision,
|
|
1916
|
+
p_replace_metadata, p_last_write_wins);
|
|
1917
|
+
v_meta_after := cerefox_metadata_apply(
|
|
1918
|
+
CASE WHEN jsonb_typeof(v_meta_before) = 'object'
|
|
1919
|
+
THEN v_meta_before ELSE '{}'::jsonb END,
|
|
1920
|
+
p_metadata, p_replace_metadata);
|
|
1921
|
+
ELSE
|
|
1922
|
+
v_meta_after := v_meta_before;
|
|
1923
|
+
END IF;
|
|
1924
|
+
|
|
1813
1925
|
-- Snapshot old version (archives current chunks, runs retention cleanup)
|
|
1814
1926
|
SELECT sv.version_id INTO v_version_id
|
|
1815
1927
|
FROM cerefox_snapshot_version(v_doc_id, p_source_label, p_retention_hours, p_cleanup_enabled) sv;
|
|
@@ -1823,7 +1935,7 @@ BEGIN
|
|
|
1823
1935
|
source = COALESCE(p_source, source),
|
|
1824
1936
|
source_path = COALESCE(p_source_path, source_path),
|
|
1825
1937
|
content_hash = p_content_hash,
|
|
1826
|
-
metadata =
|
|
1938
|
+
metadata = v_meta_after,
|
|
1827
1939
|
chunk_count = v_chunk_count,
|
|
1828
1940
|
total_chars = v_total_chars,
|
|
1829
1941
|
review_status = v_status,
|
|
@@ -1834,11 +1946,45 @@ BEGIN
|
|
|
1834
1946
|
-- ── CREATE PATH ──────────────────────────────────────────────
|
|
1835
1947
|
v_operation := 'create';
|
|
1836
1948
|
|
|
1949
|
+
-- Duplicate check (0.20.0, #332). Content is no longer UNIQUE across
|
|
1950
|
+
-- the store: that constraint refused updates that happened to match
|
|
1951
|
+
-- some other document, including ones in the trash. Duplicates are
|
|
1952
|
+
-- now caught only on CREATE, and only against LIVE documents. Every
|
|
1953
|
+
-- caller pre-checks before embedding (to save the spend) and reports
|
|
1954
|
+
-- a match as skipped; this is the authoritative copy, made race-free
|
|
1955
|
+
-- by the advisory lock.
|
|
1956
|
+
--
|
|
1957
|
+
-- Losing the race RAISES rather than returning the winner (0.21.1,
|
|
1958
|
+
-- #334 review). A 1.18 client never read `operation`: handed the
|
|
1959
|
+
-- winner's id, it reported a create and replaced the winner's
|
|
1960
|
+
-- projects with its own. Raising under 23505 gives such a client what
|
|
1961
|
+
-- the old constraint gave it (a failed write, nothing after it), and
|
|
1962
|
+
-- current clients turn CEREFOX_DUPLICATE_CONTENT into a skip.
|
|
1963
|
+
IF NULLIF(BTRIM(p_content_hash), '') IS NOT NULL THEN
|
|
1964
|
+
PERFORM pg_advisory_xact_lock(
|
|
1965
|
+
hashtextextended('cerefox_content_hash:' || p_content_hash, 0));
|
|
1966
|
+
|
|
1967
|
+
-- Read into a dedicated variable, never into the create's own:
|
|
1968
|
+
-- a no-row SELECT INTO sets its targets to NULL, which once wiped
|
|
1969
|
+
-- v_chunk_count before the INSERT below.
|
|
1970
|
+
SELECT d.id INTO v_duplicate_id
|
|
1971
|
+
FROM cerefox_documents d
|
|
1972
|
+
WHERE d.content_hash = p_content_hash
|
|
1973
|
+
AND d.deleted_at IS NULL
|
|
1974
|
+
ORDER BY d.created_at
|
|
1975
|
+
LIMIT 1;
|
|
1976
|
+
IF v_duplicate_id IS NOT NULL THEN
|
|
1977
|
+
RAISE EXCEPTION 'CEREFOX_DUPLICATE_CONTENT: document % already has this content; nothing was written', v_duplicate_id
|
|
1978
|
+
USING ERRCODE = '23505';
|
|
1979
|
+
END IF;
|
|
1980
|
+
END IF;
|
|
1981
|
+
|
|
1837
1982
|
INSERT INTO cerefox_documents (
|
|
1838
1983
|
title, source, source_path, content_hash, metadata,
|
|
1839
1984
|
chunk_count, total_chars, review_status
|
|
1840
1985
|
) VALUES (
|
|
1841
|
-
p_title, COALESCE(p_source, 'agent'), p_source_path, p_content_hash,
|
|
1986
|
+
p_title, COALESCE(p_source, 'agent'), p_source_path, p_content_hash,
|
|
1987
|
+
cerefox_metadata_apply('{}'::jsonb, COALESCE(p_metadata, '{}'::jsonb), TRUE),
|
|
1842
1988
|
v_chunk_count, v_total_chars, v_status
|
|
1843
1989
|
)
|
|
1844
1990
|
RETURNING id INTO v_doc_id;
|
|
@@ -1920,6 +2066,24 @@ BEGIN
|
|
|
1920
2066
|
);
|
|
1921
2067
|
END IF;
|
|
1922
2068
|
|
|
2069
|
+
-- ── Metadata history (#333) ──────────────────────────────────────
|
|
2070
|
+
-- A content update that changed metadata also records it as its own
|
|
2071
|
+
-- 'update-metadata' entry, with the before/after values, so the metadata
|
|
2072
|
+
-- history is complete whichever path wrote it.
|
|
2073
|
+
IF v_operation = 'update-content' AND v_meta_after IS DISTINCT FROM v_meta_before THEN
|
|
2074
|
+
PERFORM cerefox_create_audit_entry(
|
|
2075
|
+
p_document_id := v_doc_id,
|
|
2076
|
+
p_version_id := v_version_id,
|
|
2077
|
+
p_operation := 'update-metadata',
|
|
2078
|
+
p_author := p_author,
|
|
2079
|
+
p_author_type := p_author_type,
|
|
2080
|
+
p_description := format('Metadata %s on "%s" (with a content update)',
|
|
2081
|
+
CASE WHEN p_replace_metadata THEN 'replaced' ELSE 'merged' END, p_title),
|
|
2082
|
+
p_metadata_before := v_meta_before,
|
|
2083
|
+
p_metadata_after := v_meta_after
|
|
2084
|
+
);
|
|
2085
|
+
END IF;
|
|
2086
|
+
|
|
1923
2087
|
-- ── Size signal (iter-33) ────────────────────────────────────────
|
|
1924
2088
|
-- Dormant unless an operator sets a threshold. Never blocks the write: a
|
|
1925
2089
|
-- size policy is not a correctness rule, and an agent that only ever
|
|
@@ -1930,7 +2094,9 @@ BEGIN
|
|
|
1930
2094
|
END IF;
|
|
1931
2095
|
|
|
1932
2096
|
RETURN QUERY SELECT v_doc_id, v_chunk_count, v_total_chars, v_operation,
|
|
1933
|
-
v_version_id, p_content_hash, v_size_warning
|
|
2097
|
+
v_version_id, p_content_hash, v_size_warning,
|
|
2098
|
+
(SELECT d.metadata_revision FROM cerefox_documents d
|
|
2099
|
+
WHERE d.id = v_doc_id);
|
|
1934
2100
|
END;
|
|
1935
2101
|
$$;
|
|
1936
2102
|
|
|
@@ -1974,6 +2140,8 @@ $$;
|
|
|
1974
2140
|
-- Returns the created entry's id and created_at.
|
|
1975
2141
|
|
|
1976
2142
|
DROP FUNCTION IF EXISTS cerefox_create_audit_entry(UUID, UUID, TEXT, TEXT, TEXT, INT, INT, TEXT);
|
|
2143
|
+
-- #333 (0.21.0): + metadata before/after, for 'update-metadata' entries.
|
|
2144
|
+
DROP FUNCTION IF EXISTS cerefox_create_audit_entry(UUID, UUID, TEXT, TEXT, TEXT, INT, INT, TEXT, JSONB, JSONB);
|
|
1977
2145
|
CREATE FUNCTION cerefox_create_audit_entry(
|
|
1978
2146
|
p_document_id UUID DEFAULT NULL,
|
|
1979
2147
|
p_version_id UUID DEFAULT NULL,
|
|
@@ -1982,7 +2150,9 @@ CREATE FUNCTION cerefox_create_audit_entry(
|
|
|
1982
2150
|
p_author_type TEXT DEFAULT 'user',
|
|
1983
2151
|
p_size_before INT DEFAULT NULL,
|
|
1984
2152
|
p_size_after INT DEFAULT NULL,
|
|
1985
|
-
p_description TEXT DEFAULT ''
|
|
2153
|
+
p_description TEXT DEFAULT '',
|
|
2154
|
+
p_metadata_before JSONB DEFAULT NULL,
|
|
2155
|
+
p_metadata_after JSONB DEFAULT NULL
|
|
1986
2156
|
)
|
|
1987
2157
|
RETURNS TABLE (
|
|
1988
2158
|
audit_id UUID,
|
|
@@ -1994,18 +2164,19 @@ SET search_path = public, pg_catalog
|
|
|
1994
2164
|
AS $$
|
|
1995
2165
|
INSERT INTO cerefox_audit_log (
|
|
1996
2166
|
document_id, version_id, operation, author, author_type,
|
|
1997
|
-
size_before, size_after, description,
|
|
2167
|
+
size_before, size_after, description, metadata_before, metadata_after,
|
|
2168
|
+
created_at
|
|
1998
2169
|
)
|
|
1999
2170
|
VALUES (
|
|
2000
2171
|
p_document_id, p_version_id, p_operation, p_author,
|
|
2001
2172
|
CASE WHEN p_author_type IN ('user', 'agent') THEN p_author_type ELSE 'user' END,
|
|
2002
|
-
p_size_before, p_size_after, p_description,
|
|
2173
|
+
p_size_before, p_size_after, p_description, p_metadata_before, p_metadata_after,
|
|
2003
2174
|
-- clock_timestamp(), not the NOW() default: NOW() is the TRANSACTION's
|
|
2004
2175
|
-- start time, so every audit entry written by one cerefox_edit batch
|
|
2005
2176
|
-- would share a timestamp and the order of operations inside the batch
|
|
2006
2177
|
-- would be unrecoverable from the trail (iter-33). clock_timestamp()
|
|
2007
2178
|
-- advances within a transaction, so entries stay orderable. Outside a
|
|
2008
|
-
-- batch this is indistinguishable from the old
|
|
2179
|
+
-- batch this is indistinguishable from the old behavior.
|
|
2009
2180
|
clock_timestamp()
|
|
2010
2181
|
)
|
|
2011
2182
|
RETURNING id AS audit_id, cerefox_audit_log.created_at;
|
|
@@ -2043,7 +2214,10 @@ RETURNS TABLE (
|
|
|
2043
2214
|
size_before INT,
|
|
2044
2215
|
size_after INT,
|
|
2045
2216
|
description TEXT,
|
|
2046
|
-
created_at TIMESTAMPTZ
|
|
2217
|
+
created_at TIMESTAMPTZ,
|
|
2218
|
+
-- #333 (0.21.0): set on 'update-metadata' entries, NULL otherwise.
|
|
2219
|
+
metadata_before JSONB,
|
|
2220
|
+
metadata_after JSONB
|
|
2047
2221
|
)
|
|
2048
2222
|
LANGUAGE sql
|
|
2049
2223
|
SECURITY DEFINER
|
|
@@ -2061,7 +2235,9 @@ AS $$
|
|
|
2061
2235
|
a.size_before,
|
|
2062
2236
|
a.size_after,
|
|
2063
2237
|
a.description,
|
|
2064
|
-
a.created_at
|
|
2238
|
+
a.created_at,
|
|
2239
|
+
a.metadata_before,
|
|
2240
|
+
a.metadata_after
|
|
2065
2241
|
FROM cerefox_audit_log a
|
|
2066
2242
|
LEFT JOIN cerefox_documents d ON d.id = a.document_id
|
|
2067
2243
|
WHERE (p_document_id IS NULL OR a.document_id = p_document_id)
|
|
@@ -2436,7 +2612,9 @@ RETURNS TABLE (
|
|
|
2436
2612
|
-- Optimistic-concurrency token (iter-32): pass back as
|
|
2437
2613
|
-- expected_content_hash on update.
|
|
2438
2614
|
content_hash TEXT,
|
|
2439
|
-
content TEXT
|
|
2615
|
+
content TEXT,
|
|
2616
|
+
-- #333: pass back as expected_metadata_revision on a metadata write.
|
|
2617
|
+
metadata_revision INT
|
|
2440
2618
|
)
|
|
2441
2619
|
LANGUAGE plpgsql
|
|
2442
2620
|
SECURITY DEFINER
|
|
@@ -2466,6 +2644,7 @@ BEGIN
|
|
|
2466
2644
|
(SELECT COUNT(*)::INT FROM cerefox_document_versions dv
|
|
2467
2645
|
WHERE dv.document_id = d.id) AS version_count,
|
|
2468
2646
|
d.content_hash,
|
|
2647
|
+
d.metadata_revision,
|
|
2469
2648
|
CASE WHEN p_include_content THEN
|
|
2470
2649
|
(SELECT CASE WHEN MAX(c.content_format) >= 2 THEN STRING_AGG(c.content, '' ORDER BY c.chunk_index) ELSE STRING_AGG(c.content, E'\n\n' ORDER BY c.chunk_index) END
|
|
2471
2650
|
FROM cerefox_chunks c
|
|
@@ -2506,6 +2685,7 @@ BEGIN
|
|
|
2506
2685
|
version_count := v_row.version_count;
|
|
2507
2686
|
content_hash := v_row.content_hash;
|
|
2508
2687
|
content := v_row.content;
|
|
2688
|
+
metadata_revision := v_row.metadata_revision;
|
|
2509
2689
|
RETURN NEXT;
|
|
2510
2690
|
END LOOP;
|
|
2511
2691
|
END;
|
|
@@ -2516,7 +2696,7 @@ $$;
|
|
|
2516
2696
|
-- docs/research/document-relations-and-semantic-graph.md
|
|
2517
2697
|
--
|
|
2518
2698
|
-- Type dictionary: rel_type is free text (any string is accepted and returned),
|
|
2519
|
-
-- but a few KNOWN types carry
|
|
2699
|
+
-- but a few KNOWN types carry behavior. Keeping the dictionary in one place
|
|
2520
2700
|
-- here — rather than scattered CASE expressions — is what lets the set/delete
|
|
2521
2701
|
-- RPCs stay symmetric with each other.
|
|
2522
2702
|
-- symmetric both directions are written/removed together
|
|
@@ -3117,19 +3297,29 @@ $$;
|
|
|
3117
3297
|
--
|
|
3118
3298
|
-- Metadata-only: no re-chunk, no re-embed, no content version snapshot. The
|
|
3119
3299
|
-- audit trail records it as 'update-metadata', which the CHECK already allows.
|
|
3300
|
+
-- #333 (0.21.0): a concurrency token for metadata. `metadata_revision` goes up
|
|
3301
|
+
-- by one on every actual change (trigger). Merge writes MAY pass
|
|
3302
|
+
-- p_expected_metadata_revision (strict when passed); a replace MUST pass it, or
|
|
3303
|
+
-- p_last_write_wins, because a replace deletes keys. A merge of disjoint keys
|
|
3304
|
+
-- was already safe, and requiring a token there would force a read before
|
|
3305
|
+
-- every tag. The audit entry now records the metadata before and after.
|
|
3120
3306
|
DROP FUNCTION IF EXISTS cerefox_set_document_metadata(UUID, JSONB, BOOLEAN, TEXT, TEXT);
|
|
3307
|
+
DROP FUNCTION IF EXISTS cerefox_set_document_metadata(UUID, JSONB, BOOLEAN, TEXT, TEXT, INT, BOOLEAN);
|
|
3121
3308
|
CREATE FUNCTION cerefox_set_document_metadata(
|
|
3122
3309
|
p_document_id UUID,
|
|
3123
3310
|
p_metadata JSONB,
|
|
3124
3311
|
p_replace BOOLEAN DEFAULT FALSE,
|
|
3125
3312
|
p_author TEXT DEFAULT 'unknown',
|
|
3126
|
-
p_author_type TEXT DEFAULT 'user'
|
|
3313
|
+
p_author_type TEXT DEFAULT 'user',
|
|
3314
|
+
p_expected_metadata_revision INT DEFAULT NULL,
|
|
3315
|
+
p_last_write_wins BOOLEAN DEFAULT FALSE
|
|
3127
3316
|
)
|
|
3128
3317
|
RETURNS TABLE (
|
|
3129
3318
|
document_id UUID,
|
|
3130
3319
|
metadata JSONB,
|
|
3131
3320
|
keys_set INT,
|
|
3132
|
-
keys_removed INT
|
|
3321
|
+
keys_removed INT,
|
|
3322
|
+
metadata_revision INT
|
|
3133
3323
|
)
|
|
3134
3324
|
LANGUAGE plpgsql
|
|
3135
3325
|
SECURITY DEFINER
|
|
@@ -3137,11 +3327,12 @@ SET search_path = public, pg_catalog
|
|
|
3137
3327
|
AS $$
|
|
3138
3328
|
DECLARE
|
|
3139
3329
|
v_before JSONB;
|
|
3330
|
+
v_stored JSONB;
|
|
3140
3331
|
v_after JSONB;
|
|
3141
|
-
v_null_keys TEXT[];
|
|
3142
3332
|
v_title TEXT;
|
|
3143
3333
|
v_removed INT;
|
|
3144
3334
|
v_set INT;
|
|
3335
|
+
v_revision INT;
|
|
3145
3336
|
BEGIN
|
|
3146
3337
|
IF p_metadata IS NULL OR jsonb_typeof(p_metadata) <> 'object' THEN
|
|
3147
3338
|
RAISE EXCEPTION 'metadata must be a JSON object'
|
|
@@ -3149,8 +3340,8 @@ BEGIN
|
|
|
3149
3340
|
END IF;
|
|
3150
3341
|
|
|
3151
3342
|
-- Lock the row so a concurrent metadata write cannot interleave between
|
|
3152
|
-
-- the read and the merge.
|
|
3153
|
-
SELECT d.metadata, d.title INTO
|
|
3343
|
+
-- the read, the token check and the merge.
|
|
3344
|
+
SELECT d.metadata, d.title, d.metadata_revision INTO v_stored, v_title, v_revision
|
|
3154
3345
|
FROM cerefox_documents d
|
|
3155
3346
|
WHERE d.id = p_document_id AND d.deleted_at IS NULL
|
|
3156
3347
|
FOR UPDATE;
|
|
@@ -3160,17 +3351,21 @@ BEGIN
|
|
|
3160
3351
|
USING ERRCODE = 'P0002';
|
|
3161
3352
|
END IF;
|
|
3162
3353
|
|
|
3354
|
+
PERFORM cerefox_check_metadata_revision(
|
|
3355
|
+
p_document_id, v_revision, p_expected_metadata_revision,
|
|
3356
|
+
p_replace, p_last_write_wins);
|
|
3357
|
+
|
|
3163
3358
|
-- #212: a legacy row can hold NON-OBJECT metadata (the ingest RPC did not
|
|
3164
3359
|
-- validate its input until 0.12.2). Merging onto it with || would produce
|
|
3165
3360
|
-- an ARRAY — Postgres treats both sides as arrays — burying the stored
|
|
3166
3361
|
-- value one level deeper and leaving the row still corrupt. Only replace
|
|
3167
3362
|
-- actually repairs such a row, so refuse the merge and say so. jsonb
|
|
3168
3363
|
-- 'null' is treated like SQL NULL (empty), matching the CLI.
|
|
3169
|
-
IF NOT p_replace AND
|
|
3170
|
-
AND jsonb_typeof(
|
|
3364
|
+
IF NOT p_replace AND v_stored IS NOT NULL
|
|
3365
|
+
AND jsonb_typeof(v_stored) NOT IN ('object', 'null') THEN
|
|
3171
3366
|
RAISE EXCEPTION
|
|
3172
3367
|
'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.',
|
|
3173
|
-
p_document_id, jsonb_typeof(
|
|
3368
|
+
p_document_id, jsonb_typeof(v_stored)
|
|
3174
3369
|
USING ERRCODE = '22023';
|
|
3175
3370
|
END IF;
|
|
3176
3371
|
|
|
@@ -3180,35 +3375,27 @@ BEGIN
|
|
|
3180
3375
|
-- errors, which would roll back the repair itself (round-5 review,
|
|
3181
3376
|
-- verified live).
|
|
3182
3377
|
v_before := CASE
|
|
3183
|
-
WHEN
|
|
3184
|
-
ELSE
|
|
3378
|
+
WHEN v_stored IS NULL OR jsonb_typeof(v_stored) <> 'object' THEN '{}'::jsonb
|
|
3379
|
+
ELSE v_stored
|
|
3185
3380
|
END;
|
|
3186
3381
|
|
|
3187
|
-
|
|
3188
|
-
SELECT COALESCE(array_agg(key), ARRAY[]::TEXT[]) INTO v_null_keys
|
|
3189
|
-
FROM jsonb_each(p_metadata)
|
|
3190
|
-
WHERE value = 'null'::jsonb;
|
|
3382
|
+
v_after := cerefox_metadata_apply(v_before, p_metadata, p_replace);
|
|
3191
3383
|
|
|
3192
|
-
|
|
3193
|
-
v_after := p_metadata - v_null_keys;
|
|
3194
|
-
ELSE
|
|
3195
|
-
v_after := (COALESCE(v_before, '{}'::jsonb) || p_metadata) - v_null_keys;
|
|
3196
|
-
END IF;
|
|
3197
|
-
|
|
3198
|
-
UPDATE cerefox_documents
|
|
3384
|
+
UPDATE cerefox_documents d
|
|
3199
3385
|
SET metadata = v_after, updated_at = NOW()
|
|
3200
|
-
WHERE id = p_document_id
|
|
3386
|
+
WHERE d.id = p_document_id
|
|
3387
|
+
RETURNING d.metadata_revision INTO v_revision;
|
|
3201
3388
|
|
|
3202
3389
|
-- Report what actually changed, not what was asked for: a caller that sets
|
|
3203
3390
|
-- a key to the value it already had should see 0, so "nothing happened" is
|
|
3204
3391
|
-- distinguishable from "it worked".
|
|
3205
3392
|
SELECT count(*)::INT INTO v_removed
|
|
3206
|
-
FROM jsonb_object_keys(
|
|
3393
|
+
FROM jsonb_object_keys(v_before) k
|
|
3207
3394
|
WHERE NOT v_after ? k;
|
|
3208
3395
|
|
|
3209
3396
|
SELECT count(*)::INT INTO v_set
|
|
3210
3397
|
FROM jsonb_each(v_after) e
|
|
3211
|
-
WHERE
|
|
3398
|
+
WHERE v_before -> e.key IS DISTINCT FROM e.value;
|
|
3212
3399
|
|
|
3213
3400
|
PERFORM cerefox_create_audit_entry(
|
|
3214
3401
|
p_document_id := p_document_id,
|
|
@@ -3216,13 +3403,18 @@ BEGIN
|
|
|
3216
3403
|
p_author := p_author,
|
|
3217
3404
|
p_author_type := p_author_type,
|
|
3218
3405
|
p_description := format(
|
|
3219
|
-
'Metadata %s on "%s": %s key(s) set, %s removed',
|
|
3406
|
+
'Metadata %s on "%s": %s key(s) set, %s removed%s',
|
|
3220
3407
|
CASE WHEN p_replace THEN 'replaced' ELSE 'merged' END,
|
|
3221
|
-
v_title, v_set, v_removed
|
|
3222
|
-
|
|
3408
|
+
v_title, v_set, v_removed,
|
|
3409
|
+
CASE WHEN p_replace AND p_last_write_wins
|
|
3410
|
+
AND p_expected_metadata_revision IS NULL
|
|
3411
|
+
THEN ' [last-write-wins]' ELSE '' END
|
|
3412
|
+
),
|
|
3413
|
+
p_metadata_before := v_stored,
|
|
3414
|
+
p_metadata_after := v_after
|
|
3223
3415
|
);
|
|
3224
3416
|
|
|
3225
|
-
RETURN QUERY SELECT p_document_id, v_after, v_set, v_removed;
|
|
3417
|
+
RETURN QUERY SELECT p_document_id, v_after, v_set, v_removed, v_revision;
|
|
3226
3418
|
END;
|
|
3227
3419
|
$$;
|
|
3228
3420
|
|
|
@@ -3235,6 +3427,16 @@ SET search_path = public, pg_catalog
|
|
|
3235
3427
|
AS $$
|
|
3236
3428
|
-- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
|
|
3237
3429
|
-- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
|
|
3430
|
+
-- 0.21.1: a create that loses the duplicate race raises
|
|
3431
|
+
-- CEREFOX_DUPLICATE_CONTENT (23505) instead of returning 'skipped'.
|
|
3432
|
+
-- 0.21.0: migration 0036 adds metadata_revision (+ trigger) and audit
|
|
3433
|
+
-- metadata_before/after; the metadata RPC and ingest RPC check the token,
|
|
3434
|
+
-- content updates merge metadata instead of replacing it (#333).
|
|
3435
|
+
-- 0.20.0: migration 0035 drops the store-wide UNIQUE on content_hash; the
|
|
3436
|
+
-- duplicate check moves into cerefox_ingest_document's create path, live
|
|
3437
|
+
-- documents only, returning operation 'skipped' (#332).
|
|
3438
|
+
-- 0.19.0: migration 0034 drops the unused HNSW vector indexes (search has
|
|
3439
|
+
-- scanned exactly since 0.18.1). No RPC behavior change.
|
|
3238
3440
|
-- 0.18.2: min_term_coverage built-in default 0.66 (two of three terms).
|
|
3239
3441
|
-- 0.18.0's 0.67 required all three for a three-term query. RPC-only.
|
|
3240
3442
|
-- 0.18.1: exact vector candidates. Hybrid and semantic search ordered by
|
|
@@ -3243,7 +3445,7 @@ AS $$
|
|
|
3243
3445
|
-- they now order by the computed similarity (exact scan). Keyword matches
|
|
3244
3446
|
-- carry their exact cosine into fusion. RPC-only.
|
|
3245
3447
|
-- 0.18.0 (iteration 48): search calibration, RPC-only. Bounded keyword
|
|
3246
|
-
-- score in hybrid fusion (ts_rank_cd
|
|
3448
|
+
-- score in hybrid fusion (ts_rank_cd normalization 32); min_term_coverage
|
|
3247
3449
|
-- default 0.67; built-in min_search_score follows the store's embedder
|
|
3248
3450
|
-- (new cerefox_default_min_search_score). docs/specs/search-calibration.md.
|
|
3249
3451
|
-- 0.17.0 (#251): trash auto-purge. New cerefox_purge_expired_trash, called
|
|
@@ -3287,7 +3489,7 @@ AS $$
|
|
|
3287
3489
|
-- 0.11.0 supersedes 0.10.6 (v1.2.1, #191): this branch carries that fix plus
|
|
3288
3490
|
-- the partial-edit surface, and both migrations (0019, 0020) are in the
|
|
3289
3491
|
-- sequence, so a store deploying this gets everything from both lines.
|
|
3290
|
-
SELECT '0.
|
|
3492
|
+
SELECT '0.21.1'::TEXT;
|
|
3291
3493
|
$$;
|
|
3292
3494
|
|
|
3293
3495
|
-- ── cerefox_find_dead_links ──────────────────────────────────────────────────
|