# Why Basic RAG Fails in M&A Due Diligence: Architecting Zero-Drop AST Dependency Graphs for Legal…

> Source: <https://pub.towardsai.net/why-basic-rag-fails-in-m-a-due-diligence-architecting-zero-drop-ast-dependency-graphs-for-legal-c9866c81afc6?source=rss----98111c9905da---4>
> Published: 2026-08-18 12:01:01+00:00

Your autonomous document review agent parsed a 500-page M&A purchase agreement in 94 seconds. It extracted all core reps, warranties, and closing conditions, producing an executive summary that flagged zero high-risk anomalies.

Sixty days post-close, litigation hits. Outside counsel discovers an unhedged $14 million environmental liability carve-out buried in Schedule 4.12(b) — completely omitted from the deal team’s risk matrix.

Your observability stack in Datadog reported 100% uptime. Your vector database returned top-k similarity matches with cosine distances above 0.88. The LLM completion returned an HTTP 200 OK.

Yet the mission-critical clause was completely dropped from the reasoning loop.

This is not a prompt engineering failure. It is a fundamental architecture failure born from applying naive Retrieval-Augmented Generation (RAG) to non-linear legal instruments.

```
+--------------------------------------------------------------------------------------------------+|                            THE NAIVE RAG CONTEXT TRUNCATION COLLAPSE                             |+--------------------------------------------------------------------------------------------------+[Page 3: Definitions] ────────┐  ("Indemnified Claims")        │                                ▼  [Page 42: Section 11.4] ──────┼──► [Fixed 512-Token Chunking] ──► [Boundary Split Severance]  (Operative Liability Cap)     │                                                │                                │                                                ▼  [Page 94: Schedule 4.12(b)] ──┘                                   ┌────────────────────────────┐  (Environmental Carve-Out)                                         │ LLM Hallucinates Standard  │                                                                    │ Boilerplate; Drops $14M    │                                                                    │ Liability Carve-Out.       │                                                                    └────────────────────────────┘
```

Standard RAG pipelines assume documents are linear narrative streams. They ingest text, split it by arbitrary character or token counts (typically 256 to 1024 tokens), compute dense vector embeddings, and retrieve chunks based on semantic similarity.

In enterprise legal operations, contracts are **directed acyclic graphs (DAGs)** wrapped in multi-column, nested typographic layouts. Applying naive RAG to these structures triggers three architectural failure vectors:

Legal provisions rarely exist within a single paragraph. An operative covenant in Section 11.4 frequently states:

“Subject to the limitations set forth in Section 18.2(b), and except for Special Liabilities defined in Schedule 4.12(b), Seller’s aggregate liability under this Agreement shall not exceed the Cap Amount.”

When a chunking boundary splits after *“Section 18.2(b)”*, the qualification *“except for Special Liabilities defined in Schedule 4.12(b)”* lands in the subsequent chunk.

When the user queries *“What is the seller’s aggregate liability cap?”*, the vector database matches the first chunk. The retriever feeds only the capped threshold to the LLM, permanently severing the carve-out exception before inference begins.

When classical software pipelines encounter a missing parameter, the deserialization layer throws a runtime KeyError or schema validation exception.

Probabilistic LLMs do not throw exceptions when context is missing; they improvise. Trained on massive corpuses of standard commercial contracts, an LLM receiving a truncated liability section assumes market-standard terms apply. It generates a confident summary confirming liability is capped, without signaling that an unlinked schedule reference was omitted.

Vector retrieval relies on cosine similarity between embedding spaces. However, the operative clause on Page 42 (*“Seller’s aggregate liability…”*) and the specific definition in Schedule 4.12(b) (*“Environmental remediation obligations arising under CERCLA at Facility 4”*) share minimal lexical or semantic overlap.

A vector search query targeting “liability caps” will mathematically deprioritize the environmental schedule chunk, ensuring it never enters the context window.

To eliminate silent context drops, we must replace naive vector slicing with a **Stateful AST Dependency Graph** that resolves cross-clause relationships prior to model ingestion.

```
DETERMINISTIC LEGAL EXTRACTION ARCHITECTURE                           [Multi-Column Enterprise Legal PDF]              │              ▼┌─────────────────────────────────────────────────────────────────────────────┐│                    STRUCTURAL OCR & LAYOUT ANALYSIS                         ││  • Bounding-Box Coordinate Extraction [x0, y0, x1, y1, page_num]            ││  • Reading-Order Reconstruction (Eliminating Column Spillage)               │└─────────────────────────────────────┬───────────────────────────────────────┘                                      │                                      ▼┌─────────────────────────────────────────────────────────────────────────────┐│                   SYMBOLIC AST & GRAPH LINKING ENGINE                       ││  • Extract Defined Terms ("Indemnified Liabilities", "Cap Amount")          ││  • Build Section Dependency Graph:                                          ││    (Section 11.4) ───[EXCEPT]───► (Schedule 4.12(b))                        ││    (Section 11.4) ───[SUBJECT_TO]► (Section 18.2(b))                        │└─────────────────────────────────────┬───────────────────────────────────────┘                                      │ [Emits Enriched Multi-Node Payload]                                      ▼┌─────────────────────────────────────────────────────────────────────────────┐│                   STATEFUL CONTROL TOWER & AUDIT GATEWAY                    ││                                                                             ││  ┌───────────────────────┐  ┌───────────────────────┐  ┌─────────────────┐  ││  │ Unresolved Reference  │  │ Coordinate Grounding  │  │ Schema Contract │  ││  │ Circuit Breaker       │  │ Audit Tracer          │  │ Validator       │  ││  └──────────┬────────────┘  └───────────┬───────────┘  └────────┬────────┘  ││             │                           │                       │           ││             └───────────────────────────┼───────────────────────┘           ││                                         │                                   ││                        [All Graph Pointers Resolved?]                       ││                                 /               \                           ││                             YES/                 \NO                        ││                               ▼                   ▼                         ││                    ┌──────────────────┐   ┌──────────────────────────────┐  ││                    │ Emit Verified    │   │ Trip Circuit Breaker: Halt   │  ││                    │ Extraction AST   │   │ Auto-Signoff; Alert Ops      │  ││                    └──────────────────┘   └──────────────────────────────┘  │└─────────────────────────────────────────────────────────────────────────────┘
```

The following Python implementation demonstrates how an enterprise control tower intercepts raw document structures, builds an Abstract Syntax Tree of cross-references, and halts execution if an exception clause cannot be deterministically resolved:

``` python
from pydantic import BaseModel, Fieldfrom typing import List, Dict, Optional, Setimport reclass BoundingBox(BaseModel):    page: int    coordinates: List[float]  # [x0, y0, x1, y1]class AstClauseNode(BaseModel):    node_id: str    section_number: str    raw_text: str    bounding_box: BoundingBox    outbound_references: Set[str] = Field(default_factory=set)    unresolved_dependencies: Set[str] = Field(default_factory=set)class DocumentSymbolTable(BaseModel):    defined_terms: Dict[str, str] = {}    sections: Dict[str, AstClauseNode] = {}    schedules: Dict[str, AstClauseNode] = {}class LegalGraphControlTower:    def __init__(self, symbol_table: DocumentSymbolTable):        self.symbol_table = symbol_table    def extract_explicit_references(self, node: AstClauseNode) -> None:        """        Parses legal regex patterns to identify structural cross-references        such as 'Section 18.2(b)' or 'Schedule 4.12(b)'.        """        section_pattern = r"(?:Section|Clause)\s+(\d+(?:\.\d+)*(?:\([a-z0-9]+\))*)"        schedule_pattern = r"(?:Schedule|Exhibit)\s+([A-Z0-9]+(?:\.\d+)*(?:\([a-z0-9]+\))*)"        found_sections = re.findall(section_pattern, node.raw_text, re.IGNORECASE)        found_schedules = re.findall(schedule_pattern, node.raw_text, re.IGNORECASE)        for sec in found_sections:            node.outbound_references.add(f"SEC_{sec}")        for sch in found_schedules:            node.outbound_references.add(f"SCH_{sch}")    def build_enriched_context(self, target_section: str) -> Dict[str, object]:        """        Resolves the target clause and all immediate exception dependencies        into a single composite context payload before invoking downstream LLMs.        """        if target_section not in self.symbol_table.sections:            raise KeyError(f"Section {target_section} does not exist in parsed symbol table.")        root_node = self.symbol_table.sections[target_section]        self.extract_explicit_references(root_node)        resolved_payload = {            "root_clause": root_node.raw_text,            "root_bbox": root_node.bounding_box.dict(),            "resolved_dependencies": []        }        for ref in root_node.outbound_references:            if ref.startswith("SEC_") and ref.replace("SEC_", "") in self.symbol_table.sections:                sec_key = ref.replace("SEC_", "")                resolved_payload["resolved_dependencies"].append({                    "ref_id": ref,                    "text": self.symbol_table.sections[sec_key].raw_text,                    "bbox": self.symbol_table.sections[sec_key].bounding_box.dict()                })            elif ref.startswith("SCH_") and ref.replace("SCH_", "") in self.symbol_table.schedules:                sch_key = ref.replace("SCH_", "")                resolved_payload["resolved_dependencies"].append({                    "ref_id": ref,                    "text": self.symbol_table.schedules[sch_key].raw_text,                    "bbox": self.symbol_table.schedules[sch_key].bounding_box.dict()                })            else:                root_node.unresolved_dependencies.add(ref)        # Hard Circuit Breaker: Halt pipeline if dependencies are severed        if root_node.unresolved_dependencies:            raise UnresolvedDependencyError(                f"Circuit Breaker Tripped: Section {target_section} contains unlinked "                f"dependencies: {list(root_node.unresolved_dependencies)}. Halting inference."            )        return resolved_payloadclass UnresolvedDependencyError(Exception):    """Raised when an agent tries to process a clause with truncated dependencies."""    pass
```

Treating a 500-page acquisition contract like a series of disconnected text chunks is a fundamental architectural flaw. When dealing with eight-figure transaction liabilities, probabilistic text retrieval cannot be the system boundary.

Production-grade legal tech requires a stateful control plane: parsing documents as interconnected abstract syntax trees, enforcing cross-reference integrity before context hits model memory, and grounding every extraction in immutable spatial coordinates.

*Head of Multi-Agent Architecture & Product @ Claire By The Algorithm*

Explore stateful digital labor at letsaskclaire.com.

[Why Basic RAG Fails in M&A Due Diligence: Architecting Zero-Drop AST Dependency Graphs for Legal…](https://pub.towardsai.net/why-basic-rag-fails-in-m-a-due-diligence-architecting-zero-drop-ast-dependency-graphs-for-legal-c9866c81afc6) was originally published in [Towards AI](https://pub.towardsai.net) on Medium, where people are continuing the conversation by highlighting and responding to this story.
