Skip to documentation

Module references

Module reference / Measure it

CrucibleEvaluate & measure

Crucible

Golden sets, retrieval metrics, groundedness checks, and significance gates measure the real answer pipeline.
  • Lens

    Exercise the production retrieval and answer path.

  • Warrant

    Gate consequential configuration changes on measured evidence.

  • Lineage

    Record compared configurations, scores, and release verdicts.

Crucible is the measurement system: golden sets of questions with atomic assertions, graded relevance judgments over retrieved evidence, evaluation runs scored against both, and drift snapshots that notice when the corpus underneath moves. It turns "this feels better" into a number, and then into a signed delta with a confidence interval and a p-value, so a configuration change can be accepted or rejected on evidence. Its public API surface is deliberately small and read-mostly: read one run's aggregated metrics, compare two runs with paired significance, and append a relevance judgment.

Crucible evaluation runs and drift

Golden items, qrels, and a config feed a run; per-result rows roll up into the metrics endpoint and the paired comparison. Drift is a separate, deterministic sweep over documents and entities that raises deduped alerts.

Rendering diagram

Downloads

Concepts

  • Modules
  • Crucible

Keywords

  • drift.kb · kb.stale · eval.regression
  • graded relevance 0–3
  • GET {id}/metrics
  • assertions · citations · tags · concepts
  • one execution, golden set pinned by hash
  • one row per item × sample
  • per assertion, human override wins
  • tag = NULL is the overall row
  • paired bootstrap + permutation
  • documents · entities · mentions
  • golden_items
  • eval_configs
  • eval_runs
  • eval_results
  • run_metrics
  • drift_snapshots
  • /evals/runs
  • /evals/runs/compare
  • qrels
  • judgments
  • id
  • GET
  • alerts
Source and generation provenance

Status: current

Generated at: 2026-08-17T18:32:18.102Z

Source hash: 8e5a1126633b4be53704abf4dc048996b0f7bb94f5c49bef1098c4dfbf13e94e

Metadata payload hash: 935a3a0fe634d34f89d4702ee83abc1b59c4177ce89048902eab24fc7d76863a

Canonical appearance

src/app/(docs)/docs/modules/crucible/page.tsx:52 route /docs/modules/crucible

All appearances

  • canonicalsrc/app/(docs)/docs/modules/crucible/page.tsx:52 route /docs/modules/crucible

No mirrored appearances.

Generation versions

App: eli-ai 0.1.0

Mermaid: 11.16.0 · Mermaid CLI: 11.16.0

Node: v26.3.1 · Yarn: 4.17.1

Renderer config hash: 68c10966fe84406ee626034d58bfabd555df9f65f691204b7c46db24038da101

Renderer theme hash: c80287a78d80ad63d27bd5ca348b2ef9a7e2f44da289e436be6484ea28a1b033

Adapter versions: diagramGenerator=2, drawioFlowchart=1, drawioGantt=1, drawioSequence=1, drawioState=1

Full sidecar JSON: crucible-evaluation-runs-and-drift-8e5a1126.json

Use it standalone

Adopting Crucible alone means one package subpath and one API key. Nothing here imports another capability module, and none of the three endpoints requires a document, graph, connector, or agent to be configured — they read rows that already exist.

  1. Mint a key with the right scope

    Reads (getEvalRunMetrics, compareEvalRuns) require kb:read; the routes also accept a legacy runs:read key. Authoring a qrel requires kb:write. For a delegated user key (eli_uk_) the owner must additionally still hold the evals:read capability for the reads and evals:write for the write — the route passes those to the guard, and a revoked capability returns 403 insufficient_capability.
  2. Install one subpath

    @eli-ai/client/quality for headless use, or @eli-ai/react/quality for the components. The React subpath pulls in @eli-ai/react/provider and @eli-ai/react/hooks from the same package and nothing else.
  3. Call it

    Every method takes an optional per-call workspaceId and AbortSignal. Responses are plain JSON contract types — no envelope to unwrap.
installationbash
npm install @eli-ai/client @eli-ai/contracts
# React surfaces only:
npm install @eli-ai/react react
the whole module, headlessts
import { createEliTransport } from "@eli-ai/client/core";
import { createQualityClient } from "@eli-ai/client/quality";

const quality = createQualityClient(
  createEliTransport({
    baseUrl: "https://eli.ai",
    apiKey: () => process.env.ELI_API_KEY!,
    workspaceId: process.env.ELI_WORKSPACE_ID,
  }),
);

// One run's aggregate.
const metrics = await quality.getEvalRunMetrics(runId);
console.log(metrics.status, metrics.resultsCounted, metrics.abstention.abstentionRate);

// Is the candidate config actually better than the baseline?
const compare = await quality.compareEvalRuns({ a: baselineRunId, b: candidateRunId, metric: "ndcgAt10" });
if (compare.significant) {
  console.log("delta", compare.deltaMean, "CI", compare.ci, "p", compare.pValue);
}

// Append a human relevance judgment (0–3) for one golden item / chunk pair.
await quality.authorQrel({ queryId: goldenItemId, chunkId, grade: 3 });

What Crucible alone does not give you over HTTP

The public /api/v1 surface has no operation that creates a golden item, launches an eval run, diffs two runs by assertion flip, captures a drift snapshot, or lists alerts. Those live on the session-authenticated workspace BFF under /api/w/{workspaceId}/evals/*, /api/w/{workspaceId}/golden/*, and /api/w/{workspaceId}/evals/drift, which are guarded by workspace membership and capabilities (for example evals:execute) rather than a bearer key. A standalone adopter reads metrics, compares runs, and appends qrels over the API; runs are produced in the eli.ai workspace or by the scheduled runner.

React components

Four components are exported from @eli-ai/react/quality, together with the EliQualityLabels, EliQualityProps, and EliPolicyDecisionsProps types. They are client components (the file carries "use client") and each builds only a quality client from the provider's transport. Markup uses scoped eli-* classes; import @eli-ai/react/styles.css for the shipped styling, or leave it out and write your own.

EliProvider takes a transport (or an aggregate client, whose transport is retained) plus an optional workspaceId that the components pass as a per-call override. It has no API-key prop by design.

Keys do not belong in the browser

These are browser components, so the transport they receive must not carry an eli_sk_ key. Inject a fetch that calls a same-origin proxy which adds the credential server-side — see the SDK guide for the full reasoning.

EliEvalMetrics

The focused Crucible surface. Given a run id it fetches GET /api/v1/evals/runs/{id}/metrics once and renders: a status badge (succeeded positive, failed critical, anything else warning), a resultsCounted/samplesN samples counted line, a KPI grid with a meter per value, abstention badges, and — when the run stored selective-prediction pairs — an inline SVG risk–coverage curve with one point per answered result.

The KPI grid shows exactly seven values, each rendered as a rounded percentage or when null: accuracy, groundedness, citation precision, citation recall and retrieval hit rate from quality, plus recallAtK and ndcgAt10 from retrieval. The API also returns precisionAtK, mrr, hitAtK, the RAGAS block and the error-class histogram — this component does not draw those, so read them from the endpoint or a hook if you need them.

EliEvalMetrics props

runIdrequiredstringEval run id to load. It is part of the query key, so changing it refetches.
classNamestringAppended to the root element's eli-root class.
titlestringSurface heading. Default: "Evaluation metrics".
descriptionstringSub-heading. Default: "Retrieval, answer quality, and abstention signals for one evaluation run."
loadingLabelstringShown while loading. Default: "Loading evaluation metrics…".
errorLabelstringShown on failure, next to a retry button that refetches. Default: "Evaluation metrics could not be loaded."
onError(error: unknown) => voidCalled with the thrown error when the fetch rejects.
emptyLabelstringAccepted because the props type spreads the shared surface props, but this component never reads it.
EliEvalMetrics with provider wiringtsx
"use client";

import { createEliTransport } from "@eli-ai/client/core";
import { EliProvider } from "@eli-ai/react/provider";
import { EliEvalMetrics } from "@eli-ai/react/quality";
import "@eli-ai/react/styles.css";

// sameOriginProxyFetch hits your own route, which adds the Bearer key.
const transport = createEliTransport({ fetch: sameOriginProxyFetch });

export function RunReport({ runId, workspaceId }: { runId: string; workspaceId: string }) {
  return (
    <EliProvider transport={transport} workspaceId={workspaceId}>
      <EliEvalMetrics runId={runId} onError={(error) => console.error(error)} />
    </EliProvider>
  );
}

EliQuality

The composite dashboard. It renders three sections in one surface: an answer-policy table (verdict, question, surface, confidence as a rounded percentage) with a total-decisions badge, an evaluation section whose form takes a run id and loads that run's metrics on submit, and a semantic-cache KPI grid. Policy decisions, policy stats and cache stats load on mount; the evaluation panel is idle until you submit, and the submit button is disabled while the input is blank or a request is in flight.

Two of those three sections do not read Crucible endpoints — see how it composes below. A single kb:read key covers all three.

EliQuality props

classNamestringAppended to the root element's eli-root class.
titlestringSurface heading; falls back to labels.title ("Quality controls").
descriptionstringSub-heading; falls back to labels.description.
labelsPartial<EliQualityLabels>Overrides for the nine built-in strings: title, description, policy, evaluations, cache, runId, loadRun, noDecisions, noEvaluation.
initialEvalRunIdstringPrefills the run-id input. Default: "". It does not auto-submit — the metrics call still needs the button.
policyLimitnumberlimit sent to the policy-decisions list, and part of that query's key. Default: 8.
loadingLabelstringDefault: "Loading quality controls…".
errorLabelstringShown when any of the three mount-time queries fails; the retry button refetches all three. Default: "Quality controls could not be loaded."
onError(error: unknown) => voidPassed to all three queries and to the evaluation mutation.
onEvalLoaded(metrics: EvalRunMetrics) => voidFired after a successful metrics load, with the parsed EvalRunMetrics.
onPolicyDecisionSelect(decisionId: string) => voidFired when a verdict badge in the policy table is clicked.
EliQuality with provider wiringtsx
"use client";

import { createEliTransport } from "@eli-ai/client/core";
import { EliProvider } from "@eli-ai/react/provider";
import { EliQuality } from "@eli-ai/react/quality";
import "@eli-ai/react/styles.css";

const transport = createEliTransport({ fetch: sameOriginProxyFetch });

export function QualityConsole({ workspaceId, runId }: { workspaceId: string; runId?: string }) {
  return (
    <EliProvider transport={transport} workspaceId={workspaceId}>
      <EliQuality
        initialEvalRunId={runId ?? ""}
        policyLimit={12}
        labels={{ evaluations: "Eval runs" }}
        onEvalLoaded={(metrics) => console.log(metrics.runId, metrics.quality?.groundedness)}
      />
    </EliProvider>
  );
}

EliPolicyDecisions

A standalone table of answer-policy decisions — verdict badge, question, surface, and a formatted createdAt — with optional surface and verdict filters. It reads the append-only decision log, not evals; it is exported from this subpath because it shares the quality client. Verdict badges are tone-mapped: answer and proceed positive, abstain and abstain_with_pointers critical, everything else warning.

EliPolicyDecisions props

classNamestringAppended to the root element's eli-root class.
titlestringDefault: "Answer policy".
descriptionstringDefault: "Review answer, caveat, clarification, and abstention decisions."
limitnumberPage size sent as ?limit. Default: 20.
surface"chat" | "structured" | "agent"Optional ?surface filter; omitted from the request when undefined.
verdict"proceed" | "clarify" | "abstain_with_pointers" | "answer" | "answer_with_caveat" | "abstain"Optional ?verdict filter; omitted when undefined.
loadingLabelstringDefault: "Loading policy decisions…".
errorLabelstringDefault: "Policy decisions could not be loaded."
emptyLabelstringRendered when the page has zero items. Default: "No policy decisions match these filters."
onError(error: unknown) => voidCalled with the thrown error when the fetch rejects.
onDecisionSelect(decisionId: string) => voidFired when a verdict badge is clicked.
EliPolicyDecisions with provider wiringtsx
"use client";

import { createEliTransport } from "@eli-ai/client/core";
import { EliProvider } from "@eli-ai/react/provider";
import { EliPolicyDecisions } from "@eli-ai/react/quality";
import "@eli-ai/react/styles.css";

const transport = createEliTransport({ fetch: sameOriginProxyFetch });

export function Abstentions({ workspaceId }: { workspaceId: string }) {
  return (
    <EliProvider transport={transport} workspaceId={workspaceId}>
      <EliPolicyDecisions
        verdict="abstain"
        surface="chat"
        limit={50}
        onDecisionSelect={(decisionId) => console.log(decisionId)}
      />
    </EliProvider>
  );
}

EliCacheStats

A four-tile KPI grid for semantic-cache health: hit rate (with a meter, or when null), active entries with the embedding-backed count beneath, invalidated entries with the expired count beneath, and the last hit timestamp with the current policy mode (off · shadow · enforce) beneath. It reads GET /api/v1/cache/stats, which is the answer path's cache, not an eval surface.

EliCacheStats props

classNamestringAppended to the root element's eli-root class.
titlestringDefault: "Semantic cache".
descriptionstringDefault: "Reuse, invalidation, and expiry health for grounded answers."
loadingLabelstringDefault: "Loading cache statistics…".
errorLabelstringDefault: "Cache statistics could not be loaded." Rendered with a retry button.
onError(error: unknown) => voidCalled with the thrown error when the fetch rejects.
emptyLabelstringAccepted by the shared surface props type but never read by this component.
EliCacheStats with provider wiringtsx
"use client";

import { createEliTransport } from "@eli-ai/client/core";
import { EliProvider } from "@eli-ai/react/provider";
import { EliCacheStats } from "@eli-ai/react/quality";
import "@eli-ai/react/styles.css";

const transport = createEliTransport({ fetch: sameOriginProxyFetch });

export function CachePanel({ workspaceId }: { workspaceId: string }) {
  return (
    <EliProvider transport={transport} workspaceId={workspaceId}>
      <EliCacheStats />
    </EliProvider>
  );
}

Prefer your own markup?

The same subpath's data access is reachable without the components: build a client with createQualityClient(transport) inside useEliQuery / useEliMutation from @eli-ai/react/hooks. That is exactly what these components do internally, so the fetch, abort and error semantics are identical.

HTTP API

Three operations. All are workspace-scoped through the bearer key plus an optional X-Workspace-Id header, and every failure is the shared { error, message } envelope — body-validation failures add an issues array. A run that belongs to another workspace is a 404, not a 403.

GET/api/v1/evals/runs/{id}/metricskb:read (runs:read accepted) · evals:read

Aggregates one run's per-result rows to the run level. The path parameter `id` is the eval run id. `retrieval` is null unless at least one result carried a retrieval metric; `ragas` is null unless a result carried a RAGAS metric; `quality` is null when the run has no overall run_metrics row. Individual metric fields are null when nothing contributed to that mean. 404 when no run with that id exists in this workspace.

Example requestbash
curl -sS "https://eli.ai/api/v1/evals/runs/$RUN_ID/metrics" \
  -H "Authorization: Bearer $ELI_API_KEY" \
  -H "X-Workspace-Id: $ELI_WORKSPACE_ID"
Responsejson
{
  "runId": "01KZRUNA00000000000000000",
  "status": "succeeded",
  "samplesN": 3,
  "resultsCounted": 42,
  "retrieval": {
    "recallAtK": 0.74,
    "precisionAtK": 0.44,
    "ndcgAt10": 0.71,
    "mrr": 0.79,
    "hitAtK": 0.93
  },
  "ragas": {
    "faithfulness": 0.91,
    "responseRelevancy": 0.84,
    "contextPrecision": 0.66,
    "contextRecall": 0.72,
    "noiseSensitivity": 0.08
  },
  "abstention": {
    "total": 42,
    "abstained": 5,
    "answered": 37,
    "abstentionRate": 0.119,
    "meanTruthfulness": 0.62,
    "aurc": 0.074,
    "riskCoverage": [
      {
        "threshold": 0.91,
        "coverage": 0.5,
        "risk": 0.048
      },
      {
        "threshold": 0.62,
        "coverage": 1,
        "risk": 0.143
      }
    ]
  },
  "errorClasses": {
    "correct": 31,
    "retrieval_miss": 4,
    "incomplete": 3,
    "citation_error": 2,
    "hallucination": 2
  },
  "quality": {
    "accuracy": 0.81,
    "groundedness": 0.9,
    "citationPrecision": 0.88,
    "citationRecall": 0.84,
    "retrievalHitRate": 0.93,
    "nItems": 42
  }
}

Field names above are the response contract; the numbers are illustrative. Three details are worth internalising before you build on it:

  • status is the run's own status — running, succeeded, failed, or degraded. Metrics are served for a run that is still running; they are simply partial.
  • abstention.meanTruthfulness is on the CRAG signed scale (perfect 1, acceptable 0.5, missing/abstained 0, incorrect −1), so it is not a percentage and can be negative. aurc is the mean risk over the swept operating points — lower is better — and is null when the run stored no selective-prediction pairs, in which case riskCoverage is an empty array.
  • errorClasses is a histogram keyed by the deterministic error taxonomy the runner assigns per result: retrieval_miss, hallucination, citation_error, stale, incomplete, conflict_mishandled, correct. Results with no error class contribute nothing.
GET/api/v1/evals/runs/comparekb:read (runs:read accepted) · evals:read

Pairs run B against run A by golden item id (per-item mean across that item's samples) and reports the mean delta b−a with a 95% percentile-bootstrap CI and a two-sided sign-flip permutation p-value. `significant` is a two-gate verdict: the CI excludes 0 AND p < 0.05. Both resamplers are seeded, so the same two runs always produce the same numbers. 400 when a or b is missing or metric is unknown; 404 when either run is absent from this workspace.

Query parameters

arequiredstringBaseline run id.
brequiredstringComparison run id.
metricstringOne of truthfulness (default), retrievalHitRate, recallAtK, precisionAtK, ndcgAt10, mrr, hitAtK. Anything else is a 400.
Example requestbash
curl -sS "https://eli.ai/api/v1/evals/runs/compare?a=$BASELINE&b=$CANDIDATE&metric=ndcgAt10" \
  -H "Authorization: Bearer $ELI_API_KEY" \
  -H "X-Workspace-Id: $ELI_WORKSPACE_ID"
Responsejson
{
  "a": { "runId": "01JB7YB0S9K2N4V6ZQ0X1M3T8D", "n": 40 },
  "b": { "runId": "01JB8QF4H1P7R2S5TC9WZ6K0YE", "n": 40 },
  "metric": "ndcgAt10",
  "pairedN": 38,
  "meanA": 0.641,
  "meanB": 0.723,
  "deltaMean": 0.082,
  "ci": { "low": 0.021, "high": 0.144, "level": 0.95 },
  "pValue": 0.011,
  "significant": true,
  "method": "paired-percentile-bootstrap+sign-flip-permutation"
}

pairedN is the number that matters

a.n and b.n count the items each run scored on that metric; pairedN counts the intersection, and only the intersection is tested. Items present in one run but not the other are silently dropped. With fewer than two pairs, ci and pValue are null and significant is false — an honest "not enough evidence", not a failure.
POST/api/v1/qrelskb:write · evals:write

Appends a TREC-style graded-relevance judgment for a (golden item, chunk) pair. The table is append-only: a human override inserts a new row rather than mutating a machine judgment, and the metrics reader resolves precedence at read time. A duplicate within the same (source, judgeModel) lane is a no-op that returns the existing row with 200; a newly appended row returns 201.

Request body

queryIdrequiredstringGolden item id the judgment is for. Minimum length 1.
chunkIdrequiredstringRetrieved chunk id being graded. For a doc-level judgment the convention used internally is chunkId = docId.
graderequiredintegerTREC graded relevance, integer 0–3.
docIdstringOwning document id. Optional, but the metrics reader keys on docId when present.
source"llm" | "human" | "provenance"Provenance of the judgment. Defaults to "human".
judgeModelstringJudge model id — machine sources only. Part of the dedupe key.
promptVersionstringJudge prompt version — machine sources only.
Example requestbash
curl -sS -L -X POST "https://eli.ai/api/v1/qrels" \
  -H "Authorization: Bearer $ELI_API_KEY" \
  -H "X-Workspace-Id: $ELI_WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{"queryId":"01JB6M0T2Q8V3XN7ZR1C5F9WKD","chunkId":"01JB6M12H4A0B8D6E2G4J7L9NP","grade":3}'
Responsejson
{
  "id": "01JB9T5C7E1G3J5L7N9Q1S3U5W",
  "workspaceId": "org_7Yh2Kq",
  "queryId": "01JB6M0T2Q8V3XN7ZR1C5F9WKD",
  "chunkId": "01JB6M12H4A0B8D6E2G4J7L9NP",
  "docId": null,
  "grade": 3,
  "source": "human",
  "judgeModel": null,
  "promptVersion": null,
  "createdAt": "2026-07-31T14:02:11.184Z"
}

Grades collapse per document at read time with the precedence human > provenance > llm; within the winning source the maximum grade wins. A successful append also writes a qrel.author row to the audit log, targeting the queryId.

Data elements

Every table below carries workspace_id and a *_tenant_isolation row-level-security policy asserting workspace_id = current workspace for both USING and WITH CHECK, so every read and write in the module runs inside a workspace transaction under FORCE RLS. There is no cross-workspace read path.

TableWhat one row isCrucibleColumns that matter to a consumer
golden_itemsOne evaluation question, revisioned so a later edit cannot silently change what a historical run was graded against.writesquestion · difficulty(easy|medium|hard) · status(draft|active|archived) · revision · answerable · questionType · popularity · dynamism · notes
golden_assertionsOne atomic claim a correct answer must or should support.writesitemId · text · kind(must|should) · weight · conceptId (null = plain text assertion)
golden_citationsA document anchor the answer is required to cite — or forbidden from citing.writesitemId · docId · snippet · mode(required|forbidden)
golden_item_tagsOne namespaced tag on an item; the axis metrics are stratified along.writesprimary key (workspaceId, itemId, tag) · tag e.g. bu:finance, topic:invoicing
golden_conceptsAn item-level entity anchor: which concepts a correct answer must, may, or must not touch.writesitemId · entityId · relevance(required|acceptable|irrelevant)
qrelsOne append-only graded-relevance judgment for a (golden item, chunk) pair. Written by POST /api/v1/qrels.reads + writesqueryId · chunkId · docId · grade 0–3 · source(llm|human|provenance) · judgeModel · promptVersion · unique(ws, queryId, chunkId, source, judgeModel)
eval_configsA named run configuration — which model slots, prompt versions and retrieval parameters a run applies.writesname · modelSlots · promptVersions · retrievalParams · params · judgeModelId · judgePromptVersion
eval_runsOne execution of a golden set. Read by both eval endpoints to resolve the run and its status.reads + writesconfigId · goldenSetHash · status(running|succeeded|failed|degraded) · samplesN · trigger(manual|scheduled) · kbUnstable · costEstimate/costActual · targetSnapshotId · judgeSnapshotId · startedAt/finishedAt
eval_resultsOne item × sample outcome. The source rows both endpoints aggregate.reads + writesrunId · itemId · sampleIdx · answerText · citations · retrievedDocIds · evidenceSnapshot · deterministic · retrievalHitRate · abstained · truthfulness[-1,1] · errorClass · retrievalMetrics · status(ok|error|timeout) · latencyMs · costUsd
judgmentsOne judge verdict on one assertion of one result, with an optional human override that wins.writesresultId · assertionId · verdict(supported|contradicted|not_found) · confidence · judgeModelId · flaggedForReview · humanVerdict · humanNote
run_metricsRolled-up quality for a run, overall or per tag. The tag = NULL row is what the metrics endpoint returns as `quality`.reads + writesrunId · tag (null = overall) · accuracy · groundedness · citationPrecision · citationRecall · retrievalHitRate · nItems
eval_feedbackAn append-only reclassification or correction note against a run, or one of its results.writesrunId · resultId (nullable, no FK) · errorClass · note · correctedBy
drift_snapshotsA point-in-time corpus fingerprint used to detect movement between sweeps.writesentityCounts (per type) · orphanRate · docChurn · docCount · capturedAt
alertsOne raised condition, deduped while open so a repeating sweep bumps rather than duplicates.writeskind e.g. drift.kb · kb.stale · eval.regression · severity(info|warning|critical) · message · meta · dedupeKey · acknowledgedAt · resolvedAt
audit_logThe shared workspace audit trail. Authoring a qrel appends one `qrel.author` event.writesevent · target (the queryId) · actorId · meta { chunkId, grade, source }

Two schema decisions shape what a consumer can rely on. Qrel keys are plain text, not foreign keys queryId and chunkId carry no referential constraint, so judgments survive golden-item edits and chunk churn from re-ingestion. And a run pins what it was graded against: goldenSetHash plus the targetSnapshotId / judgeSnapshotId foreign keys into immutable configuration snapshots mean a historical comparison stays meaningful after the set and the models have both moved on.

Feature map

Five feature areas cover the module: what a workspace measures against, how runs are produced, how per-result rows become numbers, how two runs become a verdict, and how the corpus underneath is watched. Each area names the components the code map and invariants below locate.

Feature areaWhat it providesDelivered bySurfaces
Golden sets and graded relevanceRevisioned questions with atomic must/should assertions, doc-anchored citations, namespaced tags, concept anchors, and TREC-style 0–3 relevance judgments.Golden-set service; the golden_items, golden_assertions, golden_citations, golden_item_tags, golden_concepts, and qrels tables.POST /api/v1/qrels, workspace authoring app, provenance-derived qrels.
Evaluation runsEach active item × sample routed through the real retrieve-and-answer pipeline, with evidence snapshots, deterministic checks, and judge-graded assertions.Eval runner and scheduled runner; eval_configs, config_snapshots, eval_runs, eval_results, judgments.Workspace app and scheduler produce; the two read endpoints consume.
Metric math and aggregationGraded retrieval metrics, CRAG-scale truthfulness, RAGAS bundle, risk–coverage and AURC, and an error-class histogram rolled up per run and per tag.Metric modules plus the aggregation helper; the run_metrics table (tag null = overall).GET /api/v1/evals/runs/{id}/metrics, EliEvalMetrics.
Paired significanceA seeded, deterministic A/B comparison over shared golden items: mean delta, bootstrap confidence interval, permutation p-value, and a two-gate verdict.Paired-significance module invoked by the aggregation helper.GET /api/v1/evals/runs/compare, SDK compareEvalRuns.
Drift, freshness, and alertsPoint-in-time corpus fingerprints (entity counts, orphan rate, doc churn), threshold evaluation, staleness checks, and deduped alert raising with webhook fan-out.Drift monitor and scheduled scan; the drift_snapshots and alerts tables.Workspace app reads; alert webhooks notify externally.

System architecture

The write side and the read side are deliberately separate. Runs are produced by the runner — launched from the workspace app or the scheduler — while the three public operations only read what the runner persisted, plus one append-only judgment write. Nothing on the public surface can start or mutate a run.

Crucible — system architecture

Rectangles are API routes and services, the rounded node is the judge model, and cylinders are workspace tables under row-level security. The runner exercises the real answer pipeline and persists everything the read-side aggregation later serves.

Rendering diagram

Downloads

Concepts

  • System architecture
  • Public surface
  • Scoring services
  • Run production (workspace app + scheduler)
  • Modules
  • Crucible

Keywords

  • GET {id}/metrics (API)
  • GET (API)
  • POST (API)
  • Eval runner (service)
  • model: judge
  • store, append-only
  • Scheduled runs + drift scans (service)
  • Drift + freshness monitor (service)
  • Run aggregation + paired significance (service)
  • Real retrieval + answer pipeline under test (service)
  • Metric math: retrieval, abstention, RAGAS (service)
  • /api/v1/evals/runs
  • /api/v1/qrels
  • id
  • /api/v1/evals/runs/compare
  • store
Source and generation provenance

Status: current

Generated at: 2026-08-17T18:32:22.397Z

Source hash: 8b71cd979f407554e9aca770e4912be31e2342e0e6bebb7b062a9e1c3ec63ce1

Metadata payload hash: 051fc109f6e5d62cc8e3d14b2bba913ad54485dec8fa76e75d8ab635e1aeda89

Canonical appearance

src/app/(docs)/docs/modules/crucible/page.tsx:65 route /docs/modules/crucible

All appearances

  • canonicalsrc/app/(docs)/docs/modules/crucible/page.tsx:65 route /docs/modules/crucible

No mirrored appearances.

Generation versions

App: eli-ai 0.1.0

Mermaid: 11.16.0 · Mermaid CLI: 11.16.0

Node: v26.3.1 · Yarn: 4.17.1

Renderer config hash: 68c10966fe84406ee626034d58bfabd555df9f65f691204b7c46db24038da101

Renderer theme hash: c80287a78d80ad63d27bd5ca348b2ef9a7e2f44da289e436be6484ea28a1b033

Adapter versions: diagramGenerator=2, drawioFlowchart=1, drawioGantt=1, drawioSequence=1, drawioState=1

Full sidecar JSON: crucible-system-architecture-8b71cd97.json

How to read the Crucible architecture

  1. Start with what a run consumes: The runner loads active golden items, graded-relevance rows, and a named config, then pins immutable config, target, and judge snapshots before the first item executes.
  2. Watch the scoring fan-out: Each item runs the real pipeline; the judge model grades assertions and truthfulness; the metric modules compute retrieval, RAGAS, and selective-prediction numbers per result.
  3. Separate reads from production: The metrics and compare endpoints call the aggregation helper over persisted rows; the qrels endpoint appends one judgment and its audit event.
  4. Follow the drift lane: The scheduled scan snapshots the corpus, evaluates thresholds from workspace settings, and raises one deduped alert per condition.
Trust boundary
Public bearer keys can read metrics, compare runs, and append qrels — run creation, golden authoring, and drift capture stay behind session-authenticated workspace surfaces.
Durable state
eval_runs pin goldenSetHash and snapshot ids; eval_results, judgments, run_metrics, qrels, drift_snapshots, and alerts are the persisted evidence every score resolves to.

Failure paths

  • Provider outage marks results errored and the run degraded
  • Fewer than two paired items yields an honest null verdict
  • Duplicate qrel in the same lane no-ops to the existing row
  • Drift thresholds breached raise a deduped drift.kb alert

Signals

  • Run status mix (succeeded / degraded / failed)
  • Per-tag run_metrics deltas across configs
  • Abstention rate and mean truthfulness trends
  • Open alerts by kind and severity

Where the logic lives

The code map locates each component. Table names stay in the notes; all are defined in the schema module with workspace row-level security.

ComponentKindLives atNotes
Eval read routesAPIsrc/app/api/v1/evalsgetEvalRunMetrics and compareEvalRuns handlers under runs/; both accept kb:read or the legacy runs:read scope.
Qrel authoring routeAPIsrc/app/api/v1/qrelsauthorQrel — append-only insert with 201-versus-200 semantics on the dedupe key.
Aggregation + compare helperservicesrc/app/api/v1/_lib/evalv2-metrics.tsAggregates per-result rows to the run level, pairs runs by golden item id, and owns the append-only qrel write with its audit event.
Eval runnerservicesrc/server/evals/runner.tsExecutes item × sample against the real pipeline, snapshots config/target/judge, persists eval_results and judgments, rolls up run_metrics per tag and overall.
Golden-set serviceservicesrc/server/evals/golden.tsItem CRUD with revisions, the golden-set hash a run pins, concept anchors, and doc-grade resolution with source precedence.
Retrieval metric mathservicesrc/server/evals/metrics/retrieval.tsrecallAtK, precisionAtK, ndcgAt10, mrr, and hitAtK computed against graded qrels.
Abstention + selective predictionservicesrc/server/evals/metrics/abstention.tsCRAG signed truthfulness judging plus the risk–coverage curve and AURC over per-result confidence/correct pairs.
Paired significanceservicesrc/server/evals/metrics/significance.tsBias-corrected and accelerated bootstrap interval plus sign-flip permutation test, both driven by one seeded generator.
Assertion judgingmodelsrc/server/evals/groundedness.tsThe judge-model pass that grades each must/should assertion per result; verdicts persist to judgments with room for a human override.
Drift + alertsservicesrc/server/evals/drift.tsSnapshot capture, threshold evaluation, staleness computation, and deduped alert raising into drift_snapshots and alerts.
Scheduled executionservicesrc/server/evals/scheduled.tsCadence-triggered runs and drift scans — the producer behind trigger: scheduled.
Table definitionsstoresrc/server/db/schema.tsgolden_items, golden_assertions, golden_citations, golden_item_tags, golden_concepts, qrels, eval_configs, config_snapshots, eval_runs, eval_results, judgments, run_metrics, eval_feedback, drift_snapshots, alerts.
Quality UIUIpackages/react/src/quality/index.tsxEliEvalMetrics, EliQuality, EliPolicyDecisions, and EliCacheStats.
Headless client + contractsSDKpackages/client/src/quality.ts · packages/contracts/src/quality.tscreateQualityClient with getEvalRunMetrics, compareEvalRuns, and authorQrel plus the wire shapes they return.

Primary runtime flow

The highest-value public path is the read-and-judge loop over runs that already exist: aggregate one run, compare it against a baseline with paired significance, and append the human relevance judgments that sharpen the next run's retrieval metrics.

Crucible — primary runtime flow

The three public operations in sequence. Aggregation and pairing are pure reads inside short workspace transactions; the only public write is the append-only qrel with its audit event.

Rendering diagram

Downloads

Concepts

  • Primary runtime flow
  • Modules
  • Crucible

Keywords

  • GET {id}/metrics (API)
  • GET (API)
  • POST (API)
  • eval_runs + eval_results + run_metrics (store)
  • qrels + audit_log (store)
  • append judgment + qrel.author audit event
  • getEvalRunMetrics runId
  • aggregate one run
  • load run, per-result rows, overall run_metrics row
  • Quality client (kb:read / kb:write)
  • Eval aggregation helper (service)
  • Paired-significance module (service)
  • retrieval + RAGAS + abstention inputs
  • compareEvalRuns a, b, metric
  • pair per-item means by golden item id
  • seeded bootstrap + sign-flip permutation
  • delta, confidence interval, p-value
  • two-gate significant verdict
  • authorQrel queryId, chunkId, grade
  • metrics summary with error classes and risk coverage
  • /api/v1/evals/runs
  • /api/v1/qrels
  • /api/v1/evals/runs/compare
Source and generation provenance

Status: current

Generated at: 2026-08-17T18:32:20.358Z

Source hash: d2c7d473fa1b3035d9b50cbe4cc138849483b8945966f2aa2c6ced2a92e48c10

Metadata payload hash: 29bf6f6cdaac4b751275adb567c4c407b477129d87148d6e535c2f003ddcf73d

Canonical appearance

src/app/(docs)/docs/modules/crucible/page.tsx:113 route /docs/modules/crucible

All appearances

  • canonicalsrc/app/(docs)/docs/modules/crucible/page.tsx:113 route /docs/modules/crucible

No mirrored appearances.

Generation versions

App: eli-ai 0.1.0

Mermaid: 11.16.0 · Mermaid CLI: 11.16.0

Node: v26.3.1 · Yarn: 4.17.1

Renderer config hash: 68c10966fe84406ee626034d58bfabd555df9f65f691204b7c46db24038da101

Renderer theme hash: c80287a78d80ad63d27bd5ca348b2ef9a7e2f44da289e436be6484ea28a1b033

Adapter versions: diagramGenerator=2, drawioFlowchart=1, drawioGantt=1, drawioSequence=1, drawioState=1

Full sidecar JSON: crucible-primary-runtime-flow-d2c7d473.json

How to read the measurement flow

  1. Aggregate one run: The helper loads the run's per-result rows and the overall run_metrics row, then computes abstention, risk coverage, and the error-class histogram on the fly.
  2. Pair before testing: Comparison keys per-item means by golden item id and tests only the intersection — items present in one run only are dropped.
  3. Read the two-gate verdict: significant is true only when the bootstrap interval excludes zero and the permutation p-value clears 0.05; both resamplers are seeded, so reruns reproduce exactly.
  4. Append judgments as you review: A qrel insert lands in its (source, judge-model) lane; a duplicate returns the existing row, and every new row writes a qrel.author audit event.
Trust boundary
A run belonging to another workspace is a 404 under row-level security; the public surface cannot launch runs or mutate results.
Durable state
Aggregation reads only persisted rows — eval_runs, eval_results, judgments, run_metrics — and qrels grow append-only with their audit trail.

Failure paths

  • Unknown compare metric or missing run id returns 400/404
  • pairedN below two returns null CI and p-value, significant false
  • Metrics for a still-running run are served partial by design
  • Qrel append into an existing lane returns 200, not a new row

Signals

  • deltaMean with its confidence interval per comparison
  • resultsCounted versus samplesN completeness
  • errorClasses histogram shifts between configs
  • Human qrel volume per golden item

Internals and invariants

Golden sets and graded relevance

Items are revisioned rows with atomic assertions rather than verbatim expected strings; citations anchor to document ids as required or forbidden; tags stratify metrics; concept anchors grade which entities a correct answer must touch. Qrels are plain-text-keyed on purpose so judgments survive item edits and chunk churn, and doc-level grades collapse at read time with source precedence human over provenance over llm, maximum grade within the winning source.

  • Invariant — qrels are append-only: a duplicate in the same (source, judge-model) lane no-ops to the existing row, and every successful append writes a qrel.author audit event in the same transaction. Enforced in src/app/api/v1/_lib/evalv2-metrics.ts.
  • Invariant— grade resolution applies the human > provenance > llm precedence at read time, never by mutating rows. Enforced by resolveDocGrades in src/server/evals/golden.ts.

Evaluation runs

A run resolves its config, pins the golden-set hash plus immutable target and judge configuration snapshots, and only then executes items. Each item × sample runs the production pipeline, records an evidence snapshot, passes deterministic checks, and has every assertion graded by the judge model. Error classification assigns each result one deterministic taxonomy bucket.

  • Invariant — what a run was graded against is frozen: goldenSetHash and the snapshot foreign keys are written before results exist, so later edits to items, configs, or models cannot rewrite history. Enforced in src/server/evals/runner.ts.
  • Invariant — provider errors are recorded as errored results and excluded from metric means, and a run whose error fraction crosses the threshold is marked degraded rather than reporting a phantom regression. Enforced in src/server/evals/runner.ts.

Metric math and aggregation

Per-result rows carry the metric payloads; the run level is always derived. The aggregation helper averages retrieval and RAGAS keys over the results that carry them, counts abstentions, sweeps the persisted confidence/correct pairs into the risk–coverage curve and AURC, and reads the overall run_metrics row for the quality block.

  • Invariant — a metric mean only includes results that actually carried that metric; nothing contributes zeros, so partial instrumentation cannot dilute a score. Enforced in src/app/api/v1/_lib/evalv2-metrics.ts.

Paired significance

Comparison pairs per-item means across the two runs, then runs a bias-corrected and accelerated bootstrap on the mean paired delta alongside a sign-flip permutation test. One seeded generator drives both resamplers.

  • Invariant — identical run pairs and metric always produce identical intervals and p-values: the statistics are pure and seeded, with no unseeded randomness. Enforced in src/server/evals/metrics/significance.ts.

Drift, freshness, and alerts

Snapshots fingerprint the corpus — entity counts per type, orphan rate, and document churn since the previous snapshot. The scheduled scan evaluates thresholds from workspace settings and raises one deduped alert per open condition; staleness compares the newest document and snapshot ages against the configured window.

  • Invariant — alert rows commit in a short transaction and webhook fan-out runs only after the commit, so a delivery failure can never lose or duplicate the alert itself. Enforced in src/server/evals/drift.ts.

For a self-hosting team the additive seams are the safe ones: contribute run metrics through the plugin surface (namespaced into the run_metrics plugin bag, so first-party numbers are never overwritten), author qrels from your own review tooling over the public endpoint, and tune drift thresholds per workspace. The pinning discipline — golden-set hash, config snapshots, seeded statistics — is what makes historical comparisons meaningful and is not designed to be relaxed.

How it composes

Crucible is usable on its own for reading and judging, but it measures something — and that something is produced by other modules. Here is the honest split.

  • Lens — needed to produce runs. An eval run routes each golden item through the real retrieve-and-answer pipeline; that is where retrievalMetrics, abstained and truthfulness come from. Without an answering pipeline there is nothing to score. Reading metrics and comparing runs still work perfectly on runs that already exist.
  • Warrant — powers two of EliQuality's three sections. EliQuality and EliPolicyDecisions read GET /api/v1/policy/decisions and GET /api/v1/policy/stats — the append-only answer-policy decision log written by the chat, structured and agent pipelines. Without the policy engine those panels render their empty state; the evaluation section is unaffected. Same kb:read scope, different system.
  • Atlas — unlocks concept-anchored scoring. golden_concepts.entityId and golden_assertions.conceptId reference graph entities, and the drift snapshot's entity counts and orphan rate are computed from entities and their mentions. With no graph, leave conceptId null — assertions stay plain text and everything else still scores.
  • Intake — gives citations and drift something to point at. Golden citations anchor to document ids, and source: "provenance"qrels are derived from an item's required citations (graded 3, keyed at document granularity). Drift's docChurn and docCount are counts of ingested documents. Human and LLM qrels need none of this.
  • Ports — how the three operations are published. getEvalRunMetrics, compareEvalRuns and authorQrel are catalogued under the quality capability, with kb:read / kb:write as the canonical scopes and runs:read retained as an accepted read scope on the two GETs.
  • Lineage — complementary, not required. Crucible writes its own audit event for qrel authoring and keeps its own append-only trails (qrels, eval_feedback, judgment overrides). Nothing in the module reads a lineage surface.

Conduit has no direct relationship with this module: nothing in Crucible's code reads a data connector or a live query. If an evaluated answer happens to include live data, that arrives through the answer pipeline, and Crucible sees only the resulting text and citations.

For the conceptual model behind the scores, see the evaluation framework and Evals & drift; for authoring golden items and launching runs in the workspace UI, see Golden sets & eval runs and Retrieval metrics.

Extending Crucible

A plugin can contribute a run metric with evalMetrics. The host namespaces the key to plugin.<id>.<metric> and writes it into the additive run_metrics.plugin_metrics jsonb bag, so a contributed number can never collide with or overwrite a first-party metric, and a regression comparison can always tell the two apart. Crucible emits eval.run.completed, drift.detected and alert.raised. See the extension model for the contract and the trust boundary, and Build a plugin for a worked example.