@cerefox/memory 1.8.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -598,63 +598,18 @@ AS $$
598
598
  GROUP BY d.id, d.title, d.source, d.metadata;
599
599
  $$;
600
600
 
601
- -- ── cerefox_save_note ─────────────────────────────────────────────────────────
602
- -- Agent write tool: create a minimal document record for a short text note.
603
- -- Embedding and chunking are NOT done server-side in V1 the Python ingestion
604
- -- pipeline should be used for full ingest. This RPC is intended for quick
605
- -- one-shot note capture from AI agents that want to store something immediately.
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
- -- Parameters:
608
- -- p_title : Note title (required)
609
- -- p_content : Markdown content (required)
610
- -- p_source : Origin label, e.g. 'agent' (default: 'agent')
611
- -- p_project_id : Optional project UUID (assigns to a single project)
612
- -- p_metadata : Optional JSONB metadata (e.g. agent name, session id)
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,
@@ -2020,6 +1975,216 @@ AS $$
2020
1975
  ORDER BY p.name;
2021
1976
  $$;
2022
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
+ -- array_append, NOT `||`: with an untyped string literal on the right,
2122
+ -- `TEXT[] || 'literal'` resolves to the array||array overload and dies
2123
+ -- with `malformed array literal` — found LIVE on staging (v1.9.0; every
2124
+ -- description-only edit failed; the rename branch only survived because
2125
+ -- its parenthesized concatenation is typed TEXT).
2126
+ IF v_new.name <> v_old.name THEN
2127
+ v_changes := array_append(v_changes, 'renamed ''' || v_old.name || ''' → ''' || v_new.name || '''');
2128
+ END IF;
2129
+ IF COALESCE(v_new.description, '') <> COALESCE(v_old.description, '') THEN
2130
+ v_changes := array_append(v_changes, 'description changed');
2131
+ END IF;
2132
+
2133
+ PERFORM cerefox_create_audit_entry(
2134
+ p_operation := 'project-edit',
2135
+ p_author := p_author,
2136
+ p_author_type := p_author_type,
2137
+ p_description := 'Project ''' || v_new.name || ''' edited ('
2138
+ || COALESCE(NULLIF(array_to_string(v_changes, '; '), ''), 'no-op') || ')'
2139
+ );
2140
+ RETURN QUERY SELECT v_new.id, v_new.name, v_new.description,
2141
+ v_new.created_at, v_new.updated_at;
2142
+ END;
2143
+ $$;
2144
+
2145
+ CREATE OR REPLACE FUNCTION cerefox_delete_project(
2146
+ p_project_id UUID,
2147
+ p_author TEXT DEFAULT 'unknown',
2148
+ p_author_type TEXT DEFAULT 'user'
2149
+ )
2150
+ RETURNS TABLE (deleted BOOLEAN, project_name TEXT)
2151
+ LANGUAGE plpgsql
2152
+ SECURITY DEFINER
2153
+ SET search_path = public, pg_catalog
2154
+ AS $$
2155
+ DECLARE
2156
+ v_name TEXT;
2157
+ v_links INT;
2158
+ BEGIN
2159
+ -- Lock + read name and link count in one pass: the memberships CASCADE
2160
+ -- with the row, so the count must be read pre-DELETE — but the zero-row
2161
+ -- path (repeat delete) pays for nothing (round 4).
2162
+ SELECT p.name, (SELECT COUNT(*) FROM cerefox_document_projects dp
2163
+ WHERE dp.project_id = p.id)
2164
+ INTO v_name, v_links
2165
+ FROM cerefox_projects p WHERE p.id = p_project_id FOR UPDATE;
2166
+
2167
+ IF v_name IS NULL THEN
2168
+ -- Zero rows: nothing happened, so nothing is audited — the trail
2169
+ -- must never assert an event that did not occur. Callers decide
2170
+ -- whether "already gone" is an error (CLI) or a 404 (web).
2171
+ RETURN QUERY SELECT FALSE, NULL::TEXT;
2172
+ RETURN;
2173
+ END IF;
2174
+
2175
+ DELETE FROM cerefox_projects WHERE id = p_project_id;
2176
+
2177
+ PERFORM cerefox_create_audit_entry(
2178
+ p_operation := 'project-delete',
2179
+ p_author := p_author,
2180
+ p_author_type := p_author_type,
2181
+ p_description := 'Project ''' || v_name || ''' deleted ('
2182
+ || v_links || ' document link(s) removed)'
2183
+ );
2184
+ RETURN QUERY SELECT TRUE, v_name;
2185
+ END;
2186
+ $$;
2187
+
2023
2188
  -- ── cerefox_metadata_search ──────────────────────────────────────────────────
2024
2189
  -- Query documents by metadata key-value criteria without a text search term.
2025
2190
  -- Uses JSONB containment (@>) which leverages the existing GIN index on
@@ -2442,7 +2607,23 @@ BEGIN
2442
2607
  END;
2443
2608
  $$;
2444
2609
 
2445
- CREATE OR REPLACE FUNCTION cerefox_set_config(p_key TEXT, p_value TEXT)
2610
+ -- The 2-arg signature grew to 4 in 0.14.0 (audit trail). CREATE OR REPLACE
2611
+ -- never removes the old overload, and a surviving one makes named calls
2612
+ -- ambiguous under PostgREST (PGRST203) — the v1.7.0 purge/restore lesson.
2613
+ -- Both signatures dropped (house pattern, cf. delete_document): the old one
2614
+ -- so it cannot linger, the current one so re-applying this file stays
2615
+ -- idempotent after migration 0028 has already created it.
2616
+ DROP FUNCTION IF EXISTS cerefox_set_config(TEXT, TEXT);
2617
+ DROP FUNCTION IF EXISTS cerefox_set_config(TEXT, TEXT, TEXT, TEXT);
2618
+
2619
+ CREATE FUNCTION cerefox_set_config(
2620
+ p_key TEXT,
2621
+ p_value TEXT,
2622
+ -- 0.14.0: config changes are governance decisions ("who turned retention
2623
+ -- off, and when?") and belong in the audit trail like any other write.
2624
+ p_author TEXT DEFAULT 'unknown',
2625
+ p_author_type TEXT DEFAULT 'user'
2626
+ )
2446
2627
  RETURNS VOID
2447
2628
  LANGUAGE plpgsql
2448
2629
  SECURITY DEFINER
@@ -2467,14 +2648,30 @@ DECLARE
2467
2648
  -- point. A signal in the write's response, never a refusal.
2468
2649
  'document_size_warning_chars'
2469
2650
  ];
2651
+ v_old TEXT;
2470
2652
  BEGIN
2471
2653
  IF NOT (p_key = ANY(v_allowed)) THEN
2472
2654
  RAISE EXCEPTION 'Unknown config key: %. Allowed keys: %', p_key, v_allowed;
2473
2655
  END IF;
2474
2656
 
2657
+ SELECT value INTO v_old FROM cerefox_config WHERE key = p_key;
2658
+
2475
2659
  INSERT INTO cerefox_config (key, value)
2476
2660
  VALUES (p_key, p_value)
2477
2661
  ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
2662
+
2663
+ -- Same-transaction audit entry (document_id NULL — a store-level write).
2664
+ -- A no-op set (same value) is still recorded: "checked and confirmed" is
2665
+ -- itself a governance action, and skipping it would make the trail lie by
2666
+ -- omission when someone re-asserts a setting.
2667
+ PERFORM cerefox_create_audit_entry(
2668
+ p_operation := 'config-change',
2669
+ p_author := p_author,
2670
+ p_author_type := p_author_type,
2671
+ p_description := 'config: ' || p_key || ': '
2672
+ || COALESCE('''' || v_old || '''', '(unset)')
2673
+ || ' → ''' || p_value || ''''
2674
+ );
2478
2675
  END;
2479
2676
  $$;
2480
2677
 
@@ -2820,7 +3017,7 @@ AS $$
2820
3017
  -- 0.11.0 supersedes 0.10.6 (v1.2.1, #191): this branch carries that fix plus
2821
3018
  -- the partial-edit surface, and both migrations (0019, 0020) are in the
2822
3019
  -- sequence, so a store deploying this gets everything from both lines.
2823
- SELECT '0.13.0'::TEXT;
3020
+ SELECT '0.14.1'::TEXT;
2824
3021
  $$;
2825
3022
 
2826
3023
  -- ── 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.13.0
8
+ -- @version: 0.14.1
9
9
  -- The `@version` marker above is read by the schema-version-mismatch banner
10
10
  -- (see /api/v1/schema-version). Bump it whenever schema.sql OR rpcs.sql
11
11
  -- changes in a way that requires `cerefox server deploy` to be re-run —
@@ -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
  );
@@ -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
- // deno-lint-ignore no-explicit-any
224
- async function ensureDocumentInProject(
225
- // deno-lint-ignore no-explicit-any
226
- supabase: any,
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
@@ -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 [create\|update-content\|update-metadata\|delete\|status-change\|archive\|unarchive\|restore]` | choice | _none_ | Filter by operation type. |
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
  ---
@@ -638,7 +638,7 @@ In the action editor, paste this schema (replace `<your-project-ref>`):
638
638
  openapi: 3.1.0
639
639
  info:
640
640
  title: Cerefox Knowledge Base
641
- version: 3.2.0
641
+ version: 3.3.0
642
642
  servers:
643
643
  - url: https://<your-project-ref>.supabase.co/functions/v1
644
644
  paths:
@@ -934,7 +934,9 @@ paths:
934
934
  type: string
935
935
  description: >
936
936
  Filter by operation type: create, update-content, update-metadata,
937
- delete, status-change, archive, unarchive (optional)
937
+ insert, replace-section, delete-section, rename-section, delete,
938
+ restore, status-change, archive, unarchive, config-change,
939
+ project-create, project-edit, project-delete (optional)
938
940
  since:
939
941
  type: string
940
942
  description: ISO timestamp lower bound for temporal queries (optional)
@@ -1490,23 +1492,6 @@ neighbours** (±`p_window_size` chunks within the same document).
1490
1492
  Returns: `chunk_id`, `document_id`, `chunk_index`, `title`, `content`, `heading_path`,
1491
1493
  `heading_level`, `doc_title`, `is_seed` (TRUE for the original seed chunks)
1492
1494
 
1493
- #### `cerefox_save_note`
1494
-
1495
- Create a document record directly. The note is stored but **not embedded** — use `cerefox-ingest`
1496
- Edge Function instead for notes that need to be immediately searchable.
1497
-
1498
- | Parameter | Type | Default | Description |
1499
- |-----------|------|---------|-------------|
1500
- | `p_title` | TEXT | required | Note title |
1501
- | `p_content` | TEXT | required | Markdown content |
1502
- | `p_source` | TEXT | `'agent'` | Origin label |
1503
- | `p_project_id` | UUID | null | Project to assign |
1504
- | `p_metadata` | JSONB | `{}` | Metadata (agent name, tags, etc.) |
1505
-
1506
- Returns: `id`, `title`, `created_at`
1507
-
1508
- ---
1509
-
1510
1495
  ### Metadata RPCs
1511
1496
 
1512
1497
  #### `cerefox_list_metadata_keys`
@@ -80,9 +80,9 @@ bun scripts/db_status.ts # human-readable report
80
80
  bun scripts/db_status.ts --json # structured JSON output
81
81
  ```
82
82
 
83
- Reports:
84
- - Tables: `cerefox_documents`, `cerefox_chunks`, `cerefox_document_versions`, `cerefox_projects`, `cerefox_document_projects`, `cerefox_audit_log`, `cerefox_migrations`
85
- - RPC functions: hybrid_search, fts_search, semantic_search, reconstruct_doc, save_note, search_docs, context_expand, snapshot_version, get_document, list_document_versions, ingest_document, delete_document, create_audit_entry, list_audit_entries, list_metadata_keys, update_chunk_fts, **`cerefox_schema_version`** (new in v0.3.0), **`cerefox_pg_function_exists`** (new in v0.3.0)
83
+ Reports (the expected lists live in `_shared/db-status/index.ts` — the single source of truth):
84
+ - Tables: `cerefox_projects`, `cerefox_documents`, `cerefox_document_versions`, `cerefox_audit_log`, `cerefox_document_projects`, `cerefox_chunks`, `cerefox_migrations`, `cerefox_usage_log`, `cerefox_config`, `cerefox_document_relations`
85
+ - RPC functions: set_updated_at, hybrid_search, fts_search, semantic_search, reconstruct_doc, list_projects, log_usage, list_metadata_keys, snapshot_version, get_document, list_document_versions, create_audit_entry, list_audit_entries, ingest_document, delete_document, restore_document, purge_document, set_config, create_project, update_project, delete_project, set_document_metadata, find_dead_links, metadata_health, metadata_search, update_chunk_fts, `cerefox_schema_version`, `cerefox_pg_function_exists`
86
86
  - Row counts per table
87
87
  - **Schema-version mismatch**: compares the `@version` marker in the bundled `schema.sql` against the deployed `cerefox_schema_version()` RPC. Non-zero exit if they differ (the same check powers the web UI's schema-mismatch banner).
88
88
 
@@ -114,7 +114,7 @@ bun scripts/db_migrate.ts [OPTIONS]
114
114
 
115
115
  On a freshly deployed database, `db_migrate.ts` is always a no-op — `db_deploy.ts` has already stamped all existing migrations.
116
116
 
117
- Migration files live in `src/cerefox/db/migrations/` and are applied in filename order (`0001_...`, `0002_...`). Each file is applied exactly once; applied filenames are recorded in the `cerefox_migrations` table.
117
+ Migration files live in `src/cerefox/db/migrations/` and are applied in filename order (`0001_...`, `0002_...`). Each file is applied exactly once; applied filenames are recorded in the `cerefox_migrations` table. Recent migrations (0023–0027) include data migrations that print what they did (0027 reports reclaimed space); see [`docs/guides/upgrading.md`](upgrading.md).
118
118
 
119
119
  Always run a backup before migrating:
120
120
 
@@ -136,7 +136,7 @@ a linked Supabase project.
136
136
 
137
137
  ## Notable cross-version transitions
138
138
 
139
- Most upgrades need nothing beyond the steps above. Two transitions are worth
139
+ Most upgrades need nothing beyond the steps above. A few transitions are worth
140
140
  knowing about:
141
141
 
142
142
  - **v0.9 — CLI verbs moved to a resource-verb shape** (`cerefox document get`,
@@ -149,6 +149,10 @@ knowing about:
149
149
  `@cerefox/memory` npm package (CLI, MCP server, web server, ingestion). If
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
+ - **v1.5.0 — heading-duplication refusal.** Documents with duplicate headings
153
+ that ingested fine before now need deduplication, or are refused on edit.
154
+ - **v1.7.0 — trashed documents refuse content updates.** A soft-deleted
155
+ document must be restored before its content can be updated.
152
156
 
153
157
  ## Notable: v1.8.0 storage reclaim (migration 0027)
154
158