Quality & Access Control
Retrieval changes should ship against evidence. @absolutejs/rag/quality is an evaluation harness — golden-set suites, precision / recall / F1 scoring, answer-grounding checks, strategy and reranker comparisons with release gates — plus file and SQLite history stores and a request-scoped access-control layer for the chat plugin's mutating routes.
#Evaluation Suites
A suite is { id, input: { cases } } where each case pairs a query with expectations — expectedChunkIds, expectedSources, or expectedDocumentIds — plus optional hard negatives and a golden-set flag. Run it with runRAGEvaluationSuite over any evaluate function; evaluateRAGCollection is the standard one, and executeDryRunRAGEvaluation produces zeroed results for wiring tests.
import {
addRAGEvaluationSuiteCase,
buildRAGEvaluationLeaderboard,
createRAGEvaluationSuite,
evaluateRAGCollection,
runRAGEvaluationSuite
} from '@absolutejs/rag/quality';
let suite = createRAGEvaluationSuite({
id: 'docs-retrieval',
input: { cases: [], topK: 6 },
label: 'Docs retrieval'
});
// Every mutation returns a new suite and validates case ids.
suite = addRAGEvaluationSuiteCase({
caseInput: {
expectedDocumentIds: ['release-notes'],
hardNegativeSources: ['docs/archive.md'],
id: 'release-question',
query: 'What changed in the latest release?'
},
suite
});
const run = await runRAGEvaluationSuite({
evaluate: (input) => evaluateRAGCollection({ collection, input }),
suite
});
// run.response.summary: averagePrecision, averageRecall, averageF1,
// averageLatencyMs + passingRate over pass/partial/fail cases.
// Ranks runs by passingRate desc, then averageF1 desc, then latency.
const leaderboard = buildRAGEvaluationLeaderboard([run]);#Metrics
Retrieval metrics are set-based over the expected id set (chunk ids, sources, or document ids — the mode is reported per case). The summary aggregates averagePrecision, averageRecall, averageF1, and averageLatencyMs, and the response adds passingRate.
| Field | Computed as | Meaning |
|---|---|---|
precision | matched / retrieved | How much of what came back was expected. |
recall | matched / expected | How much of what was expected came back. |
f1 | 2pr / (p + r) | Harmonic mean of the two. |
status | pass | partial | fail | pass = every expected id matched; partial = some (or no expectations declared); fail = none. |
failureClasses | no_results, partial_recall, ... | Diagnostic tags: extra_noise, routing_miss, section / spreadsheet / media / ocr evidence misses. |
evaluateRAGAnswerGrounding scores answers instead of rankings: do the citations in the generated text resolve to retrieved sources, and do they cover the expected ones?
| Field | Computed as | Meaning |
|---|---|---|
citationPrecision | matched / cited | Cited sources that were expected. |
citationRecall | matched / expected | Expected sources that were cited. |
citationF1 | 2pr / (p + r) | Harmonic mean over citations. |
resolvedCitationRate | resolved / cited | Citations that resolve to a real retrieved source. |
coverage | grounded | partial | ungrounded | How much of the answer text is backed by citations. |
buildRAGEvaluationLeaderboard ranks runs by passingRate descending, then averageF1 descending, then averageLatencyMs ascending. The grounding leaderboard uses passingRate then averageCitationF1.#Comparisons & Gates
compareRAGRetrievalStrategies runs one suite across retrieval candidates ({ id, retrieval?, queryTransform?, rerank? }) and compareRAGRerankers does the same across rerankers. A RAGRetrievalBaselineGatePolicy turns the baseline-vs-candidate deltas into an explicit gate: minimum passing-rate / F1 / cue-case deltas, maximum latency regression, with severity deciding whether a miss warns or fails.
import {
buildRAGRetrievalComparisonDecisionSummary,
buildRAGRetrievalReleaseVerdict,
compareRAGRetrievalStrategies
} from '@absolutejs/rag/quality';
const comparison = await compareRAGRetrievalStrategies({
collection,
retrievals: [
{ id: 'vector-baseline', retrieval: 'vector' },
{
id: 'hybrid-mmr',
retrieval: { diversityStrategy: 'mmr', mode: 'hybrid' }
}
],
suite
});
// Gate the candidate against the baseline with explicit thresholds.
const decision = buildRAGRetrievalComparisonDecisionSummary({
baselineRetrievalId: 'vector-baseline',
candidateRetrievalId: 'hybrid-mmr',
comparison,
policy: {
maxElapsedMsDelta: 250,
minAverageF1Delta: 0,
minPassingRateDelta: 0,
severity: 'fail' // or 'warn' to downgrade gate misses
}
});
// pass | warn | fail | needs_review : without a gate policy, any
// negative passing-rate / F1 / cue delta yields needs_review.
const verdict = buildRAGRetrievalReleaseVerdict({
decisionSummary: decision
});Prebuilt benchmark suites cover recurring regressions: createRAGAdaptiveNativePlannerBenchmarkSuite, createRAGNativeBackendComparisonBenchmarkSuite, createRAGPresentationCueBenchmarkSuite, and createRAGSpreadsheetCueBenchmarkSuite — each with a paired snapshot factory.
#History Stores
Every history surface follows one pattern: createRAGFile*Store(path) for a JSON file, createRAGSQLite*Store({ db?, path?, tableName? }) for bun:sqlite (defaulting to :memory: with a per-store table name). Paired persist* and load* functions write and read runs, and every store slot plugs straight into the ragChat config.
| Family | Stores | What it keeps |
|---|---|---|
| Evaluation runs | EvaluationHistory, EvaluationSuiteSnapshotHistory | Suite run history and versioned suite snapshots. |
| Answer grounding | AnswerGroundingEvaluationHistory, AnswerGroundingCaseDifficultyHistory | Grounding run history and per-case difficulty tracking. |
| Baselines & comparisons | RetrievalBaseline, RetrievalComparisonHistory | Promoted retrieval baselines and comparison run history. |
| Release governance | RetrievalReleaseDecision, RetrievalReleaseIncident, *PolicyHistory | Release decisions, incidents, and lane / gate / escalation policy history. |
| Lane handoffs | RetrievalLaneHandoffDecision, ...Incident, ...IncidentHistory | Handoff decisions and incident lifecycle, plus auto-complete policy history. |
| Remediations | RetrievalIncidentRemediationDecision, ...ExecutionHistory | Recorded remediation decisions and execution history. |
| Search traces | SearchTrace, SearchTracePruneHistory | Persisted retrieval traces and prune-run history. |
import { Elysia } from 'elysia';
import { ragChat } from '@absolutejs/rag';
import {
applyRAGSQLiteStoreMigrations,
createRAGSQLiteEvaluationHistoryStore,
createRAGSQLiteGovernanceStores,
persistRAGEvaluationSuiteRun
} from '@absolutejs/rag/quality';
// One call wires all fifteen governance stores (baselines, release
// decisions, incidents, lane handoffs, traces, ...) onto ragChat.
const governance = createRAGSQLiteGovernanceStores({
path: './rag-governance.sqlite',
tablePrefix: 'docs'
});
applyRAGSQLiteStoreMigrations({ path: './rag-governance.sqlite' });
new Elysia().use(ragChat({ ...governance, collection, provider }));
// Or persist runs yourself. File stores take a path string; SQLite
// stores take { db?, path?, tableName? } and default to ':memory:'.
const history = createRAGSQLiteEvaluationHistoryStore({
path: './rag-history.sqlite'
});
await persistRAGEvaluationSuiteRun({ run, store: history });inspectRAGSQLiteStoreMigrations reports missing columns across store tables and applyRAGSQLiteStoreMigrations adds them — run it on deploy when upgrading the package.
#Search Traces
searchWithTrace returns the full retrieval trace; buildRAGSearchTraceRecord packages it with results, labels, group keys, and timing, and persistRAGSearchTraceRecord saves it. buildRAGSearchTraceDiff compares two records (added / removed / retained chunk ids, top result changes), and summarizeRAGRetrievalTraces aggregates across cases.
Retention is explicit: prune by maxAgeMs, maxRecordsPerQuery, or maxRecordsPerGroup via pruneRAGSearchTraceStore, preview the effect first with previewRAGSearchTraceStorePrune, or hand ragChat a searchTraceRetentionSchedule ({ intervalMs, runImmediately? }) and let it prune on a timer — runs are tagged manual, write, or schedule.
#Access Control
createRAGAccessControl builds the two hooks the chat plugin understands: authorizeRAGAction decides per action — ingest, create_document, delete_document, clear_index, reindex_document, reindex_source, reseed, reset, sync_source, sync_all_sources, list_sync_sources, analyze_backend, rebuild_native_index, manage_retrieval_admin, manage_retrieval_baselines, prune_search_traces — returning a boolean or { allowed, reason }, and resolveRAGAccessScope restricts what a request can see.
An RAGAccessScope narrows reads and mutations to allowedSources / allowedSourcePrefixes, allowedDocumentIds, allowedCorpusKeys and corpus groups, allowedSyncSourceIds, allowedComparisonGroupKeys, plus a requiredMetadata equality filter. There is no built-in role model — your authorize and resolveScope callbacks own the policy.
import { Elysia } from 'elysia';
import { createRAGAccessControl, ragChat } from '@absolutejs/rag';
type Session = { role: 'admin' | 'member'; tenantId: string };
const accessControl = createRAGAccessControl<Session>({
authorize: ({ action, context }) => {
if (!context) {
return { allowed: false, reason: 'Sign in required' };
}
if (action === 'reset' || action === 'clear_index') {
return { allowed: context.role === 'admin', reason: 'Admins only' };
}
return true;
},
// Called once per Request (memoized in a WeakMap) and shared by
// both the authorize and scope resolvers.
resolveContext: (request) => loadSession(request),
resolveScope: ({ context }) =>
context
? {
allowedSourcePrefixes: ['docs/'],
requiredMetadata: { tenantId: context.tenantId }
}
: undefined
});
// Spreads authorizeRAGAction + resolveRAGAccessScope onto the plugin.
new Elysia().use(ragChat({ ...accessControl, collection, provider }));authorize is omitted, every action is allowed; if resolveScope is omitted, nothing is scoped. Wire both for any multi-tenant deployment.