Skip to content

From messy enterprise data to knowledge-graph retrieval — a deep dive

How the framework turns heterogeneous, siloed files into an AI-ready knowledge corpus, and why fusing that knowledge graph into retrieval answers questions that dense and lexical search structurally cannot. Written for technical evaluators who want the reasoning, not just the claim.


The problem, stated honestly

Enterprise data is not a clean dataset. It is scans and PDFs and office documents and text dumps, sitting in different systems, with no shared schema, no resolved entities, PII scattered through the body, and — the part everyone underestimates — no explicit structure connecting one document to another. The knowledge an organization actually needs to answer a question is usually spread across several of these documents, joined only by references a human would follow instinctively ("this contract supersedes that one", "this spec cites that standard") and that no keyword or embedding sees.

Most "RAG" stacks paper over this by chunking everything and hoping a vector index finds the right passage. That works until the answer isn't near the question — which, for real enterprise questions, is exactly the case that matters.

The thesis of this framework: treat files as the source of truth at every step, extract not just entities but the relationships and cross-references that connect documents, assemble them into an evidence-linked knowledge graph, and then let that graph participate in retrieval — so the system can reason across documents, not just match text within one. Every model is swappable, every stage emits inspectable files, and the retrieval layer lifts your existing search engine rather than replacing it.

The rest of this document walks that end to end, in five acts, naming the real models and mechanisms, and closes on a measured result that isolates exactly what the knowledge graph buys you.

This page is the reasoning; Pipeline anatomy is the mechanics. Where this deep dive argues why the design is shaped this way, the anatomy walks the stack as actually configured today — every stage of the benchmarked pipelines, with its Provider, its model, its exact config values, and its I/O contract — and its chapters go all the way down per stage. Read this one for the argument, that one for the specification.


Act I — The spine: messy files → AI-ready data

Everything begins with one command. latence setup runs a guided wizard that inspects your corpus and emits a validated pipeline configuration; latence process runs it. There is no hand-authored YAML in the happy path — the wizard is the entry gate, and it composes the pod-validated enterprise-SOTA stack. What follows is that stack, in DAG order. Each stage is a Capability (a narrow, model-agnostic interface) fulfilled by a swappable Provider (a concrete model in its own package).

1 · Sourcesource.local_folder. Lists and fetches the documents, stamps initial provenance and a default internal sensitivity, and records the file extensions it sees (which later decide the parser). Deterministic, zero-dependency.

2 · Intake screeningscreening.intake_signature. The first of two safety checkpoints, before parsing: it rejects oversized, malformed, zip-bomb, and spoofed files. The size ceiling is sized from your corpus (largest observed × 2, clamped to a sane range) rather than a magic constant — a small detail that signals the design temperament. Deterministic, zero-dependency.

3 · Parseparser.lighton_vllm or parser.plaintext. The wizard routes by file type: a born-digital text corpus (.txt/.md/.jsonl/…) goes to the CPU plaintext parser (no OCR, nothing to serve); anything with scans, PDFs, office docs, or images goes to the served OCR path. - Model (OCR): lightonai/LightOnOCR-2-1B — a vision-language model served over an OpenAI-compatible vLLM endpoint. It rasterizes each page to an image and reads it, which is what lets it handle scans and complex layouts that a text extractor mangles. It runs as a served endpoint, not in-process, because a VLM is heavy and benefits from vLLM's batching — and because isolating it behind an endpoint keeps its dependency stack from colliding with the learned models downstream. - Two LightOn packages, two checkpoints — don't conflate them. latence-parser-lighton-vllm (parser.lighton_vllm) is the served path above and pins lightonai/LightOnOCR-2-1B; latence-parser-lighton (parser.lighton) is the in-process transformers path for a single box and pins lightonai/LightOnOCR-1B-1025. Both are Apache-2.0 weights and code, verified independently (2026-07-09 and 2026-07-08 respectively) and declared in each Provider's own ProviderProfile. - Output: a DocumentRecord — clean markdown plus a page map that preserves where every span came from. Provenance is never lost; it is threaded through every subsequent stage.

4 · Chunkchunk.markdown. Splits the markdown into retrieval-sized pieces, preserving character offsets and page alignment so any later result can be traced back to its exact source location. Deterministic. The engine's ChunkBudget defaults to max_tokens: 512, overlap_tokens: 64, min_tokens: 16; --chunk-max-tokens changes the ceiling, and the benchmark campaign's pipelines run 640 / 80 / 8. The downstream encoder's window is derived from this budget rather than fixed: the chunker counts ~1.2× fewer tokens than mdeberta-v3, so extract/redact bound max_len above the budget and a raised budget cannot silently truncate.

5 · Content screeningscreening.content_keyword (when PII handling is on). The second checkpoint, after chunking: a guardrail against prompt-injection, harmful content, and sensitivity escalation. Chunks that trip it are quarantined or flagged, not silently dropped.

6 · Schema induction — per chunklabel_inducer.llm. Before extracting anything, the pipeline induces the label schema per chunk: for each chunk it derives exactly the entity types, relation types, and PII categories present in that chunk — a small, relevant set, not a corpus-wide catalog. - Model: openai/gpt-4.1-mini via OpenRouter — any OpenAI-compatible endpoint (OpenRouter, a local vLLM, Azure); a free-text domain hint steers it. A predefined-only mode skips induction entirely when you already have a fixed schema (compliance taxonomies, known entity sets). - Why per chunk, not corpus-wide. A single global schema forces two bad choices: cap it — and silently drop the rare-but-critical types a corpus-frequency cap discards — or apply all of it to every chunk, drowning each chunk's extraction in dozens of irrelevant labels. Zero-shot extractors lose precision as the label count grows, so a global schema taxes every chunk to serve none of them well. Per-chunk induction avoids both: each chunk sees only what is relevant to it. - Consistency is deliberately not this stage's job. It is earned later, in the phase built for it (stage 10). Cost at scale is bounded by de-duplicating identical/boilerplate chunk texts and by a swappable, cheaper inducer for the per-chunk pass.

7 · Extraction (fused NER + relations) — per chunkfused_entity_relation.gliner2. Entities and the relations between them, in a single pass, against that chunk's own small label set. - Model: fastino/gliner2-multi-v1 — a zero-shot GLiNER2 model on an mdeberta-v3 backbone, running in-process (GPU here, CPU-viable on a laptop). Zero-shot means it extracts the induced schema without task-specific fine-tuning. - Why the small per-chunk set matters — twice over. Quality: zero-shot precision degrades with label count, so a handful of relevant labels beats dozens of mostly-irrelevant ones. Cost: the model's span-scoring tensor scales with batch × tokens × labels, so collapsing the label dimension is what keeps memory flat — a global schema is what turns a routine batch into a multi-gigabyte allocation. - Efficient batching. Chunks are bucketed by their induced label set — most chunks in a document or topic share one — and each bucket runs as a single native batched forward. Small, stable shapes per bucket also make the model's own compile and quantize paths safe to switch on, which variable-width global schemas prevented. Full batch throughput at each chunk's own label count. - Output: EntityMention and RelationMention records carrying true original-document offsets.

7b/7c · Reference extractionheader_refs and inline_refs. This is where the framework captures the structure that turns a pile of documents into a connected graph, and it earned its place the hard way (see Act IV). Entity-anchored relation extraction sees relationships between named entities in a sentence; it does not see a document's cross-references — the "Obsoletes: 7230" in a header, or the "[RFC9110]" scattered through a body. Two deterministic, zero-dependency extractors recover them: - header_refs resolves a document's self identifier and mines its metadata-header edges (obsoletes / updates / references) — the explicit, structured cross-references. - inline_refs resolves the self id once (from front matter or the filename) and mines every body chunk for inline citations, emitting references(source → cited) edges. This matters enormously: on a reference-rich corpus, inline citations are the majority of the real cross-reference structure, and a body chunk carries no self-id, so nothing else can recover them. (Measured: this one extractor lifted reference coverage from 14% to ~112% of the gold graph — Act IV.) - Both are deterministic and compose beside the learned extractor; if no structured-header family is detected in your corpus, they simply aren't in the pipeline, and the output is byte-identical.

8 · Redaction and guardrails — per chunkredaction.gliner2. Learned PII masking as a chunk→chunk transform: it reads the induced chunk stream and sets each chunk's masked_content, which every downstream stage then reads — so the exported corpus is genuinely PII-safe and still enriched. - Model: fastino/GLiNER2-Guardrails-PII-Multi — the same GLiNER2 engine family the extractor uses, so redaction adds no second model library. Induced PII and guardrail categories are per chunk, bucketed and batched exactly like extraction. - The floors are the exception, and deliberately so. Beneath the learned model sit two universal sets that are never narrowed per chunk: the financial-PII floor (person, email, phone, address, IBAN, credit-card, tax id, bank account, SSN) and the always-on safety categories. These are security invariants, not relevance labels — a leaked card number must be caught in a chunk about something else. They are enforced at the shared finalize_redacted_chunk seam every redaction provider funnels through, so they are non-bypassable by model choice: if the learned model misses a card or IBAN, the deterministic floor still masks it. - The rule, stated once: relevance labels are scoped per chunk; safety floors are universal.

The phase boundary. Redaction is the last chunk-level stage. Everything from source through redaction operates on a single chunk or document at a time — no corpus-wide state, no global schema, nothing that has to see the whole dataset. Only after this line does the pipeline go corpus-level. This is what keeps the heavy model work bounded, parallel, and independent of corpus size.

9 · Profilingprofiling.statistical. Corpus-level quality and statistical features (density, readability, entity-frequency, co-occurrence) that feed the Quality Report. Deterministic.

10 · Resolution — where corpus-level consistency is earneddisambiguation.glinker. This is the first corpus-wide stage, and it does two jobs. - Entities are resolved. The same organization named three ways across five documents becomes one canonical node. Models: knowledgator/gliner-linker-large-v1.0 (linker) + knowledgator/gliner-linker-rerank-v1.0 (reranker) — in-process encoders (FlashDeBERTa with graceful fallback), backed by an audited multi-strategy resolver (exact / alias / acronym / substring / fuzzy, with an optional embedding rung). - Types are canonicalized. Per-chunk induction means chunk A may say org, chunk B organization, chunk C company. Here those collapse into one canonical type, with an alias→canonical map applied to every mention and reported on the Quality Report. The raw induced label is kept for audit, so the mapping is inspectable rather than magic. - This is the trade the whole design turns on. Consistency is earned downstream, by a stage that can see the entire corpus and resolve ambiguity with evidence — not imposed upstream by forcing one global schema and capping it. Upstream you get complete, precise, chunk-local extraction; here you get one coherent vocabulary. Both, instead of trading one for the other. - Without this step the graph would carry duplicate nodes and drifting types, and the cross-document reasoning in Act IV would fall apart — you can only traverse from A to B if A and B are single, resolved nodes of comparable type.

11 · Graph assembly (+ centrality)graph.canonical with graph_features.pagerank. The canonical, evidence-linked knowledge graph is assembled: nodes are resolved entities, edges are extracted and reference relations, and every edge carries its Evidence (the sentence it came from). The graph_features.pagerank computer stamps PageRank centrality and label-propagation community onto every node — pure-Python, deterministic, additive. These become first-class columns and, crucially, signals the retrieval layer uses (a high-centrality "hub" entity is treated differently in multi-hop expansion and in packing).

12 · Graph completiongraph_completion.ultra. Optional inductive link prediction that infers missing edges with the ULTRA zero-shot model (mgalkin/ultra_3g, PyKEEN DistMult fallback). Inferred edges are explicitly marked (inferred: true, calibrated confidence) so they are never confused with asserted facts. If the checkpoint isn't present, the stage skips with a flag rather than failing the run.

13 · Context enrichmentcontext.kg_header. The graph is projected back onto each chunk as a compact context header (top neighbors, key triples), and the denormalized KG columns (kg_node_ids, neighbor_node_ids, kg_node_community, kg_node_centrality) are attached to every chunk. This is the quiet hinge between the pipeline and retrieval: it is because each chunk carries its own entity node ids that the multi-hop expander can later seed a graph traversal from a retrieved chunk. The enriched text is what gets embedded (embedding-input only — the stored content is unchanged), so the dense vectors are graph-aware.

14 · Exportexport.knowledge_graph + export.jsonl_parquet. The deliverable, as files: - The KG: graph-nodes.parquet + graph-edges.parquet + graph.ttl (asserted and inferred edges, with evidence). - The RAG corpus: records.parquet / .jsonl — every enriched chunk with its dense embedding (ibm-granite/granite-embedding-311m-multilingual-r2, 768-dim, in-process) and the denormalized KG columns — plus the BM25 index-time signal (bm25-stats.json + postings), written by a reference tokenizer whose term unit exactly matches the query-side rescorer. - Plus quality-report.md (a first-class artifact substantiating "AI-ready") and manifest.json.

How it connects: each stage is a pure transform over records, threaded by provenance, with the graph woven back into the corpus at enrichment so that the files themselves — not some hidden index — carry both the text-with-vectors and the structure. That is the whole point of Act I: the output is a portable, inspectable, model-agnostic dataset that already contains everything retrieval needs.


Act II — Why the knowledge graph is the difference

It is worth pausing on what was just built, because it is the part that makes the retrieval result in Act IV possible.

A conventional RAG corpus is a bag of chunks with vectors. This corpus is that plus a graph in which resolved entities are connected by extracted relations and, critically, by cross-document references — and in which every chunk knows which entity nodes it contains. The graph is not a side-artifact for browsing; it is a reasoning substrate wired into the chunks.

Three properties make it usable, not decorative: 1. Entities are resolved (Act I, stage 10), so a node is a single thing you can traverse to. 2. Edges carry evidence, so any inferred connection is auditable back to a source sentence. 3. Chunks carry their node ids (stage 13), so retrieval can cross from "a chunk I found" to "the entities it mentions" to "what those entities connect to."

Centrality and community (stage 11) add the final ingredient: the graph knows which nodes are hubs (cited by everything, like a foundational standard) versus specific. Retrieval uses this to avoid being swamped by hubs when it expands, and to prefer diverse, non-redundant evidence when it packs.

The ordering principle: extract narrow, resolve wide

The pipeline has one structural rule, and Act I is shaped entirely by it:

Source → Redaction Resolution → Export
Scope one chunk at a time the whole corpus
Goal completeness + precision consistency + deduplication
Labels each chunk's own small set one canonical vocabulary
Scales with number of workers corpus size, once

Most stacks invert this — they fix a global schema first, then extract against it. That looks tidier and costs you twice: the schema must be capped (so rare-but-critical types vanish), and every chunk is scored against labels that have nothing to do with it (so precision drops and memory balloons).

Extracting narrow and resolving wide is strictly better because the two problems have different natures. Extraction is a local question — "what is in this chunk?" — best answered with local context and nothing else. Consistency is a global question — "are these two things the same?" — and cannot be answered well before you have seen everything. Answering the global question first, by guessing a schema up front, is what forces the cap. Answering it last, with all the evidence on the table, is what makes both answers good.

It also makes the expensive half embarrassingly parallel and independent of corpus size: every chunk's extraction is self-contained, so throughput is a function of how many workers you run, not how large the corpus is. The corpus-wide pass happens once, on already-extracted records, not on raw text.


Act III — Retrieval, without replacing your engine

Here the framework makes a deliberate architectural choice that matters more to an enterprise than any single algorithm: the retrieval layer is strictly stateless. It never holds an index and never runs first-stage search. You keep your engine — Qdrant, Weaviate, Elastic, Azure AI Search, Databricks, Neo4j, whatever you already run. The framework gives that engine SOTA quality without being a search engine.

Concretely, retrieval splits into two clocks: - Index-time signal generators (part of the pipeline): the dense embedder, the SPLADE sparse embedder, the BM25 term-stats, the multi-vector embedder — each a pure transform from chunk text to a vector / term-weight map, materialized as files during latence process. - Query-time candidate processors (a downstream library): pure transforms over a candidate list you fetched from your engine — fuse, rerank, pack, expand. They hold nothing and fetch nothing.

The seam between them is one interface:

The bring-your-own-engine backend. A RetrievalBackend declares its honest capabilities — a closed set {dense, sparse, bm25, filter, multivector} — and exposes exactly one method, search(query), which is the only fetch in the entire library, and it runs on your engine. The shipped QdrantBackend translates one query to a single query_points call and maps results back to the framework's neutral Candidate type; the same key it reads for chunk text is the same key the framework's own sink writes, so a collection this framework loaded needs zero configuration. Point the seam at your store instead and nothing else changes. This is the guarantee that lets you adopt the quality layer without a migration.

Around that single fetch, the components compose:

FusionRrfFuser / WeightedFuser. When you fetch from more than one signal (say dense + BM25), you have two ranked lists on incomparable scales (cosine vs BM25). Reciprocal-rank fusion solves this by ranking only: it sums weight / (k + rank) across lists, so it is scale-free and robust — the right default when you are combining a vector score and a lexical score that have no common unit. A WeightedFuser is available when you do want to preserve magnitudes (min-max normalized). Fusion is skipped when only one leg fired, so a single store's own scores are never discarded.

The multi-hop expanderMultiHopExpander over a GraphSource. This is the component that turns the Act II graph into retrieval power. It anchors on the top-scored candidates the first stage found, reads their kg_node_ids, traverses the emitted graph-edges.parquet out to a bounded number of hops, and gives every candidate a hop-decayed bonus for the closest hop at which one of its own nodes lands in that neighborhood. A one-hop neighbor of a seed earns the full weight; a two-hop neighbor earns it decayed. The graph source is itself a seam: - DuckDBGraphSource — the zero-ops default: embedded, serverless DuckDB runs a recursive traversal directly over the parquet file, no database to stand up. (It uses UNION, not UNION ALL, in the recursion — a deliberate choice that bounds work to polynomial even on cyclic graphs.) - NetworkxGraphSource for small graphs, Neo4jGraphSource for shops already running Neo4j. Same canonical semantics behind all three, proven by an equivalence test. Hubs are handled by the traversal's work-bounding and by centrality-aware decay, so a foundational node cited by everything does not drown the result.

RerankingReranker with a cross-encoder. A precision pass: a cross-encoder scores each candidate against the query jointly (not as independent embeddings) and reorders. The checkpoint is not pinned — you name one — consistent with the license discipline below.

The knapsack packerPacker, a ported quadratic-knapsack context packer. Once you have relevant candidates, you have to fit them into a context budget, and naive top-k wastes the budget on redundant passages. The packer maximizes relevance minus redundancy subject to a token budget: a deterministic greedy marginal-gain solver repeatedly adds the candidate with the best (value − λ·similarity-to-already-picked) / cost that still fits. Value comes from first-stage relevance (optionally boosted by KG signals like a chunk's peak entity centrality); redundancy is measured by an explicit similarity map, cluster equality, or lexical overlap. The token counter is itself a seam — a zero-dependency heuristic by default, real tiktoken when you install the extra. It selects, it does not re-score — so you run rerank first, then pack. The result is a context window filled with diverse, high-value, non-redundant evidence.

SPLADE (sparse-learned) — the query-time twin of the index-time SPLADE signal reuses the same composition class the pipeline ran, so query and corpus land in the identical sparse space; the model is a seam with no pinned checkpoint. (In the measured run of Act IV, SPLADE was deliberately scoped out to keep the proof to three signals — its index path is built; native wiring is a documented next step.)

Multi-vector / MUVERA — the stubs, honestly. Late-interaction (ColBERT-style) retrieval scores a query against a document's token-level vectors with MaxSim, which is more expressive than a single dense vector but expensive to index. The framework ships this as real but experimental, off by default: - max_sim(Q, D) — the reference late-interaction score, deterministic, with a hard error (never a silent truncation) on width mismatch. - MaxSimReranker — rescores candidates against sidecar multi-vectors when you load them. - MuveraFdeConverter — the canonical MUVERA trick: a training-free, data-oblivious conversion of a multi-vector into a single dense vector whose dot-product approximates MaxSim, so late-interaction quality can live in an ordinary dense ANN index (emitted as an fde_embedding column). It uses the query/document asymmetry that gives MUVERA its guarantee. These name no model and ship no weights, and nothing wires them unless you opt in — so they are an honest on-ramp to multi-vector retrieval, not a checkbox.

The orchestrator ties it together for callers who want the opinionated chain: query-signal-gen → fetch-per-modality (each leg fires only if the backend declares it and the query has that signal) → fuse → the processor chain (expand / rerank / pack). A capability the backend can't serve is a loud error, never a silent drop, and the result reports which modes actually ran — so the adaptation is observable, not magic. A Retrieval MCP wraps this as agent-facing tools whose copy comes entirely from operator config, so it auto-adapts to the stack in production.


Act IV — Why multi-hop wins over single-hop fused retrieval

Now the payoff, and the reasoning behind it.

The mechanism. Dense and lexical retrieval both fetch chunks that are near the query text — semantically (dense) or by term overlap (BM25). Fusing them (single-hop fused retrieval) makes each signal cover the other's blind spots, and for most questions that is enough. But consider a question that is framed in one document and answered in another: "the cipher suites for the TLS protocol referenced by RFC 7230." The query talks about RFC 7230 (HTTP); the answer lives in RFC 5246 (TLS). No lexical or semantic signal puts RFC 5246's chunk near this query — its text isn't about HTTP — and each chunk only exposes its own local content. Single-hop fusion, no matter how well tuned, structurally cannot retrieve it. The evidence is one reference edge away, and neither signal follows edges.

The multi-hop expander follows the edge. It takes the RFC 7230 chunks the first stage did find, reads their entity node ids, traverses the reference edge 7230 → 5246 in the emitted graph, and rescores the RFC 5246 chunk up into the returned window. It rewards edge-path proximity — a signal that only a real graph traversal exposes. That is the difference between matching text and reasoning across documents.

The measured result. We built an AI-ready dataset from 80 IETF RFCs through the enterprise-SOTA wizard on a Blackwell GPU, then measured retrieval against a live Qdrant instance — no mocks anywhere in the measured path. For each cross-reference chain, an LLM wrote a passage-grounded question framed via the citing document; a question was kept only if the fused stack beat both dense-only and BM25-only. On the questions that genuinely require multi-hop reasoning:

retrieval configuration recall@10 · all 90 questions recall@10 · the 7 multi-hop cases
dense only (Granite → Qdrant) 0.644 0.000
BM25 only 0.656 0.000
fused — dense + BM25 + KG 2-hop + rerank 0.678 1.000

Read honestly: across the broad 90-question set the aggregate lift is modest (+3 points), because most questions are answerable by a single signal, so fusion merely ties them. The advantage lands exactly where it should — on the 7 questions that require chaining evidence across documents, where single signals score zero and the fused knowledge-graph stack scores perfect. Seven of ninety (~8%) are genuine multi-hop wins; that is the true rate, not padded. The defensible claim is not "fused is several times better everywhere" — it is that the knowledge-graph stack unlocks a class of queries that single retrievers structurally cannot answer.

One detail from Act I proved decisive here, and it is a good illustration of why the whole pipeline matters for the retrieval result: the induced knowledge graph initially recovered only 14% of the corpus's real cross-reference edges — the header-only extractor missed the inline body citations that are the bulk of them. Adding the deterministic inline_refs extractor lifted coverage to ~112% of the gold graph. Those recovered edges are precisely what the 2-hop expander traverses — no edges, no multi-hop win. The retrieval result is only as good as the graph, and the graph is only as good as the extraction. The system is a chain, and every link was made to hold.


Act V — Enterprise-readiness is an architecture, not a checklist

The reasons this is deployable in a real organization are structural, not aspirational:

  • Model-agnostic by construction. The core names only Capabilities — narrow interfaces like Parser, EntityExtractor, PIIDetector, Embedder. Every concrete model lives in its own package behind a Provider plugin. Swap the OCR VLM, the induction LLM, the embedder, the reranker — the pipeline contract doesn't move. Heavy dependencies never touch the core, so discovery stays lightweight.
  • Cloud- and engine-agnostic. The deliverable is files, and the retrieval layer is stateless and bring-your-own-engine. Nothing is locked to a vendor's store or a vendor's cloud. You adopt the quality layer on top of the infrastructure you already run.
  • Licensing is attested, separately for weights and code. Every reference model above is permissive (Apache-2.0 or MIT), and the retrieval-side reranker, SPLADE, and multi-vector encoders pin no checkpoint — you name a cleared one. A restricted-license model is always an explicit, operator-made choice, never a silent default.
  • PII safety is non-bypassable. The financial-PII floor is enforced at a shared seam under every redaction provider, so it holds regardless of which model you plug in.
  • The "AI-ready" claim is substantiated, not asserted — a first-class Quality Report ships with every run, and incremental corpus deltas keep it current without full reprocessing.
  • It is honest about its edges. The multi-vector path is marked experimental; SPLADE was scoped out of the proof and said so; the measured lift is reported with its real, modest aggregate alongside the decisive multi-hop result. An evaluator can trust the numbers precisely because nothing is dressed up.

Why this is a true solution for messy enterprise data

Because it does not pretend the mess away. It parses the formats real organizations actually have, extracts the structure — entities, relations, and the cross-references that connect documents — that turns a pile of files into a knowledge graph, keeps that graph auditable and wired into the corpus, and then makes it do work at query time by letting retrieval reason across documents instead of matching text within one. It does all of this behind swappable models, on your own engine and cloud, emitting portable files you can inspect — and it proves, on real hardware against a live index, that the graph recovers answers single-signal retrieval structurally cannot.

That last part is the whole argument in one line: single-hop fused retrieval matches text; multi-hop knowledge-graph retrieval follows the references — and following the references is what answering a real enterprise question actually requires.


Artifacts referenced throughout — the AI-ready dataset, the knowledge graph, the benchmark harness, per-stage correctness charts, and every question with its per-signal recall — are packaged under results/dogfood-rfc/. The measured proof is in results/dogfood-rfc/PROOF.md; a shareable rendering accompanies it. Model ids, provider ids, and ADR references in this document are verified against the source on branch feat/dogfood-fixes.