@cerefox/memory 1.12.1 → 1.13.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.
Files changed (39) hide show
  1. package/AGENT_GUIDE.md +26 -26
  2. package/AGENT_QUICK_REFERENCE.md +9 -9
  3. package/dist/bin/cerefox.js +7031 -7433
  4. package/dist/frontend/assets/index-InRztcXr.js +121 -0
  5. package/dist/frontend/assets/index-InRztcXr.js.map +1 -0
  6. package/dist/frontend/index.html +1 -1
  7. package/dist/server-assets/_shared/ef-meta/index.ts +3 -3
  8. package/dist/server-assets/_shared/mcp-tools/audit-log.ts +10 -8
  9. package/dist/server-assets/_shared/mcp-tools/delete-document.ts +4 -10
  10. package/dist/server-assets/_shared/mcp-tools/feature-flags.ts +42 -23
  11. package/dist/server-assets/_shared/mcp-tools/get-document.ts +3 -6
  12. package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +3 -3
  13. package/dist/server-assets/_shared/mcp-tools/get-help.ts +3 -6
  14. package/dist/server-assets/_shared/mcp-tools/identity.ts +48 -0
  15. package/dist/server-assets/_shared/mcp-tools/ingest.ts +5 -10
  16. package/dist/server-assets/_shared/mcp-tools/list-metadata-keys.ts +3 -6
  17. package/dist/server-assets/_shared/mcp-tools/list-projects.ts +3 -6
  18. package/dist/server-assets/_shared/mcp-tools/list-versions.ts +3 -6
  19. package/dist/server-assets/_shared/mcp-tools/metadata-search.ts +9 -7
  20. package/dist/server-assets/_shared/mcp-tools/partial-edits.ts +7 -12
  21. package/dist/server-assets/_shared/mcp-tools/relations.ts +11 -12
  22. package/dist/server-assets/_shared/mcp-tools/restore-document.ts +4 -10
  23. package/dist/server-assets/_shared/mcp-tools/search.ts +3 -6
  24. package/dist/server-assets/_shared/mcp-tools/set-document-metadata.ts +4 -10
  25. package/dist/server-assets/_shared/mcp-tools/set-document-projects.ts +3 -6
  26. package/dist/server-assets/db/migrations/0031_review_workflow_toggle.sql +74 -0
  27. package/dist/server-assets/db/rpcs.sql +53 -8
  28. package/dist/server-assets/db/schema.sql +13 -2
  29. package/dist/server-assets/supabase/functions/cerefox-ingest/index.ts +2 -4
  30. package/dist/server-assets/supabase/functions/cerefox-mcp/index.ts +6 -6
  31. package/dist/server-assets/supabase/functions/cerefox-metadata-search/index.ts +12 -2
  32. package/docs/guides/access-paths.md +1 -1
  33. package/docs/guides/cli.md +21 -14
  34. package/docs/guides/configuration.md +75 -18
  35. package/docs/guides/connect-agents.md +10 -6
  36. package/docs/guides/upgrading.md +32 -1
  37. package/package.json +1 -1
  38. package/dist/frontend/assets/index-P1F2Ldl9.js +0 -121
  39. package/dist/frontend/assets/index-P1F2Ldl9.js.map +0 -1
@@ -8,6 +8,7 @@ import type { MCPSupabaseClient } from "./types.ts";
8
8
 
9
9
  import { logUsage } from "./_utils.ts";
10
10
  import { McpInvalidParams, type ToolContext, type ToolDefinition } from "./types.ts";
11
+ import { AUTHOR_PARAM_READ, callerIdentity } from "./identity.ts";
11
12
 
12
13
  /**
13
14
  * Version timestamps carried only a DATE (`slice(0, 10)`), which is
@@ -46,7 +47,7 @@ async function handler(
46
47
  logUsage(supabase, {
47
48
  operation: "list_versions",
48
49
  accessPath: ctx.accessPath,
49
- requestor: args.requestor as string | undefined,
50
+ requestor: callerIdentity(args),
50
51
  document_id,
51
52
  result_count: versions.length,
52
53
  });
@@ -79,11 +80,7 @@ export const listVersionsTool: ToolDefinition = {
79
80
  type: "string",
80
81
  description: "UUID of the document whose version history to list",
81
82
  },
82
- requestor: {
83
- type: "string",
84
- description:
85
- 'Name of the agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent" if not provided. May be enforced via server config.',
86
- },
83
+ author: AUTHOR_PARAM_READ,
87
84
  },
88
85
  },
89
86
  handler,
@@ -9,7 +9,9 @@ import type { MCPSupabaseClient } from "./types.ts";
9
9
 
10
10
  import { applyByteBudget, getMaxResponseBytes, logUsage } from "./_utils.ts";
11
11
  import { lookupProjectId } from "./_projects.ts";
12
+ import { reviewWorkflowEnabled } from "./feature-flags.ts";
12
13
  import { McpInvalidParams, type ToolContext, type ToolDefinition } from "./types.ts";
14
+ import { AUTHOR_PARAM_READ, callerIdentity } from "./identity.ts";
13
15
 
14
16
  async function handler(
15
17
  supabase: MCPSupabaseClient,
@@ -85,7 +87,7 @@ async function handler(
85
87
  logUsage(supabase, {
86
88
  operation: "metadata_search",
87
89
  accessPath: ctx.accessPath,
88
- requestor: args.requestor as string | undefined,
90
+ requestor: callerIdentity(args),
89
91
  query_text: JSON.stringify(metadata_filter ?? {}),
90
92
  project_id: projectId,
91
93
  result_count: rows.length,
@@ -93,6 +95,10 @@ async function handler(
93
95
 
94
96
  if (rows.length === 0) return "No documents match the given criteria.";
95
97
 
98
+ // The review status is a column of a feature that may be off (#241); when
99
+ // it is, an agent should not see "approved" and wonder what it means.
100
+ const showReview = await reviewWorkflowEnabled(supabase);
101
+
96
102
  // Note: when include_content is true the RPC already respects p_max_bytes
97
103
  // server-side. The applyByteBudget helper is retained here only for
98
104
  // parity with the EF implementation and as a defensive trim — see the
@@ -109,7 +115,7 @@ async function handler(
109
115
  const hash = row.content_hash ? `\nhash: ${row.content_hash}` : "";
110
116
  const header =
111
117
  `## ${row.title} [id: ${row.document_id}]\n` +
112
- `${meta}${projects} | ${row.total_chars} chars | ${row.review_status} | updated ${row.updated_at?.slice(0, 10) ?? "?"}${hash}`;
118
+ `${meta}${projects} | ${row.total_chars} chars | ${showReview ? `${row.review_status} | ` : ""}updated ${row.updated_at?.slice(0, 10) ?? "?"}${hash}`;
113
119
 
114
120
  if (include_content && row.content) {
115
121
  return `${header}\n\n${row.content}`;
@@ -159,11 +165,7 @@ export const metadataSearchTool: ToolDefinition = {
159
165
  description:
160
166
  "Soft cap on total response bytes when include_content is true. Defaults to server maximum (200000).",
161
167
  },
162
- requestor: {
163
- type: "string",
164
- description:
165
- 'Name of the agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent" if not provided. May be enforced via server config.',
166
- },
168
+ author: AUTHOR_PARAM_READ,
167
169
  },
168
170
  },
169
171
  handler,
@@ -38,6 +38,7 @@ import {
38
38
  import { activeEmbedderName, embedBatch, resolveEmbedderKind } from "../embeddings/index.ts";
39
39
  import { extractConflictHashes, isMissingFunctionError, logUsage } from "./_utils.ts";
40
40
  import { McpInvalidParams, type MCPSupabaseClient, type ToolContext, type ToolDefinition } from "./types.ts";
41
+ import { AUTHOR_PARAM_WRITE, DEFAULT_IDENTITY, callerIdentity } from "./identity.ts";
41
42
 
42
43
  /**
43
44
  * Who to record as the author. Derived from the access path rather than taken
@@ -307,8 +308,8 @@ async function applyAndWrite(
307
308
  p_source: null,
308
309
  p_content_hash: newHash,
309
310
  p_metadata: null, // null = keep existing metadata
310
- // Agent writes land in review; a human at the CLI is the reviewer.
311
- p_review_status: authorType === "agent" ? "pending_review" : "approved",
311
+ // review_status: decided by the RPC from p_author_type + the store's
312
+ // review_workflow_enabled flag (#241), not here.
312
313
  p_chunks: chunkData,
313
314
  p_author: requestor,
314
315
  // Who actually made the write, not which module executed it. The CLI is a
@@ -440,7 +441,7 @@ async function insertHandler(
440
441
  documentId,
441
442
  operations,
442
443
  expectedHash,
443
- requestor: (args.requestor as string | undefined) ?? defaultRequestor(ctx),
444
+ requestor: callerIdentity(args) ?? defaultRequestor(ctx),
444
445
  toolLabel: "insert",
445
446
  authorType: resolveAuthorType(ctx, args),
446
447
  });
@@ -493,10 +494,7 @@ export const insertTool: ToolDefinition = {
493
494
  description:
494
495
  "content_hash of the version you are basing this on. Required — no last-write-wins.",
495
496
  },
496
- requestor: {
497
- type: "string",
498
- description: 'Agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent".',
499
- },
497
+ author: AUTHOR_PARAM_WRITE,
500
498
  author_type: {
501
499
  type: "string",
502
500
  enum: ["user", "agent"],
@@ -538,7 +536,7 @@ async function editHandler(
538
536
  documentId,
539
537
  operations,
540
538
  expectedHash,
541
- requestor: (args.requestor as string | undefined) ?? defaultRequestor(ctx),
539
+ requestor: callerIdentity(args) ?? defaultRequestor(ctx),
542
540
  toolLabel: "edit",
543
541
  authorType: resolveAuthorType(ctx, args),
544
542
  });
@@ -618,10 +616,7 @@ export const editTool: ToolDefinition = {
618
616
  description:
619
617
  "content_hash of the version you are basing these edits on. Required — no last-write-wins.",
620
618
  },
621
- requestor: {
622
- type: "string",
623
- description: 'Agent or user making this request. Recorded in the usage log. Defaults to "mcp-agent".',
624
- },
619
+ author: AUTHOR_PARAM_WRITE,
625
620
  author_type: {
626
621
  type: "string",
627
622
  enum: ["user", "agent"],
@@ -15,6 +15,7 @@
15
15
  import { logUsage } from "./_utils.ts";
16
16
  import { McpInvalidParams, type ToolContext, type ToolDefinition } from "./types.ts";
17
17
  import type { MCPSupabaseClient } from "./types.ts";
18
+ import { AUTHOR_PARAM_READ, AUTHOR_PARAM_WRITE, DEFAULT_IDENTITY, callerIdentity } from "./identity.ts";
18
19
 
19
20
  /** Relation types that carry behaviour; any other string is accepted too. */
20
21
  const KNOWN_TYPES =
@@ -44,7 +45,7 @@ async function setHandler(
44
45
  p_source_id: source,
45
46
  p_target_id: target,
46
47
  p_rel_type: relType,
47
- p_author: (args.author as string | undefined) ?? "mcp-agent",
48
+ p_author: callerIdentity(args) ?? DEFAULT_IDENTITY,
48
49
  p_author_type: "agent",
49
50
  p_metadata: (args.metadata as Record<string, unknown> | undefined) ?? {},
50
51
  });
@@ -57,7 +58,7 @@ async function setHandler(
57
58
  logUsage(supabase, {
58
59
  operation: "set_relation",
59
60
  accessPath: ctx.accessPath,
60
- requestor: args.requestor as string | undefined,
61
+ requestor: callerIdentity(args),
61
62
  document_id: source,
62
63
  });
63
64
 
@@ -103,8 +104,7 @@ export const setRelationTool: ToolDefinition = {
103
104
  type: "object",
104
105
  description: "Optional JSON context for the edge (note, confidence, …)",
105
106
  },
106
- author: { type: "string", description: "Who is creating this relation" },
107
- requestor: { type: "string", description: "Name of the agent making this request" },
107
+ author: AUTHOR_PARAM_WRITE,
108
108
  },
109
109
  },
110
110
  handler: setHandler,
@@ -126,7 +126,7 @@ async function deleteHandler(
126
126
  p_source_id: source,
127
127
  p_target_id: target,
128
128
  p_rel_type: relType,
129
- p_author: (args.author as string | undefined) ?? "mcp-agent",
129
+ p_author: callerIdentity(args) ?? DEFAULT_IDENTITY,
130
130
  p_author_type: "agent",
131
131
  });
132
132
  if (error) throw new Error(`RPC error: ${error.message}`);
@@ -135,7 +135,7 @@ async function deleteHandler(
135
135
  logUsage(supabase, {
136
136
  operation: "delete_relation",
137
137
  accessPath: ctx.accessPath,
138
- requestor: args.requestor as string | undefined,
138
+ requestor: callerIdentity(args),
139
139
  document_id: source,
140
140
  });
141
141
 
@@ -166,8 +166,7 @@ export const deleteRelationTool: ToolDefinition = {
166
166
  source_id: { type: "string", description: "UUID of the source document" },
167
167
  target_id: { type: "string", description: "UUID of the target document" },
168
168
  rel_type: { type: "string", description: "Relation type to remove" },
169
- author: { type: "string", description: "Who is removing this relation" },
170
- requestor: { type: "string", description: "Name of the agent making this request" },
169
+ author: AUTHOR_PARAM_WRITE,
171
170
  },
172
171
  },
173
172
  handler: deleteHandler,
@@ -197,7 +196,7 @@ async function getRelationsHandler(
197
196
  logUsage(supabase, {
198
197
  operation: "get_relations",
199
198
  accessPath: ctx.accessPath,
200
- requestor: args.requestor as string | undefined,
199
+ requestor: callerIdentity(args),
201
200
  document_id: docId,
202
201
  result_count: rows.length,
203
202
  });
@@ -231,7 +230,7 @@ export const getRelationsTool: ToolDefinition = {
231
230
  required: ["document_id"],
232
231
  properties: {
233
232
  document_id: { type: "string", description: "UUID of the document" },
234
- requestor: { type: "string", description: "Name of the agent making this request" },
233
+ author: AUTHOR_PARAM_READ,
235
234
  },
236
235
  },
237
236
  handler: getRelationsHandler,
@@ -268,7 +267,7 @@ async function getNeighborsHandler(
268
267
  logUsage(supabase, {
269
268
  operation: "get_neighbors",
270
269
  accessPath: ctx.accessPath,
271
- requestor: args.requestor as string | undefined,
270
+ requestor: callerIdentity(args),
272
271
  document_id: docId,
273
272
  result_count: rows.length,
274
273
  });
@@ -307,7 +306,7 @@ export const getNeighborsTool: ToolDefinition = {
307
306
  from_time: { type: "string", description: "ISO-8601: only neighbours created on/after" },
308
307
  to_time: { type: "string", description: "ISO-8601: only neighbours created on/before" },
309
308
  limit: { type: "integer", description: "Max documents to return (default 50, max 200)" },
310
- requestor: { type: "string", description: "Name of the agent making this request" },
309
+ author: AUTHOR_PARAM_READ,
311
310
  },
312
311
  },
313
312
  handler: getNeighborsHandler,
@@ -21,6 +21,7 @@ import type { MCPSupabaseClient } from "./types.ts";
21
21
 
22
22
  import { isDocumentNotFoundError, isMissingFunctionError, logUsage } from "./_utils.ts";
23
23
  import { McpInvalidParams, type ToolContext, type ToolDefinition } from "./types.ts";
24
+ import { AUTHOR_PARAM_WRITE, DEFAULT_IDENTITY, callerIdentity } from "./identity.ts";
24
25
 
25
26
  async function handler(
26
27
  supabase: MCPSupabaseClient,
@@ -32,7 +33,7 @@ async function handler(
32
33
 
33
34
  if (!document_id) throw new McpInvalidParams("document_id is required");
34
35
 
35
- const author = (args.author as string | undefined) ?? (args.requestor as string | undefined);
36
+ const author = callerIdentity(args);
36
37
  // Derived from the transport, never taken from the caller: an agent must not
37
38
  // be able to record itself as a user. Matches the delete handler.
38
39
  const authorType = ctx.accessPath === "cli" ? "user" : "agent";
@@ -81,7 +82,7 @@ async function handler(
81
82
  logUsage(supabase, {
82
83
  operation: "restore",
83
84
  accessPath: ctx.accessPath,
84
- requestor: args.requestor as string | undefined,
85
+ requestor: author,
85
86
  document_id,
86
87
  result_count: 1,
87
88
  });
@@ -116,14 +117,7 @@ export const restoreDocumentTool: ToolDefinition = {
116
117
  description:
117
118
  "Why this document is being restored. Recorded in the audit-log entry. Short and specific beats long.",
118
119
  },
119
- author: {
120
- type: "string",
121
- description: "Who is making this change. Recorded in the audit log.",
122
- },
123
- requestor: {
124
- type: "string",
125
- description: "Name of the agent or user making this request. Recorded in the usage log.",
126
- },
120
+ author: AUTHOR_PARAM_WRITE,
127
121
  },
128
122
  },
129
123
  handler,
@@ -22,6 +22,7 @@ import { applyByteBudget, getConfiguredMinSearchScore, getConfiguredSearchAlpha,
22
22
  getMaxResponseBytes, getMinTermCoverage, logUsage } from "./_utils.ts";
23
23
  import { lookupProjectId } from "./_projects.ts";
24
24
  import { McpInvalidParams, type ToolContext, type ToolDefinition } from "./types.ts";
25
+ import { AUTHOR_PARAM_READ, callerIdentity } from "./identity.ts";
25
26
 
26
27
  async function handler(
27
28
  supabase: MCPSupabaseClient,
@@ -135,7 +136,7 @@ async function handler(
135
136
  logUsage(supabase, {
136
137
  operation: "search",
137
138
  accessPath: ctx.accessPath,
138
- requestor: args.requestor as string | undefined,
139
+ requestor: callerIdentity(args),
139
140
  query_text: query,
140
141
  project_id: projectId,
141
142
  result_count: accepted.length,
@@ -244,11 +245,7 @@ export const searchTool: ToolDefinition = {
244
245
  description:
245
246
  "Optional response size budget in bytes. Results are dropped whole until the budget is satisfied; a truncated flag is set when results are dropped. Defaults to the server maximum (200000). Pass a smaller value if your context window is limited. Values above the server maximum are silently capped.",
246
247
  },
247
- requestor: {
248
- type: "string",
249
- description:
250
- 'Name of the agent or user making this request (e.g., "Claude Code", "archiver"). Recorded in the usage log for attribution. Defaults to "mcp-agent" if not provided. May be enforced via server config.',
251
- },
248
+ author: AUTHOR_PARAM_READ,
252
249
  },
253
250
  },
254
251
  handler,
@@ -21,6 +21,7 @@ import type { MCPSupabaseClient } from "./types.ts";
21
21
 
22
22
  import { logUsage } from "./_utils.ts";
23
23
  import { McpInvalidParams, type ToolContext, type ToolDefinition } from "./types.ts";
24
+ import { AUTHOR_PARAM_WRITE, DEFAULT_IDENTITY, callerIdentity } from "./identity.ts";
24
25
 
25
26
  async function handler(
26
27
  supabase: MCPSupabaseClient,
@@ -48,7 +49,7 @@ async function handler(
48
49
  );
49
50
  }
50
51
 
51
- const author = (args.author as string | undefined) ?? (args.requestor as string | undefined);
52
+ const author = callerIdentity(args);
52
53
  // Derived from the transport, never taken from the caller: an agent must not
53
54
  // be able to record itself as a user. Matches the partial-edit handlers.
54
55
  const authorType = ctx.accessPath === "cli" ? "user" : "agent";
@@ -71,7 +72,7 @@ async function handler(
71
72
  logUsage(supabase, {
72
73
  operation: "update_metadata",
73
74
  accessPath: ctx.accessPath,
74
- requestor: args.requestor as string | undefined,
75
+ requestor: author,
75
76
  document_id,
76
77
  result_count: 1,
77
78
  });
@@ -119,14 +120,7 @@ export const setDocumentMetadataTool: ToolDefinition = {
119
120
  description:
120
121
  "Set the metadata to EXACTLY this object, discarding any key not listed. Defaults to false (merge). Use only when you mean to reset a document's tags wholesale.",
121
122
  },
122
- author: {
123
- type: "string",
124
- description: "Who is making this change. Recorded in the audit log.",
125
- },
126
- requestor: {
127
- type: "string",
128
- description: "Name of the agent or user making this request. Recorded in the usage log.",
129
- },
123
+ author: AUTHOR_PARAM_WRITE,
130
124
  },
131
125
  },
132
126
  handler,
@@ -13,6 +13,7 @@ import type { MCPSupabaseClient } from "./types.ts";
13
13
 
14
14
  import { replaceDocumentProjects } from "./_projects.ts";
15
15
  import { McpInvalidParams, type ToolContext, type ToolDefinition } from "./types.ts";
16
+ import { AUTHOR_PARAM_WRITE, DEFAULT_IDENTITY, callerIdentity } from "./identity.ts";
16
17
 
17
18
  async function handler(
18
19
  supabase: MCPSupabaseClient,
@@ -21,7 +22,7 @@ async function handler(
21
22
  ): Promise<string> {
22
23
  const document_id = (args.document_id as string | undefined)?.trim();
23
24
  const project_names_raw = args.project_names;
24
- const author = (args.author as string | undefined) ?? "mcp-agent";
25
+ const author = callerIdentity(args) ?? DEFAULT_IDENTITY;
25
26
 
26
27
  if (!document_id) {
27
28
  throw new McpInvalidParams(
@@ -96,11 +97,7 @@ export const setDocumentProjectsTool: ToolDefinition = {
96
97
  description:
97
98
  "Explicit list of project names. Each created if absent. Order is preserved. Empty list = remove from all projects.",
98
99
  },
99
- author: {
100
- type: "string",
101
- description:
102
- 'Agent or tool name recorded in the audit log. Defaults to "mcp-agent". May be enforced via server config.',
103
- },
100
+ author: AUTHOR_PARAM_WRITE,
104
101
  },
105
102
  },
106
103
  handler,
@@ -0,0 +1,74 @@
1
+ -- 0031_review_workflow_toggle.sql — make the review workflow optional (#241,
2
+ -- schema 0.16.0, iteration 44).
3
+ --
4
+ -- New config key `review_workflow_enabled`. Fresh installs get 'false' from
5
+ -- schema.sql; this migration runs only on stores that predate the flag and
6
+ -- seeds 'true' there, so upgrading never changes what a store does. Neither
7
+ -- write ever overrides a value an operator has set (ON CONFLICT DO NOTHING).
8
+ --
9
+ -- The decision "agent write → pending_review" moves out of the six client call
10
+ -- sites and into cerefox_ingest_document. That RPC lives in rpcs.sql, which
11
+ -- `cerefox server deploy` re-applies. (0.16.0 also had the RPC read this flag
12
+ -- on write and store 'approved' for everyone while off; 0.16.1 removed that —
13
+ -- the flag governs visibility only, the stored value follows author_type.)
14
+ --
15
+ -- #240: cerefox_hybrid_search / cerefox_search_docs gain p_review_status so a
16
+ -- filtered search is applied before the limit, not after. A new argument is a
17
+ -- new overload; the old ones must go or PostgREST calls become ambiguous
18
+ -- (PGRST203). Same DROPs sit at the top of rpcs.sql for the fresh path.
19
+ --
20
+ -- Idempotent: safe to re-run.
21
+
22
+ INSERT INTO cerefox_config (key, value)
23
+ VALUES ('review_workflow_enabled', 'true')
24
+ ON CONFLICT (key) DO NOTHING;
25
+
26
+ DROP FUNCTION IF EXISTS cerefox_hybrid_search(TEXT, VECTOR(768), INT, FLOAT, BOOLEAN, UUID, FLOAT, JSONB, FLOAT);
27
+ DROP FUNCTION IF EXISTS cerefox_search_docs(TEXT, VECTOR(768), INT, FLOAT, UUID, FLOAT, INT, INT, JSONB, FLOAT);
28
+
29
+ -- The allow-list in cerefox_set_config grows by one key. Same signature, so
30
+ -- OR REPLACE is enough (no overload to drop). Carried here as well as in
31
+ -- rpcs.sql so `db_migrate` alone leaves the key settable; the unit test
32
+ -- `rpc-guard-invariants` pins the two lists to each other.
33
+ CREATE OR REPLACE FUNCTION cerefox_set_config(
34
+ p_key TEXT,
35
+ p_value TEXT,
36
+ p_author TEXT DEFAULT 'unknown',
37
+ p_author_type TEXT DEFAULT 'user'
38
+ )
39
+ RETURNS VOID
40
+ LANGUAGE plpgsql
41
+ SECURITY DEFINER
42
+ SET search_path = public, pg_catalog
43
+ AS $$
44
+ DECLARE
45
+ v_allowed TEXT[] := ARRAY[
46
+ 'usage_tracking_enabled', 'require_requestor_identity', 'requestor_identity_format',
47
+ 'min_search_score', 'min_term_coverage', 'search_alpha',
48
+ 'version_retention_hours', 'version_cleanup_enabled',
49
+ 'relations_enabled',
50
+ 'review_workflow_enabled',
51
+ 'document_size_warning_chars'
52
+ ];
53
+ v_old TEXT;
54
+ BEGIN
55
+ IF NOT (p_key = ANY(v_allowed)) THEN
56
+ RAISE EXCEPTION 'Unknown config key: %. Allowed keys: %', p_key, v_allowed;
57
+ END IF;
58
+
59
+ SELECT value INTO v_old FROM cerefox_config WHERE key = p_key;
60
+
61
+ INSERT INTO cerefox_config (key, value)
62
+ VALUES (p_key, p_value)
63
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
64
+
65
+ PERFORM cerefox_create_audit_entry(
66
+ p_operation := 'config-change',
67
+ p_author := p_author,
68
+ p_author_type := p_author_type,
69
+ p_description := 'config: ' || p_key || ': '
70
+ || COALESCE('''' || v_old || '''', '(unset)')
71
+ || ' → ''' || p_value || ''''
72
+ );
73
+ END;
74
+ $$;
@@ -76,6 +76,11 @@ DROP FUNCTION IF EXISTS cerefox_hybrid_search(TEXT, VECTOR(768), INT, FLOAT, BOO
76
76
  DROP FUNCTION IF EXISTS cerefox_fts_search(TEXT, INT, UUID, JSONB);
77
77
  DROP FUNCTION IF EXISTS cerefox_search_docs(TEXT, VECTOR(768), INT, FLOAT, UUID, FLOAT, INT, INT, JSONB);
78
78
 
79
+ -- Iteration 44 (0.16.0, #240): p_review_status added to the two search RPCs
80
+ -- so a review-status filter applies before the limit. Old overloads out.
81
+ DROP FUNCTION IF EXISTS cerefox_hybrid_search(TEXT, VECTOR(768), INT, FLOAT, BOOLEAN, UUID, FLOAT, JSONB, FLOAT);
82
+ DROP FUNCTION IF EXISTS cerefox_search_docs(TEXT, VECTOR(768), INT, FLOAT, UUID, FLOAT, INT, INT, JSONB, FLOAT);
83
+
79
84
  -- ── Shared return type note ────────────────────────────────────────────────────
80
85
  -- All chunk-level search RPCs return the same shape for consistency:
81
86
  -- chunk_id, document_id, chunk_index, title, content, heading_path,
@@ -113,7 +118,11 @@ CREATE OR REPLACE FUNCTION cerefox_hybrid_search(
113
118
  -- meant 100% of terms — the pass this gate generalizes. Chunks below the
114
119
  -- bar can still pass via the vector threshold, else they are
115
120
  -- below-confidence material. 0 restores the pre-gate OR behavior.
116
- p_min_term_coverage FLOAT DEFAULT NULL
121
+ p_min_term_coverage FLOAT DEFAULT NULL,
122
+ -- #240: 'approved' | 'pending_review' restricts the candidate pool BEFORE
123
+ -- the limit, so a filtered page is a full page. NULL = no filter. Applied
124
+ -- here, in the CTEs, rather than by the caller on the returned page.
125
+ p_review_status TEXT DEFAULT NULL
117
126
  )
118
127
  RETURNS TABLE (
119
128
  chunk_id UUID,
@@ -204,6 +213,7 @@ BEGIN
204
213
  WHERE dp.document_id = d.id AND dp.project_id = p_project_id
205
214
  ))
206
215
  AND (p_metadata_filter IS NULL OR d.metadata @> p_metadata_filter)
216
+ AND (p_review_status IS NULL OR d.review_status = p_review_status)
207
217
  ) INTO and_matches;
208
218
  END IF;
209
219
 
@@ -239,6 +249,7 @@ BEGIN
239
249
  WHERE dp.document_id = d.id AND dp.project_id = p_project_id
240
250
  ))
241
251
  AND (p_metadata_filter IS NULL OR d.metadata @> p_metadata_filter)
252
+ AND (p_review_status IS NULL OR d.review_status = p_review_status)
242
253
  ORDER BY fts_score DESC
243
254
  LIMIT candidate_count
244
255
  ),
@@ -260,6 +271,7 @@ BEGIN
260
271
  WHERE dp.document_id = d.id AND dp.project_id = p_project_id
261
272
  ))
262
273
  AND (p_metadata_filter IS NULL OR d.metadata @> p_metadata_filter)
274
+ AND (p_review_status IS NULL OR d.review_status = p_review_status)
263
275
  ORDER BY
264
276
  CASE
265
277
  WHEN p_use_upgrade AND c.embedding_upgrade IS NOT NULL
@@ -724,7 +736,9 @@ CREATE OR REPLACE FUNCTION cerefox_search_docs(
724
736
  p_metadata_filter JSONB DEFAULT NULL,
725
737
  -- NULL flows through to cerefox_hybrid_search, which resolves the
726
738
  -- caller > cerefox_config > built-in chain in one place (#133).
727
- p_min_term_coverage FLOAT DEFAULT NULL
739
+ p_min_term_coverage FLOAT DEFAULT NULL,
740
+ -- #240: optional review-status filter, applied inside cerefox_hybrid_search.
741
+ p_review_status TEXT DEFAULT NULL
728
742
  )
729
743
  RETURNS TABLE (
730
744
  document_id UUID,
@@ -765,7 +779,8 @@ AS $$
765
779
  p_project_id := p_project_id,
766
780
  p_min_score := p_min_score,
767
781
  p_metadata_filter := p_metadata_filter,
768
- p_min_term_coverage := p_min_term_coverage
782
+ p_min_term_coverage := p_min_term_coverage,
783
+ p_review_status := p_review_status
769
784
  )
770
785
  ),
771
786
  best_per_doc AS (
@@ -1379,7 +1394,12 @@ $$;
1379
1394
  -- p_metadata : JSONB metadata. NULL = "not provided" → create uses '{}',
1380
1395
  -- update keeps the existing metadata (v0.11.1). Pass '{}'
1381
1396
  -- explicitly to clear all metadata.
1382
- -- p_review_status : 'approved' or 'pending_review' (based on author_type)
1397
+ -- p_review_status : ACCEPTED AND IGNORED since 0.16.0 (#241). The status is
1398
+ -- decided here from p_author_type alone (agent →
1399
+ -- pending_review, user → approved), so every transport
1400
+ -- behaves the same. The `review_workflow_enabled` flag
1401
+ -- only governs what surfaces show (0.16.1).
1402
+ -- Kept in the signature so no caller breaks.
1383
1403
  -- p_chunks : JSONB array of chunk objects, each with:
1384
1404
  -- chunk_index, heading_path, heading_level, title,
1385
1405
  -- content, char_count, embedding (float[]), embedder (text)
@@ -1564,9 +1584,20 @@ BEGIN
1564
1584
  USING ERRCODE = '22023'; -- deterministic; never a retryable SQLSTATE
1565
1585
  END IF;
1566
1586
 
1567
- -- Validate review_status
1568
- v_status := CASE WHEN p_review_status IN ('approved', 'pending_review')
1569
- THEN p_review_status ELSE 'approved' END;
1587
+ -- Review status is decided HERE, not by the caller (#241), from the
1588
+ -- author type alone: an agent write is 'pending_review', a person's is
1589
+ -- 'approved'. The `review_workflow_enabled` flag is deliberately NOT
1590
+ -- consulted (0.16.1): it governs what the surfaces show and enforce, not
1591
+ -- what is stored, so a store that turns the workflow off and later back on
1592
+ -- sees exactly the statuses it would have had all along. 0.16.0 wrote
1593
+ -- 'approved' for every author while the flag was off; that made a stored
1594
+ -- 'approved' mean two different things depending on when it was written.
1595
+ -- p_review_status is not consulted either — six clients used to compute
1596
+ -- it and they could not have agreed on a store-level policy.
1597
+ v_status := CASE
1598
+ WHEN p_author_type = 'agent' THEN 'pending_review'
1599
+ ELSE 'approved'
1600
+ END;
1570
1601
 
1571
1602
  -- Count chunks and total chars from the input
1572
1603
  v_chunk_count := jsonb_array_length(p_chunks);
@@ -2704,6 +2735,9 @@ DECLARE
2704
2735
  'version_retention_hours', 'version_cleanup_enabled',
2705
2736
  -- Optional features, off by default (iteration 29).
2706
2737
  'relations_enabled',
2738
+ -- #241: the review workflow. Off on fresh installs, on for stores that
2739
+ -- predate the flag. Read by cerefox_ingest_document on every write.
2740
+ 'review_workflow_enabled',
2707
2741
  -- Iteration 33: flag writes that push a document past this many chars
2708
2742
  -- (0 = off). Partial edits make writes cheap, so an insert-only agent
2709
2743
  -- never assembles the document and never sees it grow past its split
@@ -3057,6 +3091,17 @@ SET search_path = public, pg_catalog
3057
3091
  AS $$
3058
3092
  -- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
3059
3093
  -- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
3094
+ -- 0.16.1 (v1.13.1): cerefox_ingest_document no longer consults
3095
+ -- `review_workflow_enabled` — review_status follows author_type whatever
3096
+ -- the flag says; the flag governs visibility/enforcement only. RPC-only,
3097
+ -- no migration (server deploy re-applies rpcs.sql).
3098
+ -- 0.16.0 (#241, #240): `review_workflow_enabled` config key (seeded false
3099
+ -- on fresh installs, true by migration 0031 on existing ones);
3100
+ -- cerefox_ingest_document decides review_status itself from author_type
3101
+ -- + the flag, p_review_status is ignored; p_review_status filter on
3102
+ -- cerefox_hybrid_search / cerefox_search_docs, applied before the limit.
3103
+ -- 0.15.0 (iteration 39): cerefox_rename_document.
3104
+ -- 0.14.0 (#147/#219): store-level writes audited in-RPC.
3060
3105
  -- 0.13.0 (#216): archived chunks carry no search artifacts —
3061
3106
  -- cerefox_snapshot_version nulls embedding_primary/embedding_upgrade/fts
3062
3107
  -- at archive time; embedding_primary becomes nullable; migration 0027
@@ -3079,7 +3124,7 @@ AS $$
3079
3124
  -- 0.11.0 supersedes 0.10.6 (v1.2.1, #191): this branch carries that fix plus
3080
3125
  -- the partial-edit surface, and both migrations (0019, 0020) are in the
3081
3126
  -- sequence, so a store deploying this gets everything from both lines.
3082
- SELECT '0.15.0'::TEXT;
3127
+ SELECT '0.16.1'::TEXT;
3083
3128
  $$;
3084
3129
 
3085
3130
  -- ── 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.15.0
8
+ -- @version: 0.16.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 —
@@ -55,7 +55,10 @@ CREATE TABLE IF NOT EXISTS cerefox_documents (
55
55
  total_chars INT NOT NULL DEFAULT 0,
56
56
  -- review_status: human governance flag. 'approved' = validated by human,
57
57
  -- 'pending_review' = modified by agent, not yet reviewed.
58
- -- Content is searchable in both states.
58
+ -- Content is searchable in both states. Written ONLY by
59
+ -- cerefox_ingest_document, from author_type alone. The store-level
60
+ -- `review_workflow_enabled` flag (#241) governs whether any surface shows
61
+ -- or enforces the column; it never changes what is stored (0.16.1).
59
62
  review_status TEXT NOT NULL DEFAULT 'approved',
60
63
  -- lifecycle_status: where this document stands relative to the graph —
61
64
  -- 'active' | 'superseded' | 'stale' | 'archived'. Distinct from
@@ -402,6 +405,14 @@ ON CONFLICT (key) DO NOTHING;
402
405
  INSERT INTO cerefox_config (key, value)
403
406
  VALUES ('relations_enabled', 'false')
404
407
  ON CONFLICT (key) DO NOTHING;
408
+ -- The review workflow (agent writes land pending_review, a person approves)
409
+ -- is OFF on a fresh install (#241): most stores have no reviewer, and a queue
410
+ -- nobody drains is noise. Migration 0031 seeds TRUE on stores that predate the
411
+ -- flag, so an upgrade never changes behaviour. Only this seed and that
412
+ -- migration ever write the value; the toggle itself is `cerefox config set`.
413
+ INSERT INTO cerefox_config (key, value)
414
+ VALUES ('review_workflow_enabled', 'false')
415
+ ON CONFLICT (key) DO NOTHING;
405
416
 
406
417
 
407
418
  -- ── Usage log ────────────────────────────────────────────────────────────────