Assay Labs
Local-firstTry it
/docs  ·  PRODUCT REFERENCE2026-04-20

What is Assay?

Heads up: this page describes an earlier version of Assay. For the current plugin, install steps, and code, read the README on GitHub.

Local-first decision memory for PMs. Every call you commit to with Claude gets captured as a typed, queryable decision with reasoning attached, stored on your laptop. No account, no cloud database, no API key required. Your PRDs, research, and decisions stay on your laptop — backed up with cp, moved with cp.

Local-first2026-05-14
EXAMPLE — what /assay-decision returns
You (in Claude):
  /assay-decision why did we kill the bulk-edit feature

Assay:
  DECISION (2026-04-12, confirmed)
  We killed bulk-edit before launch because the cross-tenant data risk
  outweighed the cycle-time gain. 3 of 4 design reviewers (Sasha, Min,
  R.) blocked on the audit-log gap. Superseded the "ship bulk-edit Q3"
  plan from 2026-02-09.

  EVIDENCE
  • Security review notes — 2026-04-10
  • Customer interviews #14, #17 — Jan 2026
  • PRD v2.3 §6.2 — 2026-03-22

  See more: /assay-decision <id> --expand

Reference, not pitch. That example is what you came to see. Everything below is the engineering reference. For high-level questions about why Assay exists and who it's for, see the landing-page FAQ.

Assay turns your PRDs, strategy documents, research notes, and recorded decisions into a structured set of cited evidence that your AI tools can query. Instead of relying on stale memory or keyword search, every question and proposal gets checked against what your organization actually knows — with source links, citation tokens, and explicit refusal when the evidence is insufficient.

This page describes the local versionof Assay. Everything — the indexed documents, the embeddings, the full-text index, the retrieval engine — lives on your machine in a single SQLite file at ~/.assay/assay.db. It plugs into Claude Code as an MCP server and exposes a set of slash commands (/assay-scan, /assay-decision, /assay-retrieve, /assay-stress-test, /assay-config, /assay-sync-status) plus a CLI. The full underlying MCP tool surface is documented in Under the hood.

Silo doctrine. Hook activation is project-scoped — Assay only fires when your cwd is the silo’d repo. Data at ~/.assay/assay.db is user-scoped — one memory across all your silos. This split is intentional: silos isolate when Assay listens, not what it remembers.

Prerequisites

  • Node.js ≥20 (LTS recommended). Required — the MCP server and CLI are Node processes.
  • git — to clone the repo.
  • Claude Code (desktop app) — the MCP client that surfaces the slash commands. Any MCP-capable client works; Claude Code is the one we test against.
  • ~600 MB free disk — the embedding model (one-time ~420 MB download) plus your corpus.
  • macOS or Linux. Windows is untested.

Embedding: local embedding model. No API key required.

Under the hood

Technical detail below. PMs can skip to Installation.

Internally, Assay plugs into Claude Code as an MCP server and exposes ten MCP tools plus a CLI. The slash commands above are thin wrappers over those tools. Architecture diagram:

      ┌────────────────────────────────────────────────────────┐
      │                    Claude Code                          │
      │   retrieve  scan  stress_test  configure                │
      │   assay_decision_recall  assay_decision_expand          │
      │   assay_recall  brief_decision_render                   │
      │   promote_claim  demote_claim                           │
      └──────────────────────────┬─────────────────────────────┘
                                 │  MCP (stdio)
                                 ▼
      ┌────────────────────────────────────────────────────────┐
      │                   Assay MCP server                      │
      │                                                         │
      │    Ingest ──► Chunk ──► Embed ──► Store ──► Retrieve    │
      │      │           │         │         │          │       │
      │      ▼           ▼         ▼         ▼          ▼       │
      │   Markdown     512-tok   local      SQLite   hybrid     │
      │   (Notion      heading-  embedding  single   keyword    │
      │   optional)    aware     model      file     + semantic │
      │                                              search     │
      └────────────────────────────┬───────────────────────────┘
                                   │
                                   ▼
                        ~/.assay/assay.db  (single file)

MCP server tool names

The slash command is what a PM types in Claude. The MCP server tool name is the underlying tool the MCP client invokes. They map 1:1 for the user-facing surface; the rest are internal recall and capture primitives.

Slash commandMCP server tool
/assay-scanscan
/assay-decisionassay_decision_recall
/assay-decision <id> --expandassay_decision_expand
/assay-retrieveretrieve
/assay-stress-teststress_test
/assay-configconfigure
/assay-sync-statusconfigure subcommand="status"
(internal — cascade recall)assay_recall
(internal — brief synthesis deposit)brief_decision_render
(internal — decision lifecycle)promote_claim / demote_claim

Installation

Heads up: this page describes an earlier version of Assay. For the current plugin, install steps, and code, read the README on GitHub.

Five minutes from clone to first cited answer.

5-minute install

Local install

Runs entirely on your machine as a single SQLite file at ~/.assay/assay.db. No Docker, no account. Uses a local embedding model. No API key required.

Install: bash scripts/install-tester.sh — project-scoped, never modifies ~/.claude/settings.json, never installs ECC, never clones everything-claude-code. ECC is a separate global tool you install independently; Assay does not bundle or wire it.

First command to try after install: /assay-scan— fast pre-flight signal check against whatever's already in your decision graph and corpus.

Prerequisites

  • Node.js ≥20
  • Claude Code installed (for the MCP integration)
  • macOS or Linux (Windows untested)

1. Clone + install

git clone https://github.com/levievanshantz/assay.git
cd assaylabs
npm install

2. Bootstrap the SQLite schema

node scripts/migrate.mjs

Idempotent. Creates ~/.assay/assay.db, applies the base schema, and walks every migration in order. Postgres-era migrations (versions 000–022) are auto-skipped on SQLite. Re-run any time — already-applied migrations stay quiet.

3. Index a folder of markdown

./node_modules/.bin/tsx bin/assay.ts add ~/my-prds --name prds
./node_modules/.bin/tsx bin/assay.ts index
./node_modules/.bin/tsx bin/assay.ts status

First index downloads the local embedding model (~420MB, one-time). A 50-PRD folder finishes in under 60 seconds.

4. Wire up Claude Desktop via the launcher

The repo ships with a launcher script at ~/.local/bin/assay-mcp that auto-discovers the built MCP server and decouples your Claude Desktop config from the repo path. Recommended over hard-coding the dist path. Add this assay entry to mcpServers in claude_desktop_config.json:

"assay": {
  "command": "/Users/YOU/.local/bin/assay-mcp",
  "args": [],
  "env": {
    "ASSAY_DB": "sqlite",
    "ASSAY_DB_PATH": "/Users/YOU/.assay/assay.db"
  }
}

Restart Claude Desktop. Test with /assay-retrieve. Verify with ~/.local/bin/assay-mcp.doctor— it checks the launcher, the config pointer, server boot, SQLite reachability, and recent log cleanliness in one pass. Run it any time Claude Desktop says “assay disconnected.”

Direct (no launcher) wiring still works too — point command at node with args: ["/abs/path/mcp-server/dist/index.js"].

5. Live-update (optional)

./node_modules/.bin/tsx bin/assay.ts watch

Known limitations: Single-machine only; no team sharing today. A shared institutional tier on Postgres + pgvector is speculative future work, not committed yet. If a query gives an obviously wrong top result, file it with the GitHub issue template; ranking tuning is an open work item.

Quick Start

See value in 5 minutes.

1. Seed the Demo Corpus

Assay ships with a small demo corpus — a handful of product strategy documents, research summaries, and decision records — so you can explore retrieval and stress-testing without connecting any real data sources first.

npm run seed-demo

This inserts roughly 40 evidence records and 120 extracted decisions into your local database. It takes about 30 seconds while embeddings are generated.

2. Configure Claude Code

Register Assay as an MCP server so Claude Code can call its tools. Add the following to your project’s .claude/settings.local.json:

{
  "mcpServers": {
    "assay": {
      "command": "node",
      "args": ["/absolute/path/to/assay/dist/index.js"],
      "env": {
        "ASSAY_DB": "sqlite",
        "ASSAY_DB_PATH": "/Users/YOU/.assay/assay.db"
      }
    }
  }
}

Tip: Replace /absolute/path/to/assay with the actual directory where you cloned the repository. The path must be absolute — Claude Code does not resolve relative paths for MCP servers.

3. Test It

Open Claude Code in any project directory and try the following prompts:

Run a pre-flight check:

Run scan

Get a briefing on a topic:

retrieve "customer feedback" mode="brief"

What to Expect

The scantool returns a fast pre-flight signal check (3–5 signals, classed as clear / caution / blocker) over a fused candidate pool: decision-graph hits at cosine ≥0.65 plus hybrid corpus retrieval. If you want corpus health instead (sync freshness, record counts, drift), call configure subcommand="status".

The retrieve tool in brief mode returns a structured summary of what your organization already knows about the given topic. It includes:

  • Context summary — a narrative overview of the topic drawn from your evidence
  • Prior work — existing decisions, research, and documents that address the topic
  • Active constraints — known blockers, limitations, and guardrails
  • Unresolved debates — open questions and contradictions found across sources
  • Open questions — gaps the team has not yet addressed

Next Steps

How It Works

The five-step pipeline from source document to retrievable intelligence.

Ingestion Pipeline

1. Ingest

Source pages are fetched from Notion or Confluence via their APIs. Each page's content is converted to a normalized Markdown representation, preserving heading structure.

2. Chunk

The Markdown is split at heading boundaries into sections of approximately 3,200 characters each. These become your evidence records — the atomic units of document-level storage.

3. Embed

Each evidence section is embedded into a dense vector using a local embedding model that runs in-process via @huggingface/transformers (no API key required). Vectors are stored alongside the rest of the corpus in the same SQLite file.

4. Index for full-text

Each section is also indexed for keyword search so retrieval can combine semantic similarity with exact lexical matching.

5. Retrieve

At query time, four parallel search lanes fire (keyword + semantic across both evidence sections and individual decisions) and their results merge into a single ranked list. Decisions resolve back to their parent evidence records via a source_id foreign key, so you always get full context.

4-Layer Hybrid Retrieval

Each query runs four search lanes in parallel:

  1. Evidence semantic search — cosine similarity against evidence record embeddings. Catches semantically related sections.
  2. Decision semantic search — cosine similarity against individual decision embeddings. Catches atomic assertions that point in a different semantic direction than their parent section.
  3. Evidence keyword search — built-in SQLite full-text search. Catches exact terminology and proper nouns that semantic search may miss.
  4. Decision keyword search — keyword search against decision text. Catches specific terms used in individual assertions.

Results from all four lanes are merged into a single ranked list using hybrid (keyword + semantic) fusion. Decisions resolve back to their parent evidence records via the source_id foreign key, so the final result set contains full-context sections rather than isolated sentences.

Index integrity — the decision keyword index stays in lockstep with the decision table via AFTER triggers (insert / update / delete) shipped in migration 032. Without those triggers, freshly drained decisions were invisible to keyword retrieval until a manual backfill. Now every write keeps both indexes synchronised.

Evidence & Decisions

The two-layer data model that powers granular retrieval. (The decision layer is internally still the claims table for code continuity.)

Evidence Records

An evidence record is a section produced by the two-pass break-point chunker (target ~512 tokens per chunk, headings used as natural breakpoints, oversized sections sub-split along sentence boundaries with heading-context preservation). Each record stores the raw text, a dense embedding vector (local embedding model; no API key required), source metadata (page URL, title, heading path), and a content hash for drift detection and delete/rename tombstone retirement.

Evidence records are the primary unit of retrieval. They provide the full context window that tools like retrieve (in brief mode) and stress_test synthesize from. Every decision links back to exactly one evidence record.

Decisions

A decision is an atomic assertion extracted from an evidence record or a Claude session — a single statement that can be independently verified, contradicted, or cited. Each decision gets its own embedding vector, which may point in a semantically different direction than its parent section. This is what enables retrieval of individual assertions that chunk-level averaging would lose.

On average, decision extraction adds 10.7 additional unique records per query, surfacing evidence that document-level search alone would miss.

Decision Types

Every decision is classified into one of seven types:

TypeDescription
findingAn observed fact or data point from research
recommendationA suggested course of action
assumptionA belief taken as given without direct evidence
metricA quantitative measurement or target
constraintA limitation, blocker, or guardrail
commitmentA promise or agreed-upon deliverable
deferralA decision explicitly postponed to a later date

Epistemic Layers

Each decision also carries an epistemic layer classification:

  • Observation — a factual report of what was seen or measured
  • Interpretation — an analysis or conclusion drawn from observations
  • Intention — a stated plan, goal, or commitment to act

Decisions & Ambient Tagging

A first-class layer for material commitments. Capture, recall, and walk the chain of why something got decided.

What a decision is

A decision is an atomic, candidate-status row that names a real choice the team has committed to, with a reasoning trail and cited evidence. Decisions live in the same physical store as claims (column decision_kind IS NOT NULL) but are a distinct surface: they have their own status lifecycle (candidate → tentative → confirmed), their own provenance fields, and an append-only claim_evidence audit trail so an evidence-set change appends a new row instead of mutating the decision.

Recall returns lightweight summaries; expand pulls the full reasoning trail, current evidence list, and the predecessor chain on demand. This semantic-pointer pattern keeps Claude's context lean while preserving the full record one tool call away.

Three capture paths

  • Structural — when retrieve mode=brief runs, the caller's LLM produces strict JSON that brief_decision_rendervalidates, renders, and deposits as candidate decisions. Zero API key required server-side; the caller's model does the synthesis.
  • Ambient (session drain) — Claude emits structured <decision> tags inline at commit moments. The assay drain CLI extracts them from the session JSONL via regex, dedupes by content hash, embeds the statement, and writes the row. No LLM call in the forward path.
  • T0 historical backfill — a one-time Phi-4 14B pass over the existing corpus seeds the decision graph from prior PRDs and research notes. Idempotent via content hash, so re-running is safe.

Tag format

When Claude commits to a material decision in conversation, it emits a structured tag that the drain parser picks up:

Eng shape — schema / tooling / policy / rollout / eval / scope commitments:

<decision impact="schema|tooling|policy|rollout|eval|scope"
          confidence="moderate|low"
          layer="observation|interpretation|decision">
  <statement>{<=200 chars; specific real subject}</statement>
  <reasoning>{<=400 chars; why this was decided}</reasoning>
</decision>

PM shape— product hypotheses with explicit problem / hypothesis / metric / outcome attributes:

<decision kind="pm"
          problem="testers abandon install at step 3 (Claude Desktop wiring)"
          hypothesis="auto-generating the mcpServers JSON block cuts step-3 drop-off in half"
          metric="install-completion rate among new testers, week-over-week"
          outcome="ship the auto-config writer behind --auto-wire before next tester cohort">
  <statement>Auto-write the Claude Desktop MCP block during install-tester.sh.</statement>
  <reasoning>Step-3 (manual JSON edit) is the highest-friction step in the install flow; a generated block removes a class of typo errors and makes the 5-minute promise real.</reasoning>
</decision>

Both child tags (<statement> and <reasoning>) are required — missing or empty bodies are dropped at drain. For the eng shape, the impact attribute drives downstream filtering. For the PM shape, kind="pm" swaps the attribute set to problem / hypothesis / metric / outcome. The optional layer attribute (default decision) marks the epistemic level — observation for direct factual reports, interpretation for inference and synthesis, decision for committed action. Recall can filter by layer to surface only the committed decisions or trace the observations and interpretations that led there.

Decision graph over time

Every state change on a decision — initial deposit, promotion through the review lifecycle, supersession by a new decision, rejection — appends a row to the decision_transitions audit table. Each row records the actor agent (which Claude instance, GPT call, or Levi input), the model id when known, the transition reason, and any evidence record that triggered the change.

assay_decision_expand returns the full transition history alongside a depth-N predecessor walk (supersedes_chain) so callers can reconstruct exactly how a decision evolved across a quarter and which model touched it at every step. Cycle-guarded with a 10-hop ceiling to keep the response bounded.

Recall ranks with a hybrid score — 0.7 cosine + 0.2 recency + 0.1 frequency (90-day half-life on recency, log-normalized citation count for frequency). Cosine remains the floor and the tiebreaker, so semantically weak matches never get resurrected by recency alone. Pure cosine is one env flag away (ASSAY_RECALL_HYBRID=0) for kill-switch revert.

Pass includeSuperseded=trueto recall when you want the “what did we change our minds about” surface — the response then carries a sidecar of recently deprecated decisions tagged with their replacement pointers, capped so the active set still dominates.

Draining a session

Run assay drain at any time to ingest tags from the most recent session JSONL into the local decision ledger.

# Drain the latest session, last 24h
assay drain

# Specific session JSONL
assay drain --session ~/.claude/projects/<slug>/<id>.jsonl

# All sessions in your projects directory
assay drain --all

# Preview without writing
assay drain --dry-run

The drain is idempotent: re-running over the same session produces zero new rows. Each session also gets a sentinel evidence record (source_kind='session') that anchors its decisions for clean provenance, kept out of normal retrieval queries.

The two commands you’ll use first

Most of the value comes from two slash commands. Start here; the full tool surface is documented below for engineering reference.

  • /assay-scan — before you commit to a call, ask Assay what it already knows. You describe the proposal in plain English; Assay returns 3–5 signals classed clear, caution, or blocker, drawing from both prior decisions and the evidence corpus. Use it when you’re about to greenlight, kill, or reverse something and want a sanity check first.
  • /assay-decision — ask “what did we decide about X, and why?” Returns the matching decision summary first; expand any hit to see the full reasoning trail, the evidence cited at the time, and the predecessor chain (what this decision superseded, and what superseded it). This is the “why did we kill that?” answer that previously required an archeology dig across Slack and docs.

Full tool surface

The remaining MCP tools are named recall surfaces (each solves one job) plus a cascade tool for when the caller doesn’t know which layer holds the answer. Engineering reference:

  • /assay-retrieve (server tool: retrieve) — hybrid keyword + semantic search over evidence chunks, in modes raw / guided / evaluate / brief. The corpus layer of the memory hierarchy. Use when you want raw evidence rather than a committed decision.
  • /assay-stress-test (server tool: stress_test) — deliberate adversarial review of a proposal. Surfaces every dissenting decision and conflicting chunk. Use when you’ve already decided and want the strongest argument against.
  • /assay-config (server tool: configure) — view and update extraction mode, retrieval depth, feature toggles, presets.
  • /assay-sync-status (server tool: configure subcommand="status") — read-only corpus health: sync freshness, record counts, drift warnings.
  • Provenance (server tool: assay_decision_expand) — given a decision id, returns the full reasoning trail, the latest cited evidence, the full decision_transitions audit log, and a depth-N predecessor walk (supersedes_chain). Invoked automatically by /assay-decision when you ask it to expand a hit.
  • Cascade (server tool: assay_recall) — tries the decision graph first, falls through to the evidence corpus if no semantic hit, returns insufficient_evidence when both layers come up empty. Every response carries a source_tierbreadcrumb so the caller knows whether it’s reading a structured decision, a raw corpus chunk, or a verdict that nothing in the ledger answers the question. Use when the question is abstract or you don’t know which layer holds the answer.

Supersession semantics — audit-only by default

When a tag includes supersedes="<id-or-prefix>", the drain resolves the predecessor with strict unique-match enforcement. Prefixes must be at least 12 characters; shorter prefixes or ambiguous matches fail the resolver and the new decision lands without the link (failure reason recorded for audit). On success the drain (a) sets superseded_by_claim_id on the new row and (b) appends a supersession-edge-recorded transition row on the predecessor. Default mode is audit-only — the predecessor’s status stays untouched so a wrong tag can’t accidentally hide live knowledge. Set ASSAY_SUPERSEDE_FLIP=1 in the MCP env to opt into the live-flip mode once you trust your tagging discipline.

Health check

assay doctor reports decision-layer state: total decisions, embedding coverage, status breakdown, and a per-source-agent breakdown so you can confirm the drain is landing rows under Claude Opus (or the appropriate model) and the corpus backfill is intact.

Retrieval Pipeline

How queries become ranked evidence.

Every retrieval query fires four parallel search lanes, each capturing a different axis of relevance:

  1. Evidence vector search — finds semantically similar document sections via cosine distance against evidence embeddings.
  2. Decision vector search— finds individual assertions whose embeddings are close to the query, even when the parent section's overall embedding diverges.
  3. Evidence keyword search — built-in SQLite full-text search for exact keyword matches in evidence records.
  4. Decision keyword search — keyword matches against the text of individual decisions.

Why hybrid search

Different search methods catch different things. Semantic search finds meaning-based similarity but can miss exact terminology. Keyword search catches precise terms but misses paraphrased content. Decision search catches individual assertions lost in chunk-level embedding averaging. Hybrid fusion merges ranked results from all four lanes into a single list, weighting results that appear across multiple lanes more heavily.

Configurable K Values

K values (the number of evidence records retrieved) are configurable via environment variables or the configure tool. Defaults: scan=40, stress_test=80, retrieve=20. Brief mode depth depends on setting: quick=5, standard=15, deep=30.

retrieve

Query the corpus with four retrieval modes.

The retrieve tool is the primary interface to the corpus. It runs 4-layer hybrid search and returns results in one of four modes, each suited to a different workflow. Default retrieval depth is K=20 evidence records.

Four Modes

  • raw — returns top-K evidence records with hybrid ranking scores. No LLM call. Fastest and cheapest.
  • guided — returns evidence plus an eval_instructions field. The calling LLM processes the results itself — zero extra API cost.
  • evaluate — the server calls an LLM to synthesize findings and returns a plain-language summary.
  • brief — generates a structured organizational briefing: context summary, prior work, active constraints, unresolved debates, open questions, and dependencies. Each brief deposits its synthesis back into the corpus, creating an accumulation loop.

Brief Output

When using mode="brief", the tool returns:

  • Context summary — narrative overview of the topic from your corpus
  • Prior work — existing decisions, research, and documents
  • Active constraints — known blockers and guardrails
  • Unresolved debates — contradictions found across sources
  • Open questions — gaps the team has not addressed
  • Dependencies — related work and cross-cutting concerns

Examples

Raw retrieval:

retrieve "competitive landscape" mode="raw" top_k=30

Organizational briefing (default K=20):

retrieve "customer onboarding friction" mode="brief"

Retrieval Depth

Default K=20 evidence records. Configurable per call via top_k parameter or globally via environment variables.

stress_test

Stress-test a proposal against organizational evidence.

The stress_test tool takes a proposal or plan and rigorously evaluates it against your corpus. It finds supporting evidence, surfaces contradictions, identifies assumption weaknesses, and delivers a verdict with confidence level.

What It Returns

  • Verdict — one of: ready, ready_with_conditions, blocked, needs_clarification
  • Overlap analysis — where the proposal aligns with existing evidence
  • Conflict analysis — contradictions found in the corpus
  • Assumption weaknesses — unstated or fragile assumptions
  • Evidence gaps — areas where the corpus has no data
  • Supporting evidence — records that back the proposal
  • Alternatives — other approaches suggested by the evidence
  • Falsification check — what evidence would need to be true for the proposal to fail

Example

stress_test "We should move the mobile onboarding flow to a native app"

Retrieval Depth

Default K=80 evidence records. K values are configurable via the configure tool or environment variables.

scan

Fast pre-flight signal check over a fused candidate pool.

The scan tool runs both layers in parallel: pulls decision-graph hits at cosine ≥0.65 plus hybrid corpus retrieval, then returns 3–5 signals classed as clear, caution, or blocker. Use it before committing to a proposal to surface anything the corpus or decision graph already says against (or for) the idea.

For corpus health (sync status, record counts, drift warnings) call configure subcommand="status" instead. The two were once one tool and split when scan absorbed the two-layer fan-out.

What It Returns

  • Header line — `[SCAN MODE] N corpus + M decisions in candidate pool` when fusion succeeds; falls back to corpus-only header when no decisions clear the floor.
  • 3–5 signals — each classed clear / caution / blocker, with citations to source records by id.
  • Decision-graph candidates block — the top decisions matching the proposal, when fusion finds any.
  • Tier errors — surfaces inline if either tier fails (embedding outage, corpus query throw, etc).

Example

scan intent="ship the new pricing tier this quarter"

One required arg: intent. Returns a fused signal report in one call.

Retrieval Depth

Default K=40 evidence records for the internal health check. Configurable via the configure tool or environment variables.

configure

View and update Assay settings at runtime.

The configure tool lets you view and update extraction mode, retrieval depth, feature toggles, and presets. Use it to adjust K values, switch extraction models, toggle dual embedding, or check current settings — all without editing environment files.

What You Can Configure

  • K values — adjust retrieval depth per tool (scan=40, stress_test=80, retrieve=20)
  • Max disclosure depth— cap retrieval at summary, full reasoning, or excerpts. (A future tier returns the original source snapshot at the time the decision was recorded; that snapshot tier isn't built today.)
  • Feature toggles — enable/disable sync, reranker, hygiene passes
  • Presets — apply a preset bundle (minimal, standard, full)
  • Embedding — local embedding model. No API key required; no remote provider switch.

Examples

View current settings:

configure

Change retrieval top-K:

configure top_k=30

Apply a preset:

configure preset="full"

Feature Toggles

Fine-tune which pipeline stages run automatically.

Assay's pipeline stages can be independently enabled or disabled. Configuration is stored in .assay.config.json at the project root.

Available Toggles

ToggleDefaultDescription
syncOnAutomatic drift detection and re-sync of tracked pages
extractionOnClaim extraction from evidence sections during ingestion
accumulationOnBrief and stress_test results deposited back into the corpus
hygieneOffScheduled background sync at a configured time

Enabling / Disabling

# Enable background hygiene
npx assay toggle hygiene on

# Disable claim extraction (evidence-only mode)
npx assay toggle extraction off

# Disable accumulation loop
npx assay toggle accumulation off

Presets

Pre-configured bundles that set all toggles at once.

PresetExtractionSyncAccumulationHygiene
minimalOffOffOffOff
standard (recommended)OnDailyOnOff
fullOn (Claude API)HourlyOnOn

minimal — evidence only. No extraction, no accumulation, no sync. Good for quick evaluation or testing retrieval with pre-existing embeddings.

standard (recommended) — evidence plus extraction with daily sync and accumulation enabled. This is the default configuration after installation. Covers the needs of most teams.

full — evidence plus high-tier Claude extraction (default Haiku 4.5; switch to Sonnet via configure subcommand="extraction" if a corpus warrants it) with hourly sync, accumulation, and background hygiene all enabled. Best for teams with large, actively changing corpora.

Notion Ingestion

Ingest your Notion workspace into the Assay corpus.

Setup

You need a Notion integration token. Create one at notion.so/my-integrations and share the pages you want to ingest with the integration.

# In .env.local:
NOTION_API_KEY=ntn_...

Two-Step Pipeline

Ingestion uses two scripts that run sequentially:

  1. notion-crawl.mjs — calls the Notion API to fetch page content, converts blocks to Markdown, and writes the result to a JSON cache file.
  2. notion-ingest.mjs — reads the JSON cache, chunks at heading boundaries, generates embeddings, and stores evidence records in the local SQLite corpus.

Incremental Mode

By default, the crawl script only re-fetches pages that have changed since the last run (based on Notion's last_edited_time). This makes subsequent ingestion runs fast — typically under 30 seconds for workspaces with few recent edits.

Background Hygiene

Automated sync to keep your corpus fresh without manual intervention.

Background hygiene runs an automated sync at a configurable time. When enabled, it checks all tracked pages for drift and re-processes any that have changed.

Important Notes

  • Default: OFF. Background hygiene is disabled by default. Enable it when you are ready with npx assay toggle hygiene on.
  • Machine must be running. If your laptop is asleep when the scheduled time arrives, the sync fires on next wake.
  • Recommended schedule: daily at a quiet hour (e.g. 3 AM local time) to minimize disruption.

Troubleshooting

Common issues and how to fix them.

IssueCauseFix
/assay-*returns "MCP not connected"Claude Desktop doesn't see the server, or the MCP binary hasn't been builtSettings → Developer → refresh the MCP entry, then restart Claude Desktop. Rebuild with cd mcp-server && npm run build.
Notion crawl returns 0 pagesIntegration disconnected or pages not sharedRe-share pages with the integration at notion.so/my-integrations
First-index stalls on model downloadTransformers.js cannot reach huggingface.co for the initial 420 MB model fetchCheck network. After the first successful download, indexing is fully offline.
Retrieve returns empty resultsCold start — no collection indexed yetRun tsx bin/assay.ts add ~/your-docs and tsx bin/assay.ts index first

Glossary

Key terms used throughout this documentation.

Hybrid search
Combines keyword and semantic search lanes into a single ranked list. Results that appear in multiple lanes are weighted more heavily.
Local embedding model
The embedding model that runs in-process on your machine to turn text into dense vectors. Embeddings live in the same SQLite file as the rest of the corpus. No API key, no network call at query time.
Decisions (internally still the claims table for code continuity)
Atomic assertions and committed choices extracted from documents and conversations, each independently embedded for granular retrieval. A single evidence section may yield 5–15 decisions.
Evidence records
Document sections stored with embeddings in the local SQLite corpus. The primary unit of storage and retrieval context.
Stance signal
A 0.0-1.0 score indicating how strongly a decision carries organizational dissent or constraint.
Tombstoning
Soft-deletion of outdated records. Tombstoned records are marked inactive but not hard-deleted, preserving historical context.
Cosine distance gate
Minimum embedding distance threshold (0.10) used to filter redundant decisions during extraction. Ensures each decision is semantically distinct.
Keyword search
Built on SQLite's full-text search module. Assay indexes every evidence record's title, summary, and body content so exact keyword matches complement semantic similarity search.
Disclosure depth
How much of a decision a retrieval call returns: summary (id, statement, status), full reasoning (the trail the decision was recorded with), or excerpts (citations + supporting evidence). A future tier returns the original source snapshot taken at the time the decision was recorded; that snapshot tier isn't built today.

VERSION · 2026-05-14 · 31/31 INTEGRATION TESTS PASS