Backfill, evaluate, and query the derivation relationships added by the Provenance Chains schema — so Open Brain can answer "show me why I believe X" and "what downstream artifacts cite this atomic thought?"
The Provenance Chains schema adds four columns (derived_from, derivation_method, derivation_layer, supersedes) and helper SQL functions to public.thoughts. This recipe is the operational layer on top:
backfill.mjs— One-time script to mark existing derived artifacts (weekly digests, wikis, lint reports, etc.) asderivation_layer='derived'and, where the saved artifact exposes thought IDs, populatederived_from.eval.mjs— Nightly (or on-demand) grader that scores each derived thought on existence / relevance / sufficiency using an LLM, and writes the scores into the thought's metadata so dashboards can surface low-quality chains. Three grader backends:openrouter(hosted, default),stdin(manual), andqueue(emit prompts → another worker grades → apply back).mcp-tools.ts— Two MCP tool handler snippets (trace_provenance,find_derivatives) to drop into youropen-brain-mcpEdge Function. They wrap the SQL helpers and handle redaction of restricted ancestors, cycle tolerance, and node caps.
Together, the schema + this recipe turn a flat thoughts table into a directed provenance graph that any Claude/GPT client can query through MCP.
- Working Open Brain setup (guide)
- Provenance Chains schema applied (adds the columns and helper SQL functions this recipe depends on)
- Node.js 18+
- Optional: an OpenRouter API key for the
openroutergrader ineval.mjs. You can use--grader stdinor--grader queueinstead if you prefer not to pay per-call grading costs. - Deno-style Supabase Edge Function runtime if you install the MCP tool handlers
PROVENANCE CHAINS PIPELINE -- CREDENTIAL TRACKER
--------------------------------------
FROM YOUR OPEN BRAIN SETUP
Supabase URL: ____________ (SUPABASE_URL)
Service role key: ____________ (SUPABASE_SERVICE_ROLE_KEY)
OPTIONAL — FOR OPENROUTER GRADER
OpenRouter API key: ____________ (OPENROUTER_API_KEY)
Grader model override: ____________ (e.g., anthropic/claude-3.5-haiku)
Legacy names (still accepted with a deprecation warning):
OPEN_BRAIN_URL, OPEN_BRAIN_SERVICE_KEY
--------------------------------------
- Install the Provenance Chains schema first. The recipe will fail with
column "derivation_layer" does not existotherwise.
-
Export your Supabase credentials (or put them in a
.envfile your shell sources). The canonical names match every other OB1 recipe:export SUPABASE_URL="https://<project-ref>.supabase.co" export SUPABASE_SERVICE_ROLE_KEY="<service_role key>" # Optional: export OPENROUTER_API_KEY="<openrouter key>"
The legacy pair
OPEN_BRAIN_URL/OPEN_BRAIN_SERVICE_KEYstill works — the scripts accept either and print a one-time deprecation warning when they see the legacy names.ANTHROPIC_API_KEYis also accepted as a fallback forOPENROUTER_API_KEYif your setup aliases the Anthropic key to an OpenRouter one.[!CAUTION] The service key must be your service_role key, not the anon key. The backfill/eval scripts need to PATCH the
thoughtstable, which is protected by RLS for non-service callers.
-
Preview what the backfill would change. The default pattern matches any
source_typeending in_pointer; adjust--patternsif your recipes use different suffixes like_digestor_summary:node backfill.mjs --dry-run --patterns _pointer,_digest,_summary
The output reports each candidate row, whether an artifact file was found on disk, and how many parent IDs were parseable.
-
When the dry-run output looks right, run it for real. If your artifacts live in a specific directory, pass
--root:node backfill.mjs --patterns _pointer,_digest,_summary --root ./artifacts
The script is idempotent — re-runs skip rows already marked
derivation_layer='derived'. Pass--forceif you need to re-process (e.g., after regenerating artifacts).The metadata mirror (
metadata.provenance) is written via themerge_thought_provenance_metadataRPC, which performs the merge in a single server-sideUPDATE. This is race-free against concurrent writers likeeval.mjs— no stale JS snapshot ofmetadatais ever replayed back.
-
Score the backfilled chains. For a quick smoke test with the default OpenRouter grader:
node eval.mjs --limit 3
For a scripted nightly run without API cost, pipe JSON scores in via stdin, or use queue mode:
# Queue mode: emit prompts, let a separate worker grade, then apply. node eval.mjs --grader queue --limit 20 --out tmp/prompts.jsonl # (grade tmp/prompts.jsonl externally, writing one JSON score per line to tmp/scores.jsonl) node eval.mjs --apply-scores tmp/scores.jsonl
Scores land on the derived thought's
metadataundereval_score,eval_dimensions,eval_rationale,eval_graded_at, andeval_grader. A low composite score (e.g.,< 3) is a good prompt to regenerate the artifact.
-
Open your
open-brain-mcpserver (in this repo,server/index.ts; in a deployed Supabase copy, usuallysupabase/functions/open-brain-mcp/index.ts) and paste the twoserver.registerTool(...)blocks frommcp-tools.tsalongside your other tool registrations.Deploy the function:
supabase functions deploy open-brain-mcp
In Claude Desktop, open your Open Brain connector — you should now see
trace_provenanceandfind_derivativesin the tool list.
After the full pipeline:
- Every derived artifact row has
derivation_layer = 'derived'andderivation_method = 'synthesis'at the top level. - The same fields (plus
backfilled_at,backfill_reason, and, when parsed,derived_from) are mirrored intometadata.provenanceso the canonicalupsert_thoughtRPC — which only preserves the metadata blob oncontent_fingerprintconflicts — round-trips provenance if the row is ever re-upserted. - Rows whose artifact files expose thought IDs have a non-empty top-level
derived_fromarray. - Derived rows have
metadata.eval_scoreand the threeeval_dimensionsset. - Two new MCP tools are live:
trace_provenance(thought_id, depth?)andfind_derivatives(thought_id, limit?). Restricted-tier rows are always filtered out byfind_derivativesat the SQL layer — there is no caller-visible override. - Claude can now answer questions like:
- "Show me the sources that feed into my March weekly digest."
- "What wikis or digests cite the thought about the bug I fixed on Friday?"
- "Is this digest actually supported by its cited thoughts, or is the model overreaching?"
This recipe assumes public.thoughts.id is a UUID (the canonical Open Brain setup). If you have customized your schema to a BIGINT primary key:
- In
mcp-tools.ts, change thez.string().uuid()input schemas toz.number().int().positive()and remove the UUID casts. - In
backfill.mjs, integer-style#123references raise a hard error on the canonical UUID install — the script logsparse error: refusing to write N integer ref(s) ...and continues. That row is still flipped toderivation_layer='derived'but without anyderived_from, so operators can fix the artifact and re-run with--force. Nothing corrupt reaches PostgREST. Users on a BIGINT fork must skip this recipe's backfill and repopulatederived_fromthemselves (or edit theparseParentIdshelper to emit the integers uncasted). Mixing UUID and integer elements in onederived_fromarray also breaks the GIN containment index; keep one ID shape per array. - In
eval.mjs, PostgRESTin.(…)accepts either shape without changes.
See the schema README's ID Type Note for the SQL-side adjustments.
To grade new derived thoughts every night, create a small wrapper that sources your env and runs eval.mjs:
#!/usr/bin/env bash
set -euo pipefail
source /opt/open-brain/.env
cd /opt/open-brain/recipes/provenance-chains
node eval.mjs --grader openrouter --limit 50 --report ./reports/eval-$(date +%F).mdDrop it in cron or a systemd timer. For a weekly deep-dive on low scorers, add --force and filter by score in your dashboard.
Backfill performs two server writes per row: a column PATCH (top-level
provenance fields like derivation_layer, derivation_method, and
derived_from) followed by an RPC call (merge_thought_provenance_metadata,
which merges into metadata.provenance). Three terminal states per row
are tracked in the summary counters, and they map 1:1 to exit codes:
| Counter | What it means | Exit trigger |
|---|---|---|
errors |
Hard failure — HTTP/transport error or RPC exception on a row that otherwise parsed cleanly. | exit 2 |
halfMigrated |
Column PATCH succeeded but the metadata merge RPC failed (transient, cache lag, permission). | exit 1 |
parseErrors |
parseParentIds threw (e.g., integer refs on a UUID install). Row flipped to derived with no parents. |
exit 1 |
deletedDuringBackfill |
The row vanished between the candidate GET and the PATCH — PATCH matched zero rows. | no effect |
Half-migrated rows leave the top-level columns set but metadata.provenance
missing. Default reruns skip rows that already have derivation_layer='derived',
so these half-migrated rows will not self-heal. To repair, run:
node backfill.mjs --forcewhich re-processes all candidate rows regardless of current state. Safe
because both writes are idempotent: the column PATCH writes the same values
it wrote last time, and the RPC merge is a server-side metadata = metadata || …
concat so running it twice produces the same blob.
deletedDuringBackfill is distinct from halfMigrated: under
Prefer: return=representation,count=exact the PATCH reports a zero-row
result when the thought was deleted by a concurrent process between
backfill's candidate fetch and its PATCH. There is no row left to repair,
so --force cannot resurrect it and this case is neutral info — it does
NOT trigger a non-zero exit.
Exit code summary: 2 for hard errors, 1 for half-migrated rows or parse
errors, 0 for clean completion (including concurrent-delete cases).
Re-run with --force to repair half-migrations. Parse errors require fixing
the artifact (or the INT_REF_RE policy) first, then --force.
Issue: [backfill] GET thoughts: 400 "column \"derivation_layer\" does not exist"
Solution: Apply the Provenance Chains schema first. This recipe depends on the columns and helper functions it installs.
Issue: Backfill patches rows but derived_from stays NULL
Solution: Most artifact files don't print thought IDs in a machine-parseable way. The script will still mark the row derived (so trace_provenance knows it's regenerable) and still mirror provenance into metadata.provenance for durability, but without a derived_from array the row appears as a synthesis with no known parents. For new artifacts, write the parent ID list into metadata.provenance.derived_from at generation time so upsert_thought (which only persists the metadata blob on content_fingerprint conflicts) keeps it across re-captures; a companion recipe or migration can then promote metadata.provenance.derived_from → the top-level derived_from column for fast GIN lookup.
Issue: eval.mjs "grader returned no valid score"
Solution: The model returned something other than strict JSON. Try a stronger/temperature-0 model via --model anthropic/claude-3.5-sonnet, or use --grader stdin to inspect the prompt and craft the JSON yourself.
Issue: find_derivatives returns no rows even after backfill
Solution: Usually an ID-type mismatch. The default SQL function builds the needle as jsonb_build_array(p_thought_id::text) (JSON string UUID). If your derived_from stored JSON numbers (integer IDs), the GIN containment won't match. See the schema README's ID Type Note for the fix.
Issue: trace_provenance returns only the root node
Solution: The root thought has derived_from = NULL or an empty array. That means it's a primary (atomic) thought, which is correct — primaries have no ancestors. Confirm with SELECT derived_from, derivation_layer FROM thoughts WHERE id = '<uuid>';.
Issue: OpenRouter returns 401 / 402
Solution: Check that OPENROUTER_API_KEY is exported in the shell running eval.mjs. 402 means your account is out of credits. Use --grader stdin or --grader queue as a free fallback.
Q: Does this replace metadata-based provenance like metadata.sources_seen?
A: No — they serve different purposes. sources_seen tracks where a thought came in from (Slack, email, etc). derived_from tracks which thoughts were synthesized into this one. Both can coexist on the same row.
Q: What about sensitivity? Can a derived thought leak a restricted parent?
A: trace_provenance redacts restricted ancestors (content → NULL, restricted=true flag set on the node). find_derivatives hardcodes the restricted filter at the SQL layer — the MCP tool exposes no caller-visible override. If you need an admin path that returns restricted rows, wire a separate service-role-only RPC in a private recipe. Sensitivity is enforced at the SQL layer, not just the MCP layer.
Q: Why JSONB array instead of a thought_parents join table?
A: One row per derived thought is simpler for the common query pattern (walk upward) and for recipes to write. The GIN index makes reverse lookup (derived_from @> '[...]') fast enough for the sizes Open Brain targets (100K–1M thoughts). Swap to a join table later if provenance becomes a hot UI path.
Q: How does this interact with upsert_thought deduplication?
A: upsert_thought deduplicates by content_fingerprint. If you re-capture a derived artifact with the same content, the fingerprint matches the existing row and no new thought is created. If you want to track "this digest regenerated on a new day" as a fresh row, change the content (e.g., prepend a timestamp) and set supersedes to the prior row's ID.