@cerefox/memory 1.12.0 → 1.13.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.
@@ -15,7 +15,7 @@
15
15
  href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&display=swap"
16
16
  />
17
17
  <title>Cerefox</title>
18
- <script type="module" crossorigin src="/app/assets/index-P1F2Ldl9.js"></script>
18
+ <script type="module" crossorigin src="/app/assets/index-InRztcXr.js"></script>
19
19
  <link rel="stylesheet" crossorigin href="/app/assets/index-Dm_zCch4.css">
20
20
  </head>
21
21
  <body>
@@ -18,7 +18,7 @@
18
18
  * doesn't touch `supabase/functions/` leaves it alone).
19
19
  */
20
20
 
21
- export const EF_VERSION = "1.12.0";
21
+ export const EF_VERSION = "1.13.0";
22
22
 
23
23
  /**
24
24
  * The Cerefox RELEASE version — what `cerefox --version` reports and what npm
@@ -36,7 +36,7 @@ export const EF_VERSION = "1.12.0";
36
36
  * is imported by the Deno Edge Functions, which cannot reach into the npm
37
37
  * package.
38
38
  */
39
- export const CEREFOX_VERSION = "1.12.0";
39
+ export const CEREFOX_VERSION = "1.13.0";
40
40
 
41
41
  /**
42
42
  * The most recent version whose EF-side SOURCE actually changed (#127).
@@ -46,7 +46,7 @@ export const CEREFOX_VERSION = "1.12.0";
46
46
  * `cut_release.ts` ONLY when EF source changed since the last tag; doctor
47
47
  * uses it to stay silent on label-only drift.
48
48
  */
49
- export const EF_LAST_CHANGED = "1.11.0";
49
+ export const EF_LAST_CHANGED = "1.13.0";
50
50
 
51
51
  /**
52
52
  * The 8 peer EFs the cerefox-mcp aggregator probes (excludes cerefox-mcp
@@ -1,17 +1,24 @@
1
1
  /**
2
- * Optional-feature gating for the MCP tool surface.
2
+ * Optional-feature gating, read from `cerefox_config`.
3
3
  *
4
- * Document relations (iteration 29) ship **dormant**: the table sits empty, the
5
- * `lifecycle_status` column defaults to `'active'`, search is untouched — but
6
- * the four relation tools would still appear in every agent's tool list, and an
7
- * agent that sees a tool may decide to use it. For a feature we intend to
8
- * evolve through experimentation, that is not "optional" enough.
4
+ * Two features are governed by store-level boolean flags:
9
5
  *
10
- * So exposure is gated on a deployment-wide flag (`relations_enabled` in
11
- * `cerefox_config`, default **false**), read through the same RPC as every
12
- * other setting. Turning it on is one command:
6
+ * - **Document relations** (iteration 29) ship **dormant**: the table sits
7
+ * empty, the `lifecycle_status` column defaults to `'active'`, search is
8
+ * untouched but the four relation tools would still appear in every
9
+ * agent's tool list, and an agent that sees a tool may decide to use it.
10
+ * For a feature we intend to evolve through experimentation, that is not
11
+ * "optional" enough. Gated on `relations_enabled` (default **false**).
13
12
  *
14
- * cerefox config set relations_enabled true
13
+ * - **The review workflow** (#241): agent writes land `pending_review` and a
14
+ * person approves them. Gated on `review_workflow_enabled` (**false** on a
15
+ * fresh install, **true** on a store that predates the flag). The write-side
16
+ * decision lives in `cerefox_ingest_document`; this reader is for the
17
+ * presentation side — every surface that would show a `review_status` asks
18
+ * here first and omits it when the workflow is off.
19
+ *
20
+ * Both are read through the same RPC as every other setting, so turning one
21
+ * on is one command: `cerefox config set <key> true`.
15
22
  *
16
23
  * Failure mode is deliberately closed: if the config read fails (older schema,
17
24
  * transient error), the feature stays hidden rather than appearing
@@ -29,27 +36,29 @@ export const RELATION_TOOL_NAMES: ReadonlySet<string> = new Set([
29
36
  ]);
30
37
 
31
38
  /**
32
- * Cached per process. `tools/list` happens once per session, but `tools/call`
33
- * checks too (a long-lived session could hold a stale list), and a round trip
34
- * per call is not worth paying.
39
+ * Cached per process, per key. `tools/list` happens once per session, but
40
+ * `tools/call` checks too (a long-lived session could hold a stale list), and
41
+ * a round trip per call is not worth paying. The web server busts the cache
42
+ * when it writes config itself; a flip made from another process shows up
43
+ * within the TTL.
35
44
  */
36
- const CACHE_TTL_MS = 60_000;
37
- let cached: { value: boolean; at: number } | null = null;
45
+ const CACHE_TTL_MS = 15_000;
46
+ const cache = new Map<string, { value: boolean; at: number }>();
38
47
 
39
- /** Test seam: drop the cache so a flag change is picked up immediately. */
48
+ /** Test seam (and the web config route's): drop the cache so a flag change
49
+ * is picked up immediately. */
40
50
  export function resetFeatureFlagCache(): void {
41
- cached = null;
51
+ cache.clear();
42
52
  }
43
53
 
44
- export async function relationsEnabled(supabase: MCPSupabaseClient): Promise<boolean> {
45
- if (cached && Date.now() - cached.at < CACHE_TTL_MS) return cached.value;
54
+ async function readBoolFlag(supabase: MCPSupabaseClient, key: string): Promise<boolean> {
55
+ const hit = cache.get(key);
56
+ if (hit && Date.now() - hit.at < CACHE_TTL_MS) return hit.value;
46
57
  try {
47
- const { data, error } = await supabase.rpc("cerefox_get_config", {
48
- p_key: "relations_enabled",
49
- });
58
+ const { data, error } = await supabase.rpc("cerefox_get_config", { p_key: key });
50
59
  if (error) throw new Error(error.message);
51
60
  const value = String(data ?? "").trim().toLowerCase() === "true";
52
- cached = { value, at: Date.now() };
61
+ cache.set(key, { value, at: Date.now() });
53
62
  return value;
54
63
  } catch {
55
64
  // Fail closed, and don't cache a failure — a transient error shouldn't
@@ -58,6 +67,15 @@ export async function relationsEnabled(supabase: MCPSupabaseClient): Promise<boo
58
67
  }
59
68
  }
60
69
 
70
+ export async function relationsEnabled(supabase: MCPSupabaseClient): Promise<boolean> {
71
+ return readBoolFlag(supabase, "relations_enabled");
72
+ }
73
+
74
+ /** Whether `review_status` is a thing on this store (#241). */
75
+ export async function reviewWorkflowEnabled(supabase: MCPSupabaseClient): Promise<boolean> {
76
+ return readBoolFlag(supabase, "review_workflow_enabled");
77
+ }
78
+
61
79
  /** Message shown when a gated tool is called while the feature is off. */
62
80
  export function disabledToolMessage(name: string): string {
63
81
  return (
@@ -136,7 +136,8 @@ async function handler(
136
136
  }
137
137
 
138
138
  const contentHash = await sha256hex(normalizeContent(content));
139
- const reviewStatus = author_type === "agent" ? "pending_review" : "approved";
139
+ // review_status is decided by cerefox_ingest_document from author_type and
140
+ // the store's review_workflow_enabled flag (#241); clients no longer send it.
140
141
 
141
142
  // ── ID-based update path ─────────────────────────────────────────────────
142
143
  if (document_id) {
@@ -202,7 +203,6 @@ async function handler(
202
203
  p_source: source,
203
204
  p_content_hash: contentHash,
204
205
  p_metadata: metadata,
205
- p_review_status: reviewStatus,
206
206
  p_chunks: chunkData,
207
207
  p_author: author,
208
208
  p_author_type: author_type,
@@ -306,7 +306,6 @@ async function handler(
306
306
  p_source: source,
307
307
  p_content_hash: contentHash,
308
308
  p_metadata: metadata,
309
- p_review_status: reviewStatus,
310
309
  p_chunks: chunkData,
311
310
  p_author: author,
312
311
  p_author_type: author_type,
@@ -384,7 +383,6 @@ async function handler(
384
383
  p_source: source,
385
384
  p_content_hash: contentHash,
386
385
  p_metadata: metadata,
387
- p_review_status: reviewStatus,
388
386
  p_chunks: chunkData,
389
387
  p_author: author,
390
388
  p_author_type: author_type,
@@ -9,6 +9,7 @@ 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";
13
14
 
14
15
  async function handler(
@@ -93,6 +94,10 @@ async function handler(
93
94
 
94
95
  if (rows.length === 0) return "No documents match the given criteria.";
95
96
 
97
+ // The review status is a column of a feature that may be off (#241); when
98
+ // it is, an agent should not see "approved" and wonder what it means.
99
+ const showReview = await reviewWorkflowEnabled(supabase);
100
+
96
101
  // Note: when include_content is true the RPC already respects p_max_bytes
97
102
  // server-side. The applyByteBudget helper is retained here only for
98
103
  // parity with the EF implementation and as a defensive trim — see the
@@ -109,7 +114,7 @@ async function handler(
109
114
  const hash = row.content_hash ? `\nhash: ${row.content_hash}` : "";
110
115
  const header =
111
116
  `## ${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}`;
117
+ `${meta}${projects} | ${row.total_chars} chars | ${showReview ? `${row.review_status} | ` : ""}updated ${row.updated_at?.slice(0, 10) ?? "?"}${hash}`;
113
118
 
114
119
  if (include_content && row.content) {
115
120
  return `${header}\n\n${row.content}`;
@@ -307,8 +307,8 @@ async function applyAndWrite(
307
307
  p_source: null,
308
308
  p_content_hash: newHash,
309
309
  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",
310
+ // review_status: decided by the RPC from p_author_type + the store's
311
+ // review_workflow_enabled flag (#241), not here.
312
312
  p_chunks: chunkData,
313
313
  p_author: requestor,
314
314
  // Who actually made the write, not which module executed it. The CLI is a
@@ -0,0 +1,72 @@
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, which reads this flag. That RPC
11
+ -- lives in rpcs.sql, which `cerefox server deploy` re-applies.
12
+ --
13
+ -- #240: cerefox_hybrid_search / cerefox_search_docs gain p_review_status so a
14
+ -- filtered search is applied before the limit, not after. A new argument is a
15
+ -- new overload; the old ones must go or PostgREST calls become ambiguous
16
+ -- (PGRST203). Same DROPs sit at the top of rpcs.sql for the fresh path.
17
+ --
18
+ -- Idempotent: safe to re-run.
19
+
20
+ INSERT INTO cerefox_config (key, value)
21
+ VALUES ('review_workflow_enabled', 'true')
22
+ ON CONFLICT (key) DO NOTHING;
23
+
24
+ DROP FUNCTION IF EXISTS cerefox_hybrid_search(TEXT, VECTOR(768), INT, FLOAT, BOOLEAN, UUID, FLOAT, JSONB, FLOAT);
25
+ DROP FUNCTION IF EXISTS cerefox_search_docs(TEXT, VECTOR(768), INT, FLOAT, UUID, FLOAT, INT, INT, JSONB, FLOAT);
26
+
27
+ -- The allow-list in cerefox_set_config grows by one key. Same signature, so
28
+ -- OR REPLACE is enough (no overload to drop). Carried here as well as in
29
+ -- rpcs.sql so `db_migrate` alone leaves the key settable; the unit test
30
+ -- `rpc-guard-invariants` pins the two lists to each other.
31
+ CREATE OR REPLACE FUNCTION cerefox_set_config(
32
+ p_key TEXT,
33
+ p_value TEXT,
34
+ p_author TEXT DEFAULT 'unknown',
35
+ p_author_type TEXT DEFAULT 'user'
36
+ )
37
+ RETURNS VOID
38
+ LANGUAGE plpgsql
39
+ SECURITY DEFINER
40
+ SET search_path = public, pg_catalog
41
+ AS $$
42
+ DECLARE
43
+ v_allowed TEXT[] := ARRAY[
44
+ 'usage_tracking_enabled', 'require_requestor_identity', 'requestor_identity_format',
45
+ 'min_search_score', 'min_term_coverage', 'search_alpha',
46
+ 'version_retention_hours', 'version_cleanup_enabled',
47
+ 'relations_enabled',
48
+ 'review_workflow_enabled',
49
+ 'document_size_warning_chars'
50
+ ];
51
+ v_old TEXT;
52
+ BEGIN
53
+ IF NOT (p_key = ANY(v_allowed)) THEN
54
+ RAISE EXCEPTION 'Unknown config key: %. Allowed keys: %', p_key, v_allowed;
55
+ END IF;
56
+
57
+ SELECT value INTO v_old FROM cerefox_config WHERE key = p_key;
58
+
59
+ INSERT INTO cerefox_config (key, value)
60
+ VALUES (p_key, p_value)
61
+ ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
62
+
63
+ PERFORM cerefox_create_audit_entry(
64
+ p_operation := 'config-change',
65
+ p_author := p_author,
66
+ p_author_type := p_author_type,
67
+ p_description := 'config: ' || p_key || ': '
68
+ || COALESCE('''' || v_old || '''', '(unset)')
69
+ || ' → ''' || p_value || ''''
70
+ );
71
+ END;
72
+ $$;
@@ -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,11 @@ $$;
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 and the store's
1399
+ -- `review_workflow_enabled` flag, so every transport
1400
+ -- behaves the same and a toggle needs no client change.
1401
+ -- Kept in the signature so no caller breaks.
1383
1402
  -- p_chunks : JSONB array of chunk objects, each with:
1384
1403
  -- chunk_index, heading_path, heading_level, title,
1385
1404
  -- content, char_count, embedding (float[]), embedder (text)
@@ -1564,9 +1583,18 @@ BEGIN
1564
1583
  USING ERRCODE = '22023'; -- deterministic; never a retryable SQLSTATE
1565
1584
  END IF;
1566
1585
 
1567
- -- Validate review_status
1568
- v_status := CASE WHEN p_review_status IN ('approved', 'pending_review')
1569
- THEN p_review_status ELSE 'approved' END;
1586
+ -- Review status is decided HERE, not by the caller (#241). With the
1587
+ -- workflow on, an agent write is queued for a person to look at; with it
1588
+ -- off, every write lands approved and no surface shows the column. The
1589
+ -- fallback FALSE matches the fresh-install seed; migration 0031 seeds TRUE
1590
+ -- on stores that predate the flag, so it only applies if the row is gone.
1591
+ -- p_review_status is deliberately not consulted — six clients used to
1592
+ -- compute it and they could not have agreed on a store-level policy.
1593
+ v_status := CASE
1594
+ WHEN NOT cerefox_config_bool('review_workflow_enabled', FALSE) THEN 'approved'
1595
+ WHEN p_author_type = 'agent' THEN 'pending_review'
1596
+ ELSE 'approved'
1597
+ END;
1570
1598
 
1571
1599
  -- Count chunks and total chars from the input
1572
1600
  v_chunk_count := jsonb_array_length(p_chunks);
@@ -2704,6 +2732,9 @@ DECLARE
2704
2732
  'version_retention_hours', 'version_cleanup_enabled',
2705
2733
  -- Optional features, off by default (iteration 29).
2706
2734
  'relations_enabled',
2735
+ -- #241: the review workflow. Off on fresh installs, on for stores that
2736
+ -- predate the flag. Read by cerefox_ingest_document on every write.
2737
+ 'review_workflow_enabled',
2707
2738
  -- Iteration 33: flag writes that push a document past this many chars
2708
2739
  -- (0 = off). Partial edits make writes cheap, so an insert-only agent
2709
2740
  -- never assembles the document and never sees it grow past its split
@@ -3057,6 +3088,13 @@ SET search_path = public, pg_catalog
3057
3088
  AS $$
3058
3089
  -- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
3059
3090
  -- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
3091
+ -- 0.16.0 (#241, #240): `review_workflow_enabled` config key (seeded false
3092
+ -- on fresh installs, true by migration 0031 on existing ones);
3093
+ -- cerefox_ingest_document decides review_status itself from author_type
3094
+ -- + the flag, p_review_status is ignored; p_review_status filter on
3095
+ -- cerefox_hybrid_search / cerefox_search_docs, applied before the limit.
3096
+ -- 0.15.0 (iteration 39): cerefox_rename_document.
3097
+ -- 0.14.0 (#147/#219): store-level writes audited in-RPC.
3060
3098
  -- 0.13.0 (#216): archived chunks carry no search artifacts —
3061
3099
  -- cerefox_snapshot_version nulls embedding_primary/embedding_upgrade/fts
3062
3100
  -- at archive time; embedding_primary becomes nullable; migration 0027
@@ -3079,7 +3117,7 @@ AS $$
3079
3117
  -- 0.11.0 supersedes 0.10.6 (v1.2.1, #191): this branch carries that fix plus
3080
3118
  -- the partial-edit surface, and both migrations (0019, 0020) are in the
3081
3119
  -- sequence, so a store deploying this gets everything from both lines.
3082
- SELECT '0.15.0'::TEXT;
3120
+ SELECT '0.16.0'::TEXT;
3083
3121
  $$;
3084
3122
 
3085
3123
  -- ── 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.0
9
9
  -- The `@version` marker above is read by the schema-version-mismatch banner
10
10
  -- (see /api/v1/schema-version). Bump it whenever schema.sql OR rpcs.sql
11
11
  -- changes in a way that requires `cerefox server deploy` to be re-run —
@@ -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, which consults `review_workflow_enabled`
60
+ -- (#241): with the workflow off every write lands 'approved' and every
61
+ -- surface hides the column; existing values are left as they are.
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 ────────────────────────────────────────────────────────────────
@@ -344,7 +344,8 @@ Deno.serve(async (req: Request) => {
344
344
 
345
345
  const contentHash = await sha256hex(normalizeContent(content));
346
346
  const headers = { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" };
347
- const reviewStatus = author_type === "agent" ? "pending_review" : "approved";
347
+ // review_status is decided by cerefox_ingest_document from author_type and
348
+ // the store's review_workflow_enabled flag (#241); the EF no longer sends it.
348
349
 
349
350
  // ── ID-based update path ────────────────────────────────────────────────────
350
351
  // When document_id is provided, update that exact document regardless of
@@ -424,7 +425,6 @@ Deno.serve(async (req: Request) => {
424
425
  p_source: source,
425
426
  p_content_hash: contentHash,
426
427
  p_metadata: metadata,
427
- p_review_status: reviewStatus,
428
428
  p_chunks: chunkData,
429
429
  p_author: author,
430
430
  p_author_type: author_type,
@@ -543,7 +543,6 @@ Deno.serve(async (req: Request) => {
543
543
  p_source: source,
544
544
  p_content_hash: contentHash,
545
545
  p_metadata: metadata,
546
- p_review_status: reviewStatus,
547
546
  p_chunks: chunkData,
548
547
  p_author: author,
549
548
  p_author_type: author_type,
@@ -655,7 +654,6 @@ Deno.serve(async (req: Request) => {
655
654
  p_source: source,
656
655
  p_content_hash: contentHash,
657
656
  p_metadata: metadata,
658
- p_review_status: reviewStatus,
659
657
  p_chunks: chunkData,
660
658
  p_author: author,
661
659
  p_author_type: author_type,
@@ -2,6 +2,7 @@ import "jsr:@supabase/functions-js/edge-runtime.d.ts";
2
2
  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
+ import { reviewWorkflowEnabled } from "../../../_shared/mcp-tools/feature-flags.ts";
5
6
 
6
7
  /**
7
8
  * cerefox-metadata-search -- Supabase Edge Function
@@ -27,7 +28,9 @@ import { efAuthGate } from "../../../_shared/ef-auth/index.ts";
27
28
  * include_content boolean optional Include full text (default: false)
28
29
  * max_bytes number optional Byte budget when include_content=true
29
30
  *
30
- * Response (200): Array of matching documents
31
+ * Response (200): Array of matching documents. `review_status` is present
32
+ * only while the review workflow is on (#241); with the flag
33
+ * off the key is absent, as on every other surface.
31
34
  * Response (400): { error: "..." }
32
35
  */
33
36
 
@@ -160,7 +163,14 @@ Deno.serve(async (req: Request): Promise<Response> => {
160
163
  p_project_id: project_id,
161
164
  })).catch(() => {});
162
165
 
163
- return new Response(JSON.stringify(data ?? []), {
166
+ // Presentation only: the same shared reader every other surface uses.
167
+ const rows = (data ?? []) as Array<Record<string, unknown>>;
168
+ const showReview = await reviewWorkflowEnabled(supabase);
169
+ const out = showReview
170
+ ? rows
171
+ : rows.map(({ review_status: _hidden, ...rest }) => rest);
172
+
173
+ return new Response(JSON.stringify(out), {
164
174
  status: 200,
165
175
  headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
166
176
  });
@@ -265,7 +265,7 @@ before "completing" the parity table by adding purge to agent-facing access path
265
265
  | Tier | Operations | Reversible? | Where exposed |
266
266
  |---|---|---|---|
267
267
  | 1. Reads + soft mutations | search, get, list-*, ingest (create/update), metadata-search, get-audit-log | n/a (reads) / yes (versioned) | All paths — MCP, Edge Functions, CLI, web UI |
268
- | 2. Soft-destructive + recovery | `delete_document` (soft delete to trash), `restore_document` (un-trash), `set_review_status` | yes — delete is restorable; restore recovers | CLI (`cerefox document delete` / `restore`), web UI, and — since v1.7.0 (#208, #210) — MCP (`cerefox_delete_document`, which requires the caller's read-hash, and `cerefox_restore_document`). **Not** the primitive GPT-Actions Edge Functions (deliberately deferred). |
268
+ | 2. Soft-destructive + recovery | `delete_document` (soft delete to trash), `restore_document` (un-trash), `set_review_status` (web only; a `404` while `review_workflow_enabled` is off) | yes — delete is restorable; restore recovers | CLI (`cerefox document delete` / `restore`), web UI, and — since v1.7.0 (#208, #210) — MCP (`cerefox_delete_document`, which requires the caller's read-hash, and `cerefox_restore_document`). **Not** the primitive GPT-Actions Edge Functions (deliberately deferred). |
269
269
  | 3. **Hard-destructive** | `purge_document` (permanent), `set_version_archived` (toggle version retention) | no (purge) | **Web UI only** |
270
270
 
271
271
  ### Why purge is web-UI-only
@@ -50,7 +50,7 @@ cerefox document ingest --paste --title "<title>" [OPTIONS] # stdin
50
50
  | `--last-write-wins` | — | flag | off | Skip the concurrency check and overwrite regardless of concurrent changes. For re-sync flows where an external source of truth makes conflicts meaningless. Recorded in the audit log. |
51
51
  | `--source` | — | str | `paste` / `file` | Source label recorded on the document. |
52
52
  | `--author` | — | str | `CEREFOX_AUTHOR_NAME` or `unknown` | Audit-log author identity. |
53
- | `--author-type` | — | `user`\|`agent` | `CEREFOX_AUTHOR_TYPE` or `user` | Caller type. Agent writes auto-routed to `pending_review`. |
53
+ | `--author-type` | — | `user`\|`agent` | `CEREFOX_AUTHOR_TYPE` or `user` | Caller type. Agent writes land `pending_review` while the review workflow is on (`review_workflow_enabled`); `approved` otherwise. |
54
54
 
55
55
  **Examples**:
56
56
  ```bash
@@ -216,7 +216,7 @@ cerefox document list [OPTIONS]
216
216
  | `--deleted` | flag | off | List soft-deleted (trashed) documents instead of active ones, newest-deleted first. Pair the ids with `cerefox document restore` / `cerefox document delete`. |
217
217
  | `--json` | flag | off | Machine-readable JSON output. |
218
218
 
219
- **Output**: tabular `id | title | source | status | updated_at` listing (or `deleted_at` with `--deleted`).
219
+ **Output**: tabular `id | title | source | status | updated_at` listing (or `deleted_at` with `--deleted`). The `status` column (and the `review_status` key in `--json`) is present only while the review workflow is on — see `review_workflow_enabled` in [configuration.md](configuration.md#review-workflow).
220
220
 
221
221
  **MCP equivalent**: scope-by-project / metadata / time listing maps to [`cerefox_metadata_search`](../../AGENT_GUIDE.md) — e.g. `cerefox_metadata_search(project_name="research")` lists that project's documents (the `metadata_filter` may be empty when another scope is supplied). The `--deleted` (trash) view and unscoped whole-KB listing remain CLI-only.
222
222
 
@@ -506,6 +506,8 @@ cerefox metadata search --metadata-filter '{"type":"decision-log"}' --updated-si
506
506
  cerefox metadata search --metadata-filter '{"status":"active"}' --project-name "research" --include-content
507
507
  ```
508
508
 
509
+ **Output**: a `## title [id: …]` block per document with its metadata, projects, size, review status and update date (plus content with `--include-content`), or JSON with `--json`. As with `document list`, the review status (and the `review_status` key in `--json`) is present only while the review workflow is on — see `review_workflow_enabled` in [configuration.md](configuration.md#review-workflow).
510
+
509
511
  **MCP equivalent**: [`cerefox_metadata_search`](../../AGENT_GUIDE.md).
510
512
 
511
513
  ---
@@ -702,11 +704,16 @@ Agents see 15 tools with the flag off and 19 with it on. See
702
704
 
703
705
  **Synopsis**:
704
706
  ```
705
- cerefox config list # all current key/value pairs
707
+ cerefox config list [--json] # every settable key, grouped, with kind + default (from the shared catalog)
706
708
  cerefox config get KEY
707
709
  cerefox config set KEY VALUE [--author NAME] [--author-type user|agent]
708
710
  ```
709
711
 
712
+ `config list` is derived from the same catalog the web Settings page renders
713
+ (v1.13.0, #239 — the earlier hand-written list had drifted and hid three
714
+ working keys). `--json` returns `{ keys: string[], catalog: [{ key, kind,
715
+ default, group, description }] }`; `keys` keeps its pre-1.13 shape.
716
+
710
717
  Since v1.9.0 every `config set` is recorded in the audit log by the server
711
718
  itself (`config-change`, with the old → new value), in the same transaction
712
719
  as the write — pass `--author` (or set `CEREFOX_AUTHOR_NAME`) so the entry
@@ -766,7 +773,7 @@ These flat commands handle install, configuration, and health. Run any with `--h
766
773
  | Command | Purpose |
767
774
  |---|---|
768
775
  | `cerefox init` | Interactive first-run setup; writes `~/.cerefox/.env`, offers `server deploy` + self-docs ingest. |
769
- | `cerefox doctor` | Diagnose the install (credentials, DB reachability, schema version). |
776
+ | `cerefox doctor` | Diagnose the install (credentials, DB reachability, schema version, whether the review workflow is on). |
770
777
  | `cerefox status` | Show connection + schema status. |
771
778
  | `cerefox configure-agent --tool <client>` | Write MCP client config (`claude-code`, `claude-desktop`, `cursor`, `codex`, `gemini`). |
772
779
  | `cerefox token generate` / `rotate` / `list` | Manage the Cerefox access token (`cfx_pat_…`) — the Edge Function Bearer credential (remote MCP, GPT Actions, curl). See the [`cerefox token`](#cerefox-token-generate--cerefox-token-rotate--cerefox-token-list) section above. |