Architecture

RAG Doctor is organized as a TypeScript monorepo. Each package has a single, well-defined responsibility. Packages import only from @rag-doctor/types — never from each other — which keeps dependencies acyclic and makes individual packages independently embeddable.

Data flow

The analysis pipeline has six stages:

text
Raw Trace (JSON)
@rag-doctor/ingestion → validate, normalize
@rag-doctor/rules → run rule engine, produce findings
@rag-doctor/diagnostics → map findings to root cause
@rag-doctor/reporters → format output (JSON or text)
Output (terminal / file / programmatic)

Packages

@rag-doctor/typesFoundation

Shared TypeScript interfaces and types used across all packages. Defines the shape of traces, findings, configurations, diagnosis results, and reporter output. All packages import from here — never from each other.

RagTraceRagChunkFindingDiagnosisResultRuleConfigAnalysisResult
@rag-doctor/ingestionInput layer

Accepts raw trace input (JSON file, object, or Buffer), validates the structure, and emits a normalized internal representation. Handles field aliasing for common trace formats from LangChain, LlamaIndex, and custom instrumentation.

ingestTracevalidateTracenormalizeChunks
@rag-doctor/rulesRule library

Contains all built-in rules as pure functions. Each rule takes a normalized trace and returns zero or more findings. Rules are grouped into named packs. Custom rules can be registered at runtime.

runRulesregisterRulegetRulePackBUILT_IN_RULES
@rag-doctor/coreOrchestration

The public embedding API. Wires ingestion → rules → reporters together into a single analyze() function. This is what you import when using RAG Doctor as a library inside your application.

analyzediagnosecreateAnalyzer
@rag-doctor/diagnosticsRoot cause engine

Takes a set of findings and applies the heuristic graph to identify the primary root cause, contributing factors, and produce structured recommendations. Stateless, deterministic.

diagnoseFindingsDIAGNOSTIC_GRAPH
@rag-doctor/reportersOutput formatters

Formats analysis output. The JSON reporter produces machine-readable structured output. The text reporter produces human-readable terminal output with color and severity indicators.

JsonReporterTextReportercreateReporter
rag-doctor (CLI)Command-line interface

Thin command-line wrapper around @rag-doctor/core. Handles argument parsing, file I/O, configuration loading, and terminal rendering. All analysis logic lives in the core package.

analyze commanddiagnose command

Embedding the analysis engine

Import @rag-doctor/core directly to use RAG Doctor as an embedded library:

analyze.ts
1import { analyze } from "@rag-doctor/core";
2import type { RagTrace } from "@rag-doctor/types";
3
4const trace: RagTrace = {
5 query: "What is the recommended dose of ibuprofen?",
6 chunks: [
7 { id: "c1", content: "...", score: 0.88, tokens: 210, source: "db/ibuprofen.txt" },
8 { id: "c2", content: "...", score: 0.43, tokens: 380, source: "db/dosage.txt" },
9 ],
10 model: "gpt-4o",
11 totalTokens: 590,
12};
13
14const result = await analyze(trace, { pack: "recommended" });
15
16if (!result.ok) {
17 console.log(`Found ${result.findings.length} issues`);
18 result.findings.forEach((f) => console.log(`[${f.severity}] ${f.rule}: ${f.message}`));
19}

Writing a custom rule

empty-chunk.rule.ts
1import { registerRule } from "@rag-doctor/rules";
2import type { RuleContext, Finding } from "@rag-doctor/types";
3
4registerRule({
5 id: "empty-chunk",
6 name: "Empty Chunk",
7 description: "Detects chunks with no usable content",
8 defaultSeverity: "error",
9 evaluate(ctx: RuleContext): Finding[] {
10 return ctx.chunks
11 .filter((chunk) => chunk.content.trim().length < 10)
12 .map((chunk) => ({
13 rule: "empty-chunk",
14 severity: "error",
15 message: `Chunk ${chunk.id} contains insufficient content`,
16 evidence: { chunkId: chunk.id, contentLength: chunk.content.length },
17 }));
18 },
19});

Package scope

Individual packages can be installed independently if you only need a subset of RAG Doctor's functionality. For example, @rag-doctor/rules can be used directly without the CLI or reporters.