Skip to content

Latest commit

 

History

History
227 lines (156 loc) · 15.3 KB

File metadata and controls

227 lines (156 loc) · 15.3 KB

Provenance Chains Pipeline

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?"

What It Does

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:

  1. backfill.mjs — One-time script to mark existing derived artifacts (weekly digests, wikis, lint reports, etc.) as derivation_layer='derived' and, where the saved artifact exposes thought IDs, populate derived_from.
  2. 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), and queue (emit prompts → another worker grades → apply back).
  3. mcp-tools.ts — Two MCP tool handler snippets (trace_provenance, find_derivatives) to drop into your open-brain-mcp Edge 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.

Prerequisites

  • 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 openrouter grader in eval.mjs. You can use --grader stdin or --grader queue instead if you prefer not to pay per-call grading costs.
  • Deno-style Supabase Edge Function runtime if you install the MCP tool handlers

Credential Tracker

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
--------------------------------------

Steps

Step 1

  1. Install the Provenance Chains schema first. The recipe will fail with column "derivation_layer" does not exist otherwise.

Step 2

  1. Export your Supabase credentials (or put them in a .env file 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_KEY still works — the scripts accept either and print a one-time deprecation warning when they see the legacy names. ANTHROPIC_API_KEY is also accepted as a fallback for OPENROUTER_API_KEY if 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 thoughts table, which is protected by RLS for non-service callers.

Step 3

  1. Preview what the backfill would change. The default pattern matches any source_type ending in _pointer; adjust --patterns if your recipes use different suffixes like _digest or _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.

Step 4

  1. 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 --force if you need to re-process (e.g., after regenerating artifacts).

    The metadata mirror (metadata.provenance) is written via the merge_thought_provenance_metadata RPC, which performs the merge in a single server-side UPDATE. This is race-free against concurrent writers like eval.mjs — no stale JS snapshot of metadata is ever replayed back.

Step 5

  1. 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 metadata under eval_score, eval_dimensions, eval_rationale, eval_graded_at, and eval_grader. A low composite score (e.g., < 3) is a good prompt to regenerate the artifact.

Step 6

  1. Open your open-brain-mcp server (in this repo, server/index.ts; in a deployed Supabase copy, usually supabase/functions/open-brain-mcp/index.ts) and paste the two server.registerTool(...) blocks from mcp-tools.ts alongside 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_provenance and find_derivatives in the tool list.

Expected Outcome

After the full pipeline:

  • Every derived artifact row has derivation_layer = 'derived' and derivation_method = 'synthesis' at the top level.
  • The same fields (plus backfilled_at, backfill_reason, and, when parsed, derived_from) are mirrored into metadata.provenance so the canonical upsert_thought RPC — which only preserves the metadata blob on content_fingerprint conflicts — round-trips provenance if the row is ever re-upserted.
  • Rows whose artifact files expose thought IDs have a non-empty top-level derived_from array.
  • Derived rows have metadata.eval_score and the three eval_dimensions set.
  • Two new MCP tools are live: trace_provenance(thought_id, depth?) and find_derivatives(thought_id, limit?). Restricted-tier rows are always filtered out by find_derivatives at 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?"

ID Type Note

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 the z.string().uuid() input schemas to z.number().int().positive() and remove the UUID casts.
  • In backfill.mjs, integer-style #123 references raise a hard error on the canonical UUID install — the script logs parse error: refusing to write N integer ref(s) ... and continues. That row is still flipped to derivation_layer='derived' but without any derived_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 repopulate derived_from themselves (or edit the parseParentIds helper to emit the integers uncasted). Mixing UUID and integer elements in one derived_from array also breaks the GIN containment index; keep one ID shape per array.
  • In eval.mjs, PostgREST in.(…) accepts either shape without changes.

See the schema README's ID Type Note for the SQL-side adjustments.

Scheduling the Eval

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).md

Drop it in cron or a systemd timer. For a weekly deep-dive on low scorers, add --force and filter by score in your dashboard.

Recovery from interrupted backfill

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 --force

which 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.

Troubleshooting

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.

FAQ

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.