@cerefox/memory 1.16.1 → 1.17.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.
@@ -152,7 +152,7 @@ DECLARE
152
152
  -- plainto_tsquery: ANDs all terms, treats every token as a literal word.
153
153
  -- We deliberately avoid websearch_to_tsquery here because it interprets `-` as
154
154
  -- a negation operator, which traps natural queries against dashed titles
155
- -- (e.g. `Job Hunting - Opportunity Index`). Agent queries don't use the
155
+ -- (e.g. `Release Notes - Q3 Index`). Agent queries don't use the
156
156
  -- websearch operators (phrase, OR, NOT); semantic ranking is the soft-match
157
157
  -- layer for "broadly related". If operator support is ever needed, gate it
158
158
  -- behind an opt-in flag rather than changing the default.
@@ -177,12 +177,17 @@ DECLARE
177
177
  total_tokens INT;
178
178
  candidate_count INT := p_match_count * 5;
179
179
  -- #133 resolution: caller value, else deployment config, else built-in.
180
+ -- 0.18.0: the built-in floor follows the store's embedder (0.5 OpenAI,
181
+ -- 0.6 nomic), so a store with no config row gets the right one.
180
182
  v_min_score FLOAT := COALESCE(p_min_score,
181
- cerefox_config_float('min_search_score', 0.5));
183
+ cerefox_config_float('min_search_score',
184
+ cerefox_default_min_search_score()));
182
185
  v_alpha FLOAT := COALESCE(p_alpha,
183
186
  cerefox_config_float('search_alpha', 0.7));
187
+ -- 0.18.0: 0.67 (was 0.5), so a confident OR-fallback match needs two of
188
+ -- three terms, not one of two (docs/specs/search-calibration.md).
184
189
  v_min_coverage FLOAT := COALESCE(p_min_term_coverage,
185
- cerefox_config_float('min_term_coverage', 0.5));
190
+ cerefox_config_float('min_term_coverage', 0.67));
186
191
  BEGIN
187
192
  -- Build the OR-composed query: plainto each whitespace token (so tokens get
188
193
  -- the same normalization/stemming as the AND path), skip stopword-only
@@ -225,7 +230,22 @@ BEGIN
225
230
  fts_results AS (
226
231
  SELECT
227
232
  c.id,
228
- ts_rank_cd(c.fts, query_fts)::FLOAT AS fts_score,
233
+ -- 0.18.0: normalisation 32 bounds the keyword score to
234
+ -- rank/(rank+1), in [0, 1) like the cosine it is fused with.
235
+ -- Unbounded (up to ~4 when every term matches), one keyword
236
+ -- hit outweighed any semantic evidence, and the same document
237
+ -- scored 1.2 or 0.3 depending on which phrasing found it
238
+ -- (docs/specs/search-calibration.md).
239
+ ts_rank_cd(c.fts, query_fts, 32)::FLOAT AS fts_score,
240
+ -- 0.18.1: the exact cosine of every keyword match, so a chunk
241
+ -- that matched by keyword is never fused as if its meaning
242
+ -- scored 0 merely because it fell outside vec_results.
243
+ CASE
244
+ WHEN p_use_upgrade AND c.embedding_upgrade IS NOT NULL
245
+ THEN (1.0 - (c.embedding_upgrade <=> p_query_embedding))::FLOAT
246
+ ELSE
247
+ (1.0 - (c.embedding_primary <=> p_query_embedding))::FLOAT
248
+ END AS fts_vec_score,
229
249
  -- v1.0.4 coverage gate: in AND mode a match means 100% of the
230
250
  -- query's terms are present, so the unconditional pass is
231
251
  -- earned by construction. In OR-fallback mode, earn it only
@@ -272,21 +292,24 @@ BEGIN
272
292
  ))
273
293
  AND (p_metadata_filter IS NULL OR d.metadata @> p_metadata_filter)
274
294
  AND (p_review_status IS NULL OR d.review_status = p_review_status)
275
- ORDER BY
276
- CASE
277
- WHEN p_use_upgrade AND c.embedding_upgrade IS NOT NULL
278
- THEN c.embedding_upgrade <=> p_query_embedding
279
- ELSE c.embedding_primary <=> p_query_embedding
280
- END
295
+ -- 0.18.1: ordered by the computed similarity, NOT by the bare
296
+ -- `embedding <=> query` distance. The bare form (which the CASE
297
+ -- folds to once p_use_upgrade is known) lets the planner serve this
298
+ -- from the HNSW index, which is approximate and returns at most
299
+ -- hnsw.ef_search (40) rows: on a store with heavy version churn it
300
+ -- returned ~38 of the 250 candidates asked for, mostly not the
301
+ -- nearest ones. An exact scan is also faster at knowledge-base
302
+ -- scale (thousands of chunks).
303
+ ORDER BY vec_score DESC
281
304
  LIMIT candidate_count
282
305
  ),
283
306
  combined AS (
284
307
  SELECT
285
308
  COALESCE(f.id, v.id) AS id,
286
- ( v_alpha * COALESCE(v.vec_score, 0.0) +
309
+ ( v_alpha * COALESCE(v.vec_score, f.fts_vec_score, 0.0) +
287
310
  (1.0 - v_alpha) * COALESCE(f.fts_score, 0.0)
288
311
  ) AS score,
289
- COALESCE(v.vec_score, 0.0) AS vec_score,
312
+ COALESCE(v.vec_score, f.fts_vec_score, 0.0) AS vec_score,
290
313
  -- TRUE when the chunk matched the @@ FTS operator WITH enough
291
314
  -- term coverage to earn the unconditional pass (v1.0.4; always
292
315
  -- true for AND-mode matches). We use this flag rather than
@@ -406,7 +429,7 @@ DECLARE
406
429
  seen_tokens TEXT[] := '{}';
407
430
  total_tokens INT;
408
431
  v_min_coverage FLOAT := COALESCE(p_min_term_coverage,
409
- cerefox_config_float('min_term_coverage', 0.5));
432
+ cerefox_config_float('min_term_coverage', 0.67));
410
433
  BEGIN
411
434
  FOR tok IN SELECT unnest(regexp_split_to_array(trim(p_query_text), '\s+')) LOOP
412
435
  tok_q := plainto_tsquery('english', tok);
@@ -554,12 +577,10 @@ BEGIN
554
577
  THEN (1.0 - (c.embedding_upgrade <=> p_query_embedding))::FLOAT
555
578
  ELSE (1.0 - (c.embedding_primary <=> p_query_embedding))::FLOAT
556
579
  END >= p_min_score
557
- ORDER BY
558
- CASE
559
- WHEN p_use_upgrade AND c.embedding_upgrade IS NOT NULL
560
- THEN c.embedding_upgrade <=> p_query_embedding
561
- ELSE c.embedding_primary <=> p_query_embedding
562
- END
580
+ -- 0.18.1: by the computed similarity, so the approximate HNSW index cannot
581
+ -- serve (and truncate) it; see vec_results in cerefox_hybrid_search.
582
+ -- By position: `score` would be ambiguous with the OUT column here.
583
+ ORDER BY 8 DESC
563
584
  LIMIT p_match_count;
564
585
  END;
565
586
  $$;
@@ -2755,6 +2776,28 @@ BEGIN
2755
2776
  END;
2756
2777
  $$;
2757
2778
 
2779
+ -- Built-in vector-similarity floor for this store (0.18.0), used when
2780
+ -- `min_search_score` has no config row. It depends on the embedder: nomic's
2781
+ -- cosine for unrelated text sits around 0.4-0.55 (OpenAI's around 0.1-0.3), so
2782
+ -- 0.5 lets unrelated documents through as confident results. Cerefox Local
2783
+ -- used to get its 0.6 from a config row seeded at container start; the s6
2784
+ -- image never ran that seed, so fresh Local stores searched at 0.5. Deriving it
2785
+ -- here covers every store whatever created it. A store holds one embedder
2786
+ -- (`cerefox doctor` enforces it), so one current chunk decides.
2787
+ CREATE OR REPLACE FUNCTION cerefox_default_min_search_score()
2788
+ RETURNS FLOAT
2789
+ LANGUAGE sql
2790
+ SECURITY DEFINER
2791
+ STABLE
2792
+ SET search_path = public, pg_catalog
2793
+ AS $$
2794
+ SELECT CASE
2795
+ WHEN (SELECT embedder_primary FROM cerefox_chunks
2796
+ WHERE version_id IS NULL LIMIT 1) LIKE 'nomic-%'
2797
+ THEN 0.6 ELSE 0.5
2798
+ END::FLOAT;
2799
+ $$;
2800
+
2758
2801
  -- Integer/boolean companions to cerefox_config_float. Same contract: fall back
2759
2802
  -- to the caller's default when the key is unset or unparseable, so a malformed
2760
2803
  -- row can never break a write path.
@@ -3186,6 +3229,15 @@ SET search_path = public, pg_catalog
3186
3229
  AS $$
3187
3230
  -- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
3188
3231
  -- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
3232
+ -- 0.18.1: exact vector candidates. Hybrid and semantic search ordered by
3233
+ -- the bare distance, which let the planner use the approximate HNSW index
3234
+ -- (at most ef_search = 40 rows, poor recall after heavy version churn);
3235
+ -- they now order by the computed similarity (exact scan). Keyword matches
3236
+ -- carry their exact cosine into fusion. RPC-only.
3237
+ -- 0.18.0 (iteration 48): search calibration, RPC-only. Bounded keyword
3238
+ -- score in hybrid fusion (ts_rank_cd normalisation 32); min_term_coverage
3239
+ -- default 0.67; built-in min_search_score follows the store's embedder
3240
+ -- (new cerefox_default_min_search_score). docs/specs/search-calibration.md.
3189
3241
  -- 0.17.0 (#251): trash auto-purge. New cerefox_purge_expired_trash, called
3190
3242
  -- by cerefox_delete_document after a real soft delete (which now returns
3191
3243
  -- `auto_purged`); two config keys, off by default. Migration 0033.
@@ -3227,7 +3279,7 @@ AS $$
3227
3279
  -- 0.11.0 supersedes 0.10.6 (v1.2.1, #191): this branch carries that fix plus
3228
3280
  -- the partial-edit surface, and both migrations (0019, 0020) are in the
3229
3281
  -- sequence, so a store deploying this gets everything from both lines.
3230
- SELECT '0.17.0'::TEXT;
3282
+ SELECT '0.18.1'::TEXT;
3231
3283
  $$;
3232
3284
 
3233
3285
  -- ── 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.17.0
8
+ -- @version: 0.18.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 —
@@ -144,9 +144,26 @@ This handles intermittent OpenAI API errors (500s) that would otherwise cause se
144
144
  > ```
145
145
  >
146
146
  > Resolution order, highest first: **per-call argument** (`--min-score`, the
147
- > `min_score` MCP parameter) → **client env var** below → **`cerefox_config`**
148
- > → built-in default. A malformed stored value is ignored in favour of the
149
- > built-in, so a bad setting can never break search.
147
+ > `min_score` MCP parameter) → **`cerefox_config`** → built-in default. A
148
+ > malformed stored value is ignored in favour of the built-in, so a bad setting
149
+ > can never break search. (Before v1.17.0 the web UI and `/api/v1` search sent
150
+ > their own `min_search_score` and `search_alpha`, so they did not obey a stored
151
+ > value; now every path does.)
152
+ >
153
+ > **Built-in defaults (v1.17.0, schema 0.18.0)**, chosen by the search
154
+ > calibration benchmark (`docs/specs/search-calibration.md`):
155
+ >
156
+ > | Key | Default | Meaning |
157
+ > |---|---|---|
158
+ > | `min_search_score` | **0.5** on an OpenAI store, **0.6** on a local-embedder (nomic) store, derived from the store's own embeddings | A result with no confident keyword match must be at least this similar to the query. Nomic scores unrelated text around 0.4 to 0.55, so 0.5 would let it through. |
159
+ > | `min_term_coverage` | **0.67** (was 0.5) | When no document contains every query word, a keyword match counts as confident only if it covers this share of the words: two of three, not one of two. |
160
+ > | `search_alpha` | **0.7** | Weight of meaning against keywords. Both sides are on a 0 to 1 scale since v1.17.0, so the weight now means what it says. |
161
+ >
162
+ > Tune only with evidence: `bun scripts/search_benchmark.ts` measures any
163
+ > setting against a labelled query vocabulary on a staging store
164
+ > ([ops-scripts.md](ops-scripts.md#search_benchmarkts--search-calibration-benchmark)).
165
+ > A setting that suits one store's content can cost another kind of query
166
+ > without anything looking wrong.
150
167
 
151
168
  > **Which paths read these?** Client-side tunables in this section are read
152
169
  > from *your* `.env` by the **CLI**, the **local MCP server**, and `cerefox
@@ -162,8 +179,8 @@ This handles intermittent OpenAI API errors (500s) that would otherwise cause se
162
179
  |----------|---------|-------------|
163
180
  | `CEREFOX_MAX_RESPONSE_BYTES` | `200000` | Maximum bytes in a single search response (local MCP path). See explanation below. |
164
181
  | `CEREFOX_MIN_SEARCH_SCORE` | **Retired in v1.1.0 — no longer read.** | Now a store setting: `cerefox config set min_search_score <value>`, or the **Settings** page. The right floor depends on which embedder produced the vectors, and the embedder belongs to the store — every client querying one database must use the same one. A per-call override still works (`cerefox search --min-score`, the MCP `min_score` param). `cerefox doctor` and the Settings page report the variable if it is still set. |
165
- | `CEREFOX_SEARCH_ALPHA` | **Retired in v1.1.0 — no longer read.** | Now a store setting: `cerefox config set search_alpha <value>`, or the **Settings** page. A per-call override still works (`cerefox search --min-score`, the MCP `min_score` param). `cerefox doctor` and the Settings page report the variable if it is still set. |
166
- | `CEREFOX_MIN_TERM_COVERAGE` | **Retired in v1.1.0 — no longer read.** | Now a store setting: `cerefox config set min_term_coverage <value>`, or the **Settings** page. A per-call override still works (`cerefox search --min-score`, the MCP `min_score` param). `cerefox doctor` and the Settings page report the variable if it is still set. |
182
+ | `CEREFOX_SEARCH_ALPHA` | **Retired in v1.1.0 — no longer read.** | Now a store setting: `cerefox config set search_alpha <value>`, or the **Settings** page. A per-call override still works (`cerefox search --alpha`). `cerefox doctor` and the Settings page report the variable if it is still set. |
183
+ | `CEREFOX_MIN_TERM_COVERAGE` | **Retired in v1.1.0 — no longer read.** | Now a store setting: `cerefox config set min_term_coverage <value>`, or the **Settings** page. A per-call override still works (`cerefox search --min-term-coverage`, the MCP `min_term_coverage` param). `cerefox doctor` and the Settings page report the variable if it is still set. |
167
184
  | `CEREFOX_EMBED_MAX_INPUT_CHARS` | `20000` | Safety cap on the characters sent to the embedding model per input. The full chunk content is always stored and reconstructed untouched; only the embedding uses the (rare) truncated prefix, so an oversized chunk can never fail an ingest. |
168
185
  | `CEREFOX_MODELS_DIR` | `~/.cerefox/models` (in-container: inside the data volume) | Where the local embedder caches downloaded model weights (Cerefox Local; `CEREFOX_EMBEDDER=local`). |
169
186
  | `CEREFOX_ONNX_BATCH` | `4` | Texts per local-embedder inference call. Peak memory scales with this; the small default keeps ingest/reindex safe on small Docker VMs. |
@@ -10,7 +10,7 @@ corrupting a long document id while rewriting content.
10
10
  Write a normal markdown link whose target is the document's UUID:
11
11
 
12
12
  ```markdown
13
- See the [Opportunity Index](c937b70f-77af-43d3-b9bc-9f31e0d2041d) for details.
13
+ See the [Q3 Goals](3f2b9c1e-8a47-4d2e-9b6a-1c5e7d0a4f82) for details.
14
14
  ```
15
15
 
16
16
  The Cerefox web UI resolves these at click time — the stored markdown is
@@ -22,7 +22,7 @@ Four link forms are accepted; one is recommended:
22
22
 
23
23
  | Form | Example target | Use |
24
24
  |---|---|---|
25
- | **Document UUID** | `c937b70f-…` | **Always, for cross-references.** Stable, unambiguous, encoding-safe, validated (below). |
25
+ | **Document UUID** | `3f2b9c1e-…` | **Always, for cross-references.** Stable, unambiguous, encoding-safe, validated (below). |
26
26
  | Repo-relative path | `docs/guides/quickstart.md` | Repo-ingested files only (their markdown naturally uses paths). |
27
27
  | Basename | `quickstart.md` | Best-effort fallback for the same. |
28
28
  | `[[Wikilink]]` | `[[Future Doc Name]]` | A deliberate **dangling** reference — "this document should exist someday." Never validated. |
@@ -31,6 +31,7 @@ The canonical scripts are **TypeScript**, run with [Bun](https://bun.sh) (instal
31
31
  | `reindex_all.ts` | `bun scripts/reindex_all.ts` |
32
32
  | `cerefox_export.ts` | `bun scripts/cerefox_export.ts` |
33
33
  | `gen_openapi.ts` | `bun scripts/gen_openapi.ts` |
34
+ | `search_benchmark.ts` | `bun scripts/search_benchmark.ts --api … --db … --embed …` (staging only) |
34
35
  | `cut_release.ts` | `bun scripts/cut_release.ts` (see [RELEASING.md](../../RELEASING.md)) |
35
36
 
36
37
  The remaining files under `scripts/` are not run by hand: `bundle_help.ts`,
@@ -326,6 +327,50 @@ Two things worth knowing about what it does *not* claim:
326
327
  every release cut — which broke the v1.15.1 npm publish — and churned every
327
328
  generated client on patch releases that did not touch the API.
328
329
 
330
+ ## search_benchmark.ts — Search-calibration benchmark
331
+
332
+ Measures how search ranks against a labelled query vocabulary
333
+ (`_shared/search-benchmark/vocabulary/`: a synthetic corpus, about 150 queries
334
+ in 13 categories, variant groups, no-answer queries) and compares alternative
335
+ scoring formulas and settings. It is how the v1.17.0 defaults were chosen. Design
336
+ and results: [`docs/specs/search-calibration.md`](../specs/search-calibration.md).
337
+
338
+ **It writes**, so it refuses a target without a label (`--label` or
339
+ `CEREFOX_ENV_LABEL`): the corpus goes into a project named
340
+ `Search calibration benchmark`, and a temporary read-only probe function
341
+ (`cerefox_bench_signals`) is created for the run and dropped after it. Re-runs
342
+ write only documents whose content changed.
343
+
344
+ ```bash
345
+ # Staging (OpenAI): the staging web server ingests, the database URL probes
346
+ set -a; . ~/.cerefox/staging/.env; set +a
347
+ bun scripts/search_benchmark.ts --api http://127.0.0.1:8030 \
348
+ --db "$CEREFOX_DATABASE_URL" --embed openai --out /tmp/bench.json > /tmp/bench.md
349
+
350
+ # A throwaway Cerefox Local (nomic), postgres published on 55432
351
+ bun scripts/search_benchmark.ts --api http://127.0.0.1:8099 \
352
+ --db postgresql://cerefox:cerefox@127.0.0.1:55432/cerefox \
353
+ --embed container:<container-name> --label bench-throwaway
354
+ ```
355
+
356
+ | Flag | Description |
357
+ |------|-------------|
358
+ | `--api URL` | The target's web server; the corpus is ingested through `/api/v1/ingest` |
359
+ | `--db URL` | Direct Postgres URL, for the probe and the live search calls |
360
+ | `--embed openai \| container:<name>` | How queries are embedded: OpenAI (needs `OPENAI_API_KEY`), or the repo's own local embedder run inside a Cerefox Local container |
361
+ | `--label TEXT` | Required unless `CEREFOX_ENV_LABEL` is set |
362
+ | `--corpus DIR` | Use another vocabulary directory (for drafting one) |
363
+ | `--out FILE` | JSON report (the markdown summary goes to stdout) |
364
+ | `--write-floors` | Record the per-category floors of the built-in defaults for this store's embedder in `vocabulary/floors.json` |
365
+ | `--check-floors` | Fail (exit 1) if the built-in defaults on this store fall below any recorded floor for its embedder. The release check for search changes (RELEASING.md) |
366
+ | `--cleanup` | Delete the benchmark documents and project afterwards |
367
+
368
+ Before reporting any number it checks that its reproduction of the live formula
369
+ matches `cerefox_search_docs` exactly for every query, and stops if it does not.
370
+ A mismatch means the harness no longer models the RPC; fix the harness first.
371
+
372
+ ---
373
+
329
374
  ## sync_docs.ts — Sync project documentation into Cerefox
330
375
 
331
376
  Ingests `README.md`, `AGENT_GUIDE.md`, `AGENT_QUICK_REFERENCE.md`, and every Markdown file under `docs/` into your Cerefox knowledge base, updating existing documents in-place. Run this any time after editing documentation so AI agents always have access to the current state of the project.
@@ -62,8 +62,10 @@ data volume, so it survives `cerefox-local upgrade`.
62
62
 
63
63
  > Scores are calibrated per embedder: with the local model the default semantic
64
64
  > threshold is **0.6** (vs 0.5 for OpenAI) because nomic scores unrelated text
65
- > higher. The container seeds `min_search_score = 0.6` into its own config at
66
- > first boot; change it with `cerefox config set min_search_score <value>` or the
65
+ > higher. Since v1.17.0 the store derives that default from its own embedder,
66
+ > so no setting is needed (before it, containers created from the current image
67
+ > never got the 0.6 they were meant to seed, and searched at 0.5 through MCP and
68
+ > the CLI). Change it with `cerefox config set min_search_score <value>` or the
67
69
  > Settings page, or override a single query with `--min-score`.
68
70
 
69
71
  ---
@@ -197,6 +197,13 @@ knowing about:
197
197
  - **v1.13.1 — the flag hides, it does not rewrite.** Agent writes are
198
198
  recorded `pending_review` whether or not the workflow is shown (v1.13.0
199
199
  stored `approved` while off). Redeploy to pick it up.
200
+ - **v1.17.0 — search scoring recalibrated (schema 0.18.0).** Run
201
+ `cerefox server deploy` (Cerefox Local does it on container start). Scores in
202
+ `docs` and `hybrid` search are now between 0 and 1, and rankings shift: mostly
203
+ for the better on paraphrased and short queries, and fewer unrelated results
204
+ come back as confident. If you compare scores against a fixed number, or set
205
+ `min_search_score` / `min_term_coverage` yourself, recheck those values; see
206
+ [configuration.md](configuration.md) for the new defaults.
200
207
 
201
208
  ## Notable: v1.8.0 storage reclaim (migration 0027)
202
209
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cerefox/memory",
3
- "version": "1.16.1",
3
+ "version": "1.17.1",
4
4
  "description": "Cerefox \u2014 user-owned shared memory for AI agents. CLI + stdio MCP server + web UI + ingestion for a knowledge base on your own Supabase project (or fully self-hosted with Cerefox Local).",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/fstamatelopoulos/cerefox",