AbsoluteJS

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.

createRAGEvaluationSuiteNormalizes cases and throws on duplicate case ids.
addRAGEvaluationSuiteCase / updateRAGEvaluationSuiteCase / removeRAGEvaluationSuiteCaseImmutable case edits — each returns a new suite and validates ids.
reorderRAGEvaluationSuiteCasesReorders by explicit id list; requires exactly one id per case.
setRAGEvaluationSuiteCaseGoldenSetMarks or unmarks a case as part of the golden set.
addRAGEvaluationSuiteCaseHardNegativeAttaches a hard negative by kind: 'chunkId', 'source', or 'documentId'.
generateRAGEvaluationSuiteFromDocumentsSynthesizes one case per indexed document (defaults: maxCases 20, topK 5, includeGoldenSet true, hardNegativePerCase 1).
createRAGEvaluationSuiteSnapshot / buildRAGEvaluationSuiteSnapshotDiffVersioned suite snapshots plus added / removed / changed / reordered case diffs.
summarizeRAGEvaluationSuiteDatasetCase, golden-set, and hard-negative counts for a suite.
TS
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.

FieldComputed asMeaning
precisionmatched / retrievedHow much of what came back was expected.
recallmatched / expectedHow much of what was expected came back.
f12pr / (p + r)Harmonic mean of the two.
statuspass | partial | failpass = every expected id matched; partial = some (or no expectations declared); fail = none.
failureClassesno_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?

FieldComputed asMeaning
citationPrecisionmatched / citedCited sources that were expected.
citationRecallmatched / expectedExpected sources that were cited.
citationF12pr / (p + r)Harmonic mean over citations.
resolvedCitationRateresolved / citedCitations that resolve to a real retrieved source.
coveragegrounded | partial | ungroundedHow much of the answer text is backed by citations.
Leaderboard ordering
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.

passGate passed, or no gate and every tracked delta is non-negative.
warnGate thresholds missed under severity: 'warn'.
failGate thresholds missed under severity: 'fail' (the default).
needs_reviewNo gate policy and at least one negative delta — or no baseline verdict derivable.
TS
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.

FamilyStoresWhat it keeps
Evaluation runsEvaluationHistory, EvaluationSuiteSnapshotHistorySuite run history and versioned suite snapshots.
Answer groundingAnswerGroundingEvaluationHistory, AnswerGroundingCaseDifficultyHistoryGrounding run history and per-case difficulty tracking.
Baselines & comparisonsRetrievalBaseline, RetrievalComparisonHistoryPromoted retrieval baselines and comparison run history.
Release governanceRetrievalReleaseDecision, RetrievalReleaseIncident, *PolicyHistoryRelease decisions, incidents, and lane / gate / escalation policy history.
Lane handoffsRetrievalLaneHandoffDecision, ...Incident, ...IncidentHistoryHandoff decisions and incident lifecycle, plus auto-complete policy history.
RemediationsRetrievalIncidentRemediationDecision, ...ExecutionHistoryRecorded remediation decisions and execution history.
Search tracesSearchTrace, SearchTracePruneHistoryPersisted retrieval traces and prune-run history.
TS
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.

TS
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 }));
Deny by omission does not exist
If authorize is omitted, every action is allowed; if resolveScope is omitted, nothing is scoped. Wire both for any multi-tenant deployment.