@cerefox/memory 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_QUICK_REFERENCE.md +2 -1
- package/dist/bin/cerefox.js +25 -5
- package/dist/frontend/assets/{index-CLD16BC4.js → index-Csj-6UHY.js} +2 -2
- package/dist/frontend/assets/{index-CLD16BC4.js.map → index-Csj-6UHY.js.map} +1 -1
- package/dist/frontend/index.html +1 -1
- package/dist/server-assets/_shared/ef-meta/index.ts +11 -1
- package/dist/server-assets/_shared/mcp-tools/get-help-content.ts +2 -2
- package/dist/server-assets/_shared/mcp-tools/search.ts +15 -1
- package/dist/server-assets/db/rpcs.sql +124 -15
- package/dist/server-assets/db/schema.sql +1 -1
- package/package.json +1 -1
package/dist/frontend/index.html
CHANGED
|
@@ -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-
|
|
18
|
+
<script type="module" crossorigin src="/app/assets/index-Csj-6UHY.js"></script>
|
|
19
19
|
<link rel="stylesheet" crossorigin href="/app/assets/index-Asx5wD7g.css">
|
|
20
20
|
</head>
|
|
21
21
|
<body>
|
|
@@ -18,7 +18,17 @@
|
|
|
18
18
|
* doesn't touch `supabase/functions/` leaves it alone).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
export const EF_VERSION = "1.0.
|
|
21
|
+
export const EF_VERSION = "1.0.3";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The most recent version whose EF-side SOURCE actually changed (#127).
|
|
25
|
+
* `EF_VERSION` bumps unconditionally at stable cuts (so stable deployments
|
|
26
|
+
* never display a pre-release label), which means a version delta no longer
|
|
27
|
+
* implies the deployed behaviour differs. This constant is bumped by
|
|
28
|
+
* `cut_release.ts` ONLY when EF source changed since the last tag; doctor
|
|
29
|
+
* uses it to stay silent on label-only drift.
|
|
30
|
+
*/
|
|
31
|
+
export const EF_LAST_CHANGED = "1.0.3";
|
|
22
32
|
|
|
23
33
|
/**
|
|
24
34
|
* The 8 peer EFs the cerefox-mcp aggregator probes (excludes cerefox-mcp
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
* docs/specs/polish-and-distribution-design.md §10d.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
export const HELP_FULL = "# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **10 MCP tools** (9 of them have CLI equivalents — `cerefox_get_help` is MCP-only). For the full guide, search Cerefox for \"How AI Agents Use Cerefox\" or call `cerefox_get_help` to retrieve this content over MCP.\n\n## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token) | `document_id` (required) |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc's project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., \"Claude Code\", \"archiver\"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user's `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` (\"decision-log\", \"research\", \"design-doc\") and `status` (\"active\", \"draft\").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don't write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don't construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you read (shown by `cerefox_get_document`, `cerefox_search`, and `cerefox_metadata_search`) when updating a document. If it's stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer's work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc's full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean \"add\" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.\n\n## Update Workflow (ID-based -- preferred)\n\n```\nsearch(\"topic\") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title=\"Same Title\", content=\"...\", document_id=\"abc123\",\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch(\"topic\") -> find doc (note its hash) -> modify ->\ningest(title=\"Same Title\", content=\"...\", update_if_exists=true,\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\n## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={\"type\": \"decision-log\"}, updated_since=\"2026-03-28T00:00:00Z\")\n```\n\n## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search \"<q>\" --requestor \"<your-name>\"` |\n| `cerefox_ingest` (paste) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --document-id \"<uuid>\" --expected-content-hash \"<hash>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor \"<your-name>\"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor \"<your-name>\"` |\n| `cerefox_list_projects` | `cerefox project list --requestor \"<your-name>\"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --requestor \"<your-name>\"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author \"<your-name>\" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor \"<your-name>\"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author \"<your-name>\" --author-type agent`\n- Reads: `--requestor \"<your-name>\"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.\n";
|
|
14
|
+
export const HELP_FULL = "# Cerefox Knowledge Base -- Agent Quick Reference\n\nCerefox is a persistent, shared knowledge base. You have **10 MCP tools** (9 of them have CLI equivalents — `cerefox_get_help` is MCP-only). For the full guide, search Cerefox for \"How AI Agents Use Cerefox\" or call `cerefox_get_help` to retrieve this content over MCP.\n\n## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token) | `document_id` (required) |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc's project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |\n\n## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., \"Claude Code\", \"archiver\"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user's `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` (\"decision-log\", \"research\", \"design-doc\") and `status` (\"active\", \"draft\").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don't write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don't construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you read (shown by `cerefox_get_document`, `cerefox_search`, and `cerefox_metadata_search`) when updating a document. If it's stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer's work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc's full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean \"add\" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.\n\n## Update Workflow (ID-based -- preferred)\n\n```\nsearch(\"topic\") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title=\"Same Title\", content=\"...\", document_id=\"abc123\",\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.\n\n## Update Workflow (title-based -- fallback)\n\n```\nsearch(\"topic\") -> find doc (note its hash) -> modify ->\ningest(title=\"Same Title\", content=\"...\", update_if_exists=true,\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\n## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={\"type\": \"decision-log\"}, updated_since=\"2026-03-28T00:00:00Z\")\n```\n\n## CLI fallback (when MCP is unavailable)\n\nIf `cerefox_search` is not in your tool list, your user has likely installed the Cerefox CLI. The canonical invocation is plain **`cerefox <subcommand>`** (the TypeScript CLI, installed via `npm install -g @cerefox/memory`). It uses a resource-verb shape (`cerefox document get`, `cerefox project list`, …).\n\nSame operations, same conventions. Full reference: [`docs/guides/cli.md`](docs/guides/cli.md). CLI flag names match MCP parameter names exactly (e.g. `metadata_filter` ↔ `--metadata-filter`); common flags also have single-letter short forms (`-f`, `-p`, `-c`, `-m`, `-u`, `-a`, `-r`). Use the canonical long name (what `--help` shows) or its short form — there are no long-form aliases like `--filter` or `--count`.\n\n| MCP tool | CLI |\n|---|---|\n| `cerefox_search` | `cerefox search \"<q>\" --requestor \"<your-name>\"` |\n| `cerefox_ingest` (paste) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_ingest` (update by ID) | `printf '...' \\| cerefox document ingest --paste --title \"<t>\" --document-id \"<uuid>\" --expected-content-hash \"<hash>\" --author \"<your-name>\" --author-type agent` |\n| `cerefox_get_document` | `cerefox document get <id> --version-id <vid> --requestor \"<your-name>\"` |\n| `cerefox_list_versions` | `cerefox document version list <id> --requestor \"<your-name>\"` |\n| `cerefox_list_projects` | `cerefox project list --requestor \"<your-name>\"` |\n| `cerefox_list_metadata_keys` | `cerefox metadata keys` |\n| `cerefox_metadata_search` | `cerefox metadata search --metadata-filter '<json>' --requestor \"<your-name>\"` (list a project: `cerefox document list --project <name>`) |\n| `cerefox_set_document_projects` | `cerefox document set-projects <id> <name...> --author \"<your-name>\" --author-type agent` (or `--clear` to remove all) |\n| `cerefox_get_audit_log` | `cerefox audit list --requestor \"<your-name>\"` (add `--json` for scripted access) |\n| `cerefox_get_help` | `cerefox guides show agent-quick-reference` (or `cerefox guides list` for the full bundled-docs index) |\n\n**Set identity on every call**, exactly as you would on MCP:\n- Writes (`document ingest`, `document ingest-dir`): `--author \"<your-name>\" --author-type agent`\n- Reads: `--requestor \"<your-name>\"`\n\nOr have your user set `CEREFOX_AUTHOR_NAME` / `CEREFOX_AUTHOR_TYPE` / `CEREFOX_REQUESTOR_NAME` in their `.env` to apply defaults once.\n";
|
|
15
15
|
|
|
16
16
|
/** Sections keyed by their H2 heading text (lower-cased for matching). */
|
|
17
17
|
export const HELP_SECTIONS: Record<string, string> = {
|
|
18
18
|
"Tools": "## Tools\n\n| Tool | Purpose | Key params |\n|------|---------|------------|\n| `cerefox_search` | Find documents (hybrid FTS + semantic) | `query` (required), `project_name`, `metadata_filter`, `requestor` |\n| `cerefox_ingest` | Save or update a document | `title`, `content` (required), `document_id` (update by ID), `expected_content_hash` (**required on content updates** — see rule 9), `last_write_wins`, `update_if_exists`, `project_name` (single, non-destructive add on update), `project_names` (list, destructive replace on update), `metadata` (omit on update to keep existing tags; `{}` clears), `author` |\n| `cerefox_get_document` | Get full document by ID (header includes `content_hash` — the update token) | `document_id` (required) |\n| `cerefox_list_versions` | Version history of a document | `document_id` (required) |\n| `cerefox_metadata_search` | Find or list docs by metadata, project, or time (no text query) | `metadata_filter`, `project_name` (list a project's docs), `updated_since`, `include_content` — **at least one** of metadata_filter/project_name/updated_since/created_since |\n| `cerefox_list_metadata_keys` | Discover available metadata keys | (none required) |\n| `cerefox_list_projects` | List all projects | (none required) |\n| `cerefox_set_document_projects` | Set doc's project memberships to exactly the given list (destructive replace; metadata-only, no content change) | `document_id`, `project_names` (required) |\n| `cerefox_get_audit_log` | Query write operation history | `document_id`, `author`, `operation`, `since` |\n| `cerefox_get_help` | Retrieve Cerefox conventions (this reference) over MCP. **Call this whenever uncertain.** | `topic` (optional, case-insensitive H2 substring match) |",
|
|
19
|
-
"Essential Rules": "## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., \"Claude Code\", \"archiver\"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user's `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` (\"decision-log\", \"research\", \"design-doc\") and `status` (\"active\", \"draft\").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don't write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don't construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you read (shown by `cerefox_get_document`, `cerefox_search`, and `cerefox_metadata_search`) when updating a document. If it's stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer's work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc's full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean \"add\" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.",
|
|
19
|
+
"Essential Rules": "## Essential Rules\n\n1. **Search before ingesting** -- check if the document exists first.\n2. **Prefer ID-based updates** -- pass `document_id` from search results for deterministic updates. Falls back to title-matching with `update_if_exists: true`.\n3. **Set `author`/`requestor`** to your name on every call (e.g., \"Claude Code\", \"archiver\"). On MCP, pass as parameters. On CLI, pass `--author`/`--author-type`/`--requestor` flags, or rely on `CEREFOX_AUTHOR_NAME`/`CEREFOX_AUTHOR_TYPE`/`CEREFOX_REQUESTOR_NAME` env vars set in the user's `.env`.\n4. **Use `document_id` from search results** `[id: uuid]` for get_document and list_versions.\n5. **Add metadata** -- at minimum `type` (\"decision-log\", \"research\", \"design-doc\") and `status` (\"active\", \"draft\").\n6. **Write structured Markdown** with H1/H2/H3 headings for good chunking and search.\n7. **Deletes are soft (recoverable); purge is web-UI-only.** If you decide to delete, surface it to the user (`I soft-deleted X — recoverable from the Cerefox web UI trash`). You cannot un-do your own delete from agent code by design.\n8. **Cross-doc links inside content**: **always use `[Text](document-uuid)`.** UUIDs are the only fully reliable link form — stable across title changes, never ambiguous, no encoding gotchas. Every `cerefox_search` result shows `[id: <uuid>]` after the title; grab it and use it. Title-based linking (`[Text](<Title With Spaces>)`) is fragile (breaks on colons, parens, ampersands, brackets — silently navigates to wrong page) — **don't write title-based links**; do an extra search to get the UUID instead. Repo-path forms (`[Text](docs/path.md)`) exist for repo-ingested files; don't construct manually. See `AGENT_GUIDE.md → Writing linkable content` for the full rule.\n9. **Concurrency: content updates require `expected_content_hash`.** Pass the `content_hash` you read (shown by `cerefox_get_document`, `cerefox_search`, and `cerefox_metadata_search`) when updating a document. If it's stale you get a **conflict** — re-read the document, merge your changes into the latest content, retry with the new hash. **Never resolve a conflict by overwriting blindly** — the current content includes another writer's work. `last_write_wins: true` skips the check; use it ONLY when an external source of truth makes conflicts meaningless (file re-sync), never to silence a conflict.\n10. **Search: prefer a few distinctive terms; heed `below confidence`.** When nothing clears the relevance threshold, `cerefox_search` returns the closest candidates prefixed with a `below confidence` warning instead of an empty set — that flag means **weak signal, not absent knowledge**: check the candidates' scores and titles before concluding the KB lacks the content. A truly empty response means nothing even weakly related exists.\n11. **Project memberships — non-destructive by default**: on `cerefox_ingest` updates, **`project_name` (singular) is a non-destructive add** (ensures membership, preserves others). Use **`project_names` (list)** when you want to set the doc's full project set in one call (destructive replace). For metadata-only project changes without writing content, use **`cerefox_set_document_projects(document_id, project_names)`** — that tool is the destructive-replace contract made explicit. Never call `cerefox_set_document_projects` with a single name when you mean \"add\" — that would REMOVE the doc from all other projects. When in doubt, use `cerefox_ingest` with singular `project_name`.",
|
|
20
20
|
"Update Workflow (ID-based -- preferred)": "## Update Workflow (ID-based -- preferred)\n\n```\nsearch(\"topic\") -> find doc [id: abc123] -> get_document(abc123) -> note its content_hash -> modify ->\ningest(title=\"Same Title\", content=\"...\", document_id=\"abc123\",\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```\n\nOn a **conflict** error: get_document again (fresh content + fresh hash) -> merge your changes -> retry with the new hash.",
|
|
21
21
|
"Update Workflow (title-based -- fallback)": "## Update Workflow (title-based -- fallback)\n\n```\nsearch(\"topic\") -> find doc (note its hash) -> modify ->\ningest(title=\"Same Title\", content=\"...\", update_if_exists=true,\n expected_content_hash=\"<the hash you read>\", author=\"my-agent\")\n```",
|
|
22
22
|
"Catch-Up Workflow": "## Catch-Up Workflow\n\n```\nmetadata_search(metadata_filter={\"type\": \"decision-log\"}, updated_since=\"2026-03-28T00:00:00Z\")\n```",
|
|
@@ -132,16 +132,24 @@ async function handler(
|
|
|
132
132
|
doc_title?: string;
|
|
133
133
|
full_content?: string;
|
|
134
134
|
best_score?: number;
|
|
135
|
+
score?: number;
|
|
135
136
|
is_partial?: boolean;
|
|
136
137
|
chunk_count?: number;
|
|
137
138
|
total_chars?: number;
|
|
138
139
|
content_hash?: string;
|
|
140
|
+
below_confidence?: boolean;
|
|
139
141
|
}>;
|
|
140
142
|
|
|
143
|
+
// 28I: nothing cleared the relevance threshold, so the server returned its
|
|
144
|
+
// best-effort top candidates flagged below_confidence instead of an empty
|
|
145
|
+
// set (which agents misread as "this knowledge does not exist").
|
|
146
|
+
const belowConfidence = rows.length > 0 && rows.every((r) => r.below_confidence === true);
|
|
147
|
+
|
|
141
148
|
const parts: string[] = rows.map((row) => {
|
|
142
149
|
const title = row.doc_title ?? "Untitled";
|
|
143
150
|
const docId = row.document_id ? ` [id: ${row.document_id}]` : "";
|
|
144
|
-
const
|
|
151
|
+
const rawScore = row.best_score ?? row.score;
|
|
152
|
+
const score = rawScore != null ? ` (score: ${rawScore.toFixed(3)})` : "";
|
|
145
153
|
const partial = row.is_partial
|
|
146
154
|
? ` -- partial (${row.chunk_count} of ${(row.total_chars ?? 0).toLocaleString()} chars)`
|
|
147
155
|
: "";
|
|
@@ -151,6 +159,12 @@ async function handler(
|
|
|
151
159
|
});
|
|
152
160
|
|
|
153
161
|
let output = parts.join("\n\n---\n\n");
|
|
162
|
+
if (belowConfidence) {
|
|
163
|
+
output =
|
|
164
|
+
`⚠ No results cleared the confidence threshold. Showing the closest ${rows.length} ` +
|
|
165
|
+
`candidate(s) with scores — judge relevance yourself; a low score means weak signal, ` +
|
|
166
|
+
`not necessarily absent knowledge.\n\n` + output;
|
|
167
|
+
}
|
|
154
168
|
if (truncated) {
|
|
155
169
|
output +=
|
|
156
170
|
`\n\n[Results truncated at ${usedBytes} bytes. Use a more specific query or a smaller match_count to see more.]`;
|
|
@@ -63,6 +63,12 @@ DROP FUNCTION IF EXISTS cerefox_search_docs(TEXT, VECTOR(768), INT, FLOAT, UUID,
|
|
|
63
63
|
DROP FUNCTION IF EXISTS cerefox_reconstruct_doc(UUID);
|
|
64
64
|
DROP FUNCTION IF EXISTS cerefox_get_document(UUID, UUID);
|
|
65
65
|
|
|
66
|
+
-- Iteration 28I (v1.0.3, search recall): below_confidence BOOLEAN added to the
|
|
67
|
+
-- return types of cerefox_hybrid_search and cerefox_search_docs (never-silently-
|
|
68
|
+
-- empty fallback). Drop the pre-change signatures first.
|
|
69
|
+
DROP FUNCTION IF EXISTS cerefox_hybrid_search(TEXT, VECTOR(768), INT, FLOAT, BOOLEAN, UUID, FLOAT, JSONB);
|
|
70
|
+
DROP FUNCTION IF EXISTS cerefox_search_docs(TEXT, VECTOR(768), INT, FLOAT, UUID, FLOAT, INT, INT, JSONB);
|
|
71
|
+
|
|
66
72
|
-- ── Shared return type note ────────────────────────────────────────────────────
|
|
67
73
|
-- All chunk-level search RPCs return the same shape for consistency:
|
|
68
74
|
-- chunk_id, document_id, chunk_index, title, content, heading_path,
|
|
@@ -106,7 +112,11 @@ RETURNS TABLE (
|
|
|
106
112
|
doc_project_ids UUID[],
|
|
107
113
|
doc_project_names TEXT[],
|
|
108
114
|
doc_metadata JSONB,
|
|
109
|
-
version_count INT
|
|
115
|
+
version_count INT,
|
|
116
|
+
-- 28I: TRUE on every row of a below-confidence fallback response — nothing
|
|
117
|
+
-- cleared the pass-filter, so these are the best-effort top candidates for
|
|
118
|
+
-- the caller to judge (scores included). FALSE on all normal results.
|
|
119
|
+
below_confidence BOOLEAN
|
|
110
120
|
)
|
|
111
121
|
LANGUAGE plpgsql
|
|
112
122
|
SECURITY DEFINER
|
|
@@ -120,9 +130,54 @@ DECLARE
|
|
|
120
130
|
-- websearch operators (phrase, OR, NOT); semantic ranking is the soft-match
|
|
121
131
|
-- layer for "broadly related". If operator support is ever needed, gate it
|
|
122
132
|
-- behind an opt-in flag rather than changing the default.
|
|
123
|
-
|
|
133
|
+
--
|
|
134
|
+
-- 28I progressive relaxation: AND-first, OR-fallback. The AND query stays
|
|
135
|
+
-- the primary match (byte-identical behavior whenever it matches anything),
|
|
136
|
+
-- but when it matches ZERO chunks (under the same project/metadata filters),
|
|
137
|
+
-- we retry with an OR-composition of the same tokens: one absent term then
|
|
138
|
+
-- no longer vetoes the terms that DO occur, and ts_rank_cd naturally ranks
|
|
139
|
+
-- chunks matching more terms higher. Multi-term evidence accumulates
|
|
140
|
+
-- instead of vetoing.
|
|
141
|
+
query_fts_and tsquery := plainto_tsquery('english', p_query_text);
|
|
142
|
+
query_fts_or tsquery := NULL;
|
|
143
|
+
query_fts tsquery;
|
|
144
|
+
tok TEXT;
|
|
145
|
+
tok_q tsquery;
|
|
146
|
+
and_matches BOOLEAN := FALSE;
|
|
124
147
|
candidate_count INT := p_match_count * 5;
|
|
125
148
|
BEGIN
|
|
149
|
+
-- Build the OR-composed query: plainto each whitespace token (so tokens get
|
|
150
|
+
-- the same normalization/stemming as the AND path), skip stopword-only
|
|
151
|
+
-- tokens, fold with the tsquery OR operator (||).
|
|
152
|
+
FOR tok IN SELECT unnest(regexp_split_to_array(trim(p_query_text), '\s+')) LOOP
|
|
153
|
+
tok_q := plainto_tsquery('english', tok);
|
|
154
|
+
IF numnode(tok_q) > 0 THEN
|
|
155
|
+
query_fts_or := CASE WHEN query_fts_or IS NULL
|
|
156
|
+
THEN tok_q ELSE query_fts_or || tok_q END;
|
|
157
|
+
END IF;
|
|
158
|
+
END LOOP;
|
|
159
|
+
|
|
160
|
+
-- Does the strict AND query match anything at all (under the caller's
|
|
161
|
+
-- filters)? Cheap probe against the partial FTS index.
|
|
162
|
+
IF numnode(query_fts_and) > 0 THEN
|
|
163
|
+
SELECT EXISTS (
|
|
164
|
+
SELECT 1
|
|
165
|
+
FROM cerefox_chunks c
|
|
166
|
+
JOIN cerefox_documents d ON c.document_id = d.id
|
|
167
|
+
WHERE c.version_id IS NULL
|
|
168
|
+
AND d.deleted_at IS NULL
|
|
169
|
+
AND c.fts @@ query_fts_and
|
|
170
|
+
AND (p_project_id IS NULL OR EXISTS (
|
|
171
|
+
SELECT 1 FROM cerefox_document_projects dp
|
|
172
|
+
WHERE dp.document_id = d.id AND dp.project_id = p_project_id
|
|
173
|
+
))
|
|
174
|
+
AND (p_metadata_filter IS NULL OR d.metadata @> p_metadata_filter)
|
|
175
|
+
) INTO and_matches;
|
|
176
|
+
END IF;
|
|
177
|
+
|
|
178
|
+
query_fts := CASE WHEN and_matches THEN query_fts_and
|
|
179
|
+
ELSE COALESCE(query_fts_or, query_fts_and) END;
|
|
180
|
+
|
|
126
181
|
RETURN QUERY
|
|
127
182
|
WITH
|
|
128
183
|
fts_results AS (
|
|
@@ -183,7 +238,17 @@ BEGIN
|
|
|
183
238
|
f.id IS NOT NULL AS has_fts_match
|
|
184
239
|
FROM fts_results f
|
|
185
240
|
FULL OUTER JOIN vec_results v ON f.id = v.id
|
|
186
|
-
)
|
|
241
|
+
),
|
|
242
|
+
-- 28I: pass-filter as a flag rather than a WHERE, so we can fall back.
|
|
243
|
+
-- FTS matches pass through unconditionally: the @@ operator is a hard
|
|
244
|
+
-- gate and guarantees the query terms appear in the chunk. Vector-only
|
|
245
|
+
-- results (no FTS match) are filtered by the cosine threshold.
|
|
246
|
+
flagged AS (
|
|
247
|
+
SELECT *,
|
|
248
|
+
(combined.has_fts_match OR combined.vec_score >= p_min_score) AS passes
|
|
249
|
+
FROM combined
|
|
250
|
+
),
|
|
251
|
+
any_pass AS (SELECT bool_or(fl.passes) AS ok FROM flagged fl)
|
|
187
252
|
SELECT
|
|
188
253
|
c.id AS chunk_id,
|
|
189
254
|
c.document_id,
|
|
@@ -202,16 +267,22 @@ BEGIN
|
|
|
202
267
|
WHERE dp.document_id = d.id) AS doc_project_names,
|
|
203
268
|
d.metadata AS doc_metadata,
|
|
204
269
|
(SELECT COUNT(*)::INT FROM cerefox_document_versions dv
|
|
205
|
-
WHERE dv.document_id = d.id) AS version_count
|
|
206
|
-
|
|
270
|
+
WHERE dv.document_id = d.id) AS version_count,
|
|
271
|
+
-- 28I: when NOTHING clears the pass-filter, return the top candidates
|
|
272
|
+
-- anyway, flagged — an empty response reads to agent callers as "this
|
|
273
|
+
-- knowledge does not exist", the most expensive wrong conclusion a
|
|
274
|
+
-- memory layer can produce. "Truly nothing" (no candidates at all)
|
|
275
|
+
-- still returns zero rows.
|
|
276
|
+
NOT ap.ok AS below_confidence
|
|
277
|
+
FROM flagged cm
|
|
278
|
+
CROSS JOIN any_pass ap
|
|
207
279
|
JOIN cerefox_chunks c ON c.id = cm.id
|
|
208
280
|
JOIN cerefox_documents d ON c.document_id = d.id
|
|
209
|
-
|
|
210
|
-
-- and guarantees the query terms appear in the chunk.
|
|
211
|
-
-- Vector-only results (no FTS match) are filtered by the cosine threshold.
|
|
212
|
-
WHERE cm.has_fts_match OR cm.vec_score >= p_min_score
|
|
281
|
+
WHERE cm.passes OR NOT ap.ok
|
|
213
282
|
ORDER BY cm.score DESC
|
|
214
|
-
LIMIT p_match_count
|
|
283
|
+
LIMIT (SELECT CASE WHEN ap2.ok THEN p_match_count
|
|
284
|
+
ELSE LEAST(p_match_count, 3) END
|
|
285
|
+
FROM any_pass ap2);
|
|
215
286
|
END;
|
|
216
287
|
$$;
|
|
217
288
|
|
|
@@ -245,9 +316,42 @@ SECURITY DEFINER
|
|
|
245
316
|
SET search_path = public, pg_catalog
|
|
246
317
|
AS $$
|
|
247
318
|
DECLARE
|
|
248
|
-
-- plainto_tsquery: see rationale
|
|
249
|
-
|
|
319
|
+
-- plainto_tsquery + 28I AND-first/OR-fallback: see the rationale comments
|
|
320
|
+
-- in cerefox_hybrid_search above.
|
|
321
|
+
query_fts_and tsquery := plainto_tsquery('english', p_query_text);
|
|
322
|
+
query_fts_or tsquery := NULL;
|
|
323
|
+
query_fts tsquery;
|
|
324
|
+
tok TEXT;
|
|
325
|
+
tok_q tsquery;
|
|
326
|
+
and_matches BOOLEAN := FALSE;
|
|
250
327
|
BEGIN
|
|
328
|
+
FOR tok IN SELECT unnest(regexp_split_to_array(trim(p_query_text), '\s+')) LOOP
|
|
329
|
+
tok_q := plainto_tsquery('english', tok);
|
|
330
|
+
IF numnode(tok_q) > 0 THEN
|
|
331
|
+
query_fts_or := CASE WHEN query_fts_or IS NULL
|
|
332
|
+
THEN tok_q ELSE query_fts_or || tok_q END;
|
|
333
|
+
END IF;
|
|
334
|
+
END LOOP;
|
|
335
|
+
|
|
336
|
+
IF numnode(query_fts_and) > 0 THEN
|
|
337
|
+
SELECT EXISTS (
|
|
338
|
+
SELECT 1
|
|
339
|
+
FROM cerefox_chunks c
|
|
340
|
+
JOIN cerefox_documents d ON c.document_id = d.id
|
|
341
|
+
WHERE c.version_id IS NULL
|
|
342
|
+
AND d.deleted_at IS NULL
|
|
343
|
+
AND c.fts @@ query_fts_and
|
|
344
|
+
AND (p_project_id IS NULL OR EXISTS (
|
|
345
|
+
SELECT 1 FROM cerefox_document_projects dp
|
|
346
|
+
WHERE dp.document_id = d.id AND dp.project_id = p_project_id
|
|
347
|
+
))
|
|
348
|
+
AND (p_metadata_filter IS NULL OR d.metadata @> p_metadata_filter)
|
|
349
|
+
) INTO and_matches;
|
|
350
|
+
END IF;
|
|
351
|
+
|
|
352
|
+
query_fts := CASE WHEN and_matches THEN query_fts_and
|
|
353
|
+
ELSE COALESCE(query_fts_or, query_fts_and) END;
|
|
354
|
+
|
|
251
355
|
RETURN QUERY
|
|
252
356
|
SELECT
|
|
253
357
|
c.id AS chunk_id,
|
|
@@ -602,7 +706,10 @@ RETURNS TABLE (
|
|
|
602
706
|
is_partial BOOL,
|
|
603
707
|
-- Optimistic-concurrency token (iter-32): the document's current
|
|
604
708
|
-- content_hash, to pass back as expected_content_hash on update.
|
|
605
|
-
content_hash TEXT
|
|
709
|
+
content_hash TEXT,
|
|
710
|
+
-- 28I: TRUE when this is a below-confidence fallback response (nothing
|
|
711
|
+
-- cleared the hybrid pass-filter). See cerefox_hybrid_search.
|
|
712
|
+
below_confidence BOOLEAN
|
|
606
713
|
)
|
|
607
714
|
LANGUAGE sql
|
|
608
715
|
SECURITY DEFINER
|
|
@@ -635,6 +742,7 @@ AS $$
|
|
|
635
742
|
cr.doc_project_ids,
|
|
636
743
|
cr.doc_project_names,
|
|
637
744
|
cr.version_count,
|
|
745
|
+
cr.below_confidence,
|
|
638
746
|
d.updated_at AS doc_updated_at,
|
|
639
747
|
d.content_hash
|
|
640
748
|
FROM chunk_results cr
|
|
@@ -720,7 +828,8 @@ AS $$
|
|
|
720
828
|
td.doc_updated_at,
|
|
721
829
|
td.version_count,
|
|
722
830
|
ac.is_partial,
|
|
723
|
-
td.content_hash
|
|
831
|
+
td.content_hash,
|
|
832
|
+
td.below_confidence
|
|
724
833
|
FROM top_docs td
|
|
725
834
|
JOIN doc_sizes ds ON ds.document_id = td.document_id
|
|
726
835
|
JOIN all_content ac ON ac.document_id = td.document_id
|
|
@@ -1776,7 +1885,7 @@ SET search_path = public, pg_catalog
|
|
|
1776
1885
|
AS $$
|
|
1777
1886
|
-- Keep in lockstep with the `@version:` marker in schema.sql (cut_release.ts
|
|
1778
1887
|
-- enforces it). Bump whenever schema.sql OR rpcs.sql changes.
|
|
1779
|
-
SELECT '0.
|
|
1888
|
+
SELECT '0.9.0'::TEXT;
|
|
1780
1889
|
$$;
|
|
1781
1890
|
|
|
1782
1891
|
-- ── cerefox_content_format_stats ─────────────────────────────────────────────
|
|
@@ -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.
|
|
8
|
+
-- @version: 0.9.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 —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cerefox/memory",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Cerefox — 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",
|