MEMON SYSTEMS

Independent analysis

A commercial configuration measured from outside, without its operator. The system is not named.

Retrieval Integrity Under Statute Collision in Legal RAG

Profile Commercial legal-RAG configuration, independent analysis (Spain)
Stack Audited system: black box, stack not observedSpecified control: spaCy router, hard metadata pre-filter, late-interaction rerank, NLI verificationBuilt for UK legislation (legal-rag-router): a deterministic citation grammar over a pre-built index and a boolean partition filter, no model in the path
Duration 2 Weeks, measurement and control design
Measured, audited system
3 of 3 article-collision probes returned the wrong statute: a commercial-code query answered with tax law, a fabricated sub-clause, and a repealed statute cited as current. Black-box probes against the live public interface, Spanish statutory law, Código de Comercio Art. 42 as ground truth, April 2026. Three probes establish reproducibility.
Measured, reference implementation
UK legislation. legal-rag-router, 29 September 2026, 2,112 labelled test queries fixed and hashed before the router ran on them, index snapshot 28 September 2026: 0 of 40 same-numbered sections bound to the wrong Act, 0 of 1,030 real citations set aside before development bound wrongly, 0 of 68 invented Acts bound, 82 of 1,030 real citations refused. Routing p99 2.10 ms on Apple M4, 30 September 2026.
By design
Deterministic statute pre-filtering binds the target code before vector embedding. A boolean index predicate returns chunks from the bound statute only, so cross-statute collision has nowhere to occur.

The Challenge

A legal-RAG platform marketed a sub-1% hallucination rate. On 3 of 3 black-box probes where one article number exists in several Spanish statutes, it answered from the wrong statute: tax law quoted as commercial code, an invented sub-clause, and a repealed law cited as current. A solicitor relying on any of these answers would breach SRA Principle 2. The cause sits in the retrieval tier, before the model: nothing binds the query to one statute.

This study pairs black-box probes of a live legal-RAG interface with a control and its reference implementation. §3 gives the three failed probes verbatim. §5 specifies a router that binds the statute before vector search. §5.5 records the implementation built for UK legislation and what it measured. §8 gives the argument that a boolean filter cannot return a chunk from an unbound statute.

Data table: Label, Meaning
Label Meaning
Measured, audited system Captured from the live audited system, April 2026.
Measured, reference implementation Run on my own code, legal-rag-router, on UK legislation (§5.5). Not run against the audited system or against Spanish law.
By design Reasoned from the architecture, or set as an acceptance threshold, and not run. Where it is arithmetic from component latencies and declared assumptions, it says modeled.
Inferred, Assumed Provenance of a model input (§4.1): read from the product, or declared.

1. Summary

In Spanish law one article number exists in many statutes. Artículo 42 is corporate group control in the Código de Comercio, solidary tax liability in the Ley General Tributaria, and subcontracting in the Estatuto de los Trabajadores.

A legal-RAG platform that markets a sub-1% hallucination rate returned the wrong statute on 3 of 3 article-collision probes, each time quoting the wrong text with full confidence. (Measured, audited system.)

The model was never given the right evidence. The two retrieval steps, dense (embedding) search and a ColBERT reranker, both score the colliding articles almost equally, so the retrieved set mixes statutes. The agent then loops for 30 seconds to 5 minutes and answers from the wrong one.

Under SRA Principles 2 and 7, the solicitor who relies on the answer carries the liability, wherever the error began. Under the EU AI Act, Articles 14 and 15, retrieval accuracy is part of the system's compliance case.

The control is a router that binds the statute before vector search and a boolean filter that restricts retrieval to it. A reference implementation exists for UK legislation (§5.5).


2. Why Vector Search Fails on Shared Article Numbers

2.1 Collision Space

Civil-law statutes number their articles independently, so numbers repeat across codes:

2.1 Collision Space: Article Number, Statute, Domain, Subject Matter (Data Table)
Article Number Statute Domain Subject Matter
Art. 42 Código de Comercio (CdC) Commercial Law Corporate group control & consolidation
Art. 42 Ley General Tributaria (LGT) Tax Law Solidary tax liability
Art. 42 Estatuto de los Trabajadores (ET) Labour Law Subcontracting obligations
Art. 42 Ley 15/1988 Arbitration (additional collision)

A query such as "¿Qué establece el Artículo 42?" ("What does Article 42 establish?") has to be resolved to one statute before the vector database is queried. A standard RAG pipeline does not do this.

2.2 Three Points of Failure

The audited pipeline is a standard retrieve-then-generate chain:

flowchart LR
    A["User Query: 'Art. 42 CdC'"] --> B["Embed Query"]
    B --> C["HNSW Dense Search"]
    C --> D["ColBERT Rerank"]
    D --> E["LLM Agent"]
    E --> F["Response"]

The failures in §3 are measured. The account below of why they happen is an argument.

Failure Point 1: Dense Search Returns All Three Statutes

A dense embedding model turns the whole query into one vector. For a query containing "Artículo 42", that vector sits close to every chunk that mentions "Artículo 42":

q=Embed(“Artıˊculo 42 del Coˊdigo de Comercio”)\mathbf{q} = \text{Embed}(\text{``Artículo 42 del Código de Comercio''})

dCdC=Embed(“Art. 42 CdC: control de grupo societario...”)\mathbf{d}_{\text{CdC}} = \text{Embed}(\text{``Art. 42 CdC: control de grupo societario...''})

dLGT=Embed(“Art. 42 LGT: responsabilidad tributaria solidaria...”)\mathbf{d}_{\text{LGT}} = \text{Embed}(\text{``Art. 42 LGT: responsabilidad tributaria solidaria...''})

dET=Embed(“Art. 42 ET: subcontratacioˊn de obras...”)\mathbf{d}_{\text{ET}} = \text{Embed}(\text{``Art. 42 ET: subcontratación de obras...''})

The shared anchor "Artículo 42" dominates the similarity, so the nearest-neighbour search returns chunks from all three statutes.

Failure Point 2: The Reranker Scores Them Equally

ColBERT scores each query token against its best-matching document token:

Score(q,d)=∑i=1∣q∣max⁡j∈{1,…,∣d∣}cos⁡(qi,dj)\text{Score}(q, d) = \sum_{i=1}^{|q|} \max_{j \in \{1, \dots, |d|\}} \cos(\mathbf{q}_i, \mathbf{d}_j)

The tokens ["Artículo", "42"] appear verbatim in all three statutes and score the same against each. The tokens that name the code are too few to break the tie:

Score(q,dCdC)≈Score(q,dLGT)≈Score(q,dET)\text{Score}(q, d_{\text{CdC}}) \approx \text{Score}(q, d_{\text{LGT}}) \approx \text{Score}(q, d_{\text{ET}})

The reranker returns clauses from unrelated laws with near-equal scores.

Failure Point 3: The Agent Loops

The agent receives conflicting chunks and cannot tell which statute the user meant, so it keeps searching:

graph TD
    A([Agent Receives Conflicting Chunks]) --> B{Can Agent Resolve<br/>Statute Identity?}
    B -->|No| C[Fetch More Context<br/>from Vector DB]
    C --> D{Still Ambiguous?}
    D -->|Yes| E[Execute Web Scraping<br/>Fallback]
    E --> F{Resolved?}
    F -->|No| G[Thrash: 29-32<br/>Reasoning Steps]
    G --> H[Budget Exhaustion<br/>or Timeout]
    H --> I[Generate Confident<br/>Hallucination]
    B -->|Yes| J([Generate Correct Response])
    D -->|No| J
    F -->|Yes| J

    style I fill:#000000,stroke:#dc2626,stroke-width:2px
    style J fill:#000000,stroke:#16a34a,stroke-width:2px
    style G fill:#000000,stroke:#f57c00,stroke-width:2px

Latency rises to 30 seconds to 5 minutes per query (Measured, audited system: wall-clock, client-side, across the test battery). The agent then answers from the wrong statute, with no disclaimer.

The model summarises the evidence it was given. The evidence was wrong before the model saw it. Prompting, a larger model or a check on the output does not change what was retrieved. The fix has to sit before the vector database query.


3. The Probes

Three probes were run against the live product, with Código de Comercio Artículo 42 as the target. Queries and answers are verbatim.

Test 1 (T3): Verbatim Sub-Clause, letra b) of Art. 42.1 CdC

The probe asked: "Reproduce the exact text of Art. 42.1, letra b) of the Código de Comercio."

The correct text (ground truth):

"Tenga la facultad de nombrar o destituir a la mayoría de los miembros del órgano de administración." (English: "Has the power to appoint or remove the majority of the members of the administrative body.") Art. 42.1 b) CdC (corporate group control: power to appoint or remove board members)

The system returned:

"Las que sean causantes o colaboren en la ocultación o transmisión de bienes... de la Administración tributaria." (English: "Those who cause or collaborate in the concealment or transfer of assets... of the Tax Administration.") Art. 42 of the Ley General Tributaria (solidary tax liability for concealment of assets)

The result is the wrong statute: tax-liability text quoted as commercial code, with no disclaimer.


Test 2 (T2): apartado c), the letra/apartado Distinction

The probe asked: "¿Qué establece el apartado c) del Artículo 42 del Código de Comercio?" (English: "What does paragraph c) of Article 42 of the Commercial Code establish?")

The correct answer is the commercial-law sub-clause apartado c) of Art. 42 CdC.

The system returned three failures:

  1. It did not use the Código de Comercio.
  2. It invented a premise: "según el contexto de los hechos que estamos tratando" ("according to the context of the facts we are discussing"), referring to a prior conversation that did not exist.
  3. It cited two wrong statutes: Art. 42.4 c) of the Estatuto de los Trabajadores (subcontracting) and Ley 15/1988 (arbitration).

The result is labour law and arbitration text for a commercial-law query, and an invented premise.


Test 3 (T1): Sub-Clause That Does Not Exist, apartado 8

The probe asked: "¿Qué establece el apartado 8 del Artículo 42 del Código de Comercio?" (English: "What does paragraph 8 of Article 42 of the Commercial Code establish?")

The correct answer is that Art. 42 CdC has no apartado 8: "El Artículo 42 del Código de Comercio no contiene un apartado 8." (English: "Article 42 of the Commercial Code does not contain a paragraph 8.")

The system returned:

  1. It invented text for apartado 8:

    "Se presumirá que existe control cuando una sociedad, por cualquier otro medio, pueda ejercer una influencia dominante sobre otra." (English: "It shall be presumed that control exists when a company, by any other means, can exercise a dominant influence over another.") No such provision exists in Art. 42 CdC.

  2. It invented a source: "De acuerdo con la información proporcionada en el análisis previo" ("According to the information provided in the previous analysis"). There was no previous analysis.
  3. It cited repealed law as current: the "unidad de decisión" ("unity of decision") concept from Ley 62/2003, repealed by Ley 16/2007.

The result is three failures in one answer: an invented sub-clause, an invented source and a repealed law.

The French version of this battery, on L233-3 and L233-10, which are near-unique identifiers, passed. That control condition ties the failure to article-number collision, not to the model.

3.1 Sample Size

Three probes establish that the failure exists and reproduces. They do not give its frequency.

  • Every collision probe failed, and the French control passed.
  • §2 and §8 explain why any collision query meets the same failure, which is why three failures characterise the class.
  • The failure rate over the vendor's real traffic is unknown. The α=5%\alpha = 5\% collision rate in §4.2 is an assumption.

A larger battery would tighten the frequency estimate. It would not change the argument.

3.2 Corpus and Ground Truth

Each answer was checked against the consolidated text of the Código de Comercio in my own reference corpus. The correct text of Art. 42.1 b) is known; the system returned Art. 42 of the Ley General Tributaria instead.

Two limits:

  • The ground truth is my copy of the law. A reader can check the three answers against the BOE's consolidated text of each statute.
  • Temporal validity was not tested systematically. The repealed-law failure in T1 appeared inside a collision probe. Whether the system returns the provision in force on a given date is a separate question, and the control in §5 does not address it (§10.3).

The collision finding stands: the wrong statute, checked against the right text, three times out of three.


4. Cost of Agent Thrashing

4.1 Per-Query Cost

With mixed chunks, the agent requests more context, falls back to web scraping, and runs 29 to 32 reasoning steps, each an LLM call.

Let nn be the number of steps, cstepc_{\text{step}} the cost per step and tstept_{\text{step}} the latency per step:

Cthrash=n⋅cstepC_{\text{thrash}} = n \cdot c_{\text{step}}

Tthrash=n⋅tstepT_{\text{thrash}} = n \cdot t_{\text{step}}

4.1 Per-Query Cost: Input, Value, Provenance (Data Table)
Input Value Provenance
nn, ReAct steps per collision query 30 Inferred, read from the product's step indicator
cstepc_{\text{step}}, cost per agent step $0.003 Assumed, mid-tier frontier model, about 1k output tokens per step at published rates
tstept_{\text{step}}, latency per step 6s By design, modeled, measured wall-clock latency divided by the inferred step count

Cthrash=30×$0.003=$0.09 per failed queryC_{\text{thrash}} = 30 \times \$0.003 = \$0.09 \text{ per failed query}

Tthrash=30×6s=180s (3 minutes)T_{\text{thrash}} = 30 \times 6\text{s} = 180\text{s} \text{ (3 minutes)}

180 s sits inside the measured 30 s to 5 min range, the only cross-check available.

4.2 Monthly Cost at Scale

Let α\alpha be the share of queries that hit a colliding article number (Art. 1, 42, 50, 100 and others):

Daily Thrashing Cost=Qdaily⋅α⋅Cthrash\text{Daily Thrashing Cost} = Q_{\text{daily}} \cdot \alpha \cdot C_{\text{thrash}}

With Qdaily=50,000Q_{\text{daily}} = 50{,}000 and α=5%\alpha = 5\%:

Daily Cost=50,000×0.05×$0.09=$225/day=$6,750/month\text{Daily Cost} = 50{,}000 \times 0.05 \times \$0.09 = \$225/\text{day} = \$6{,}750/\text{month}

Warning

QdailyQ_{\text{daily}} and α\alpha are illustrative. I have no view of the vendor's traffic. The monthly figure is linear in both: $1,350 at α=1%\alpha = 1\%, $13,500 at α=10%\alpha = 10\%. The figure to rely on is the per-query cost.

This is compute spent on steps that end in a wrong answer. It excludes the cost of the wrong answers themselves.

4.3 Latency Under Collision

Tquery={Tnormal≈2–5sif article number is unique (no collision)Tthrash≈30–300sif article number collides across statutesT_{\text{query}} = \begin{cases} T_{\text{normal}} \approx 2\text{–}5\text{s} & \text{if article number is unique (no collision)} \\ T_{\text{thrash}} \approx 30\text{–}300\text{s} & \text{if article number collides across statutes} \end{cases}

Both branches are Measured, audited system: the fast one from the French control, the slow one from the collision battery.

A mean-latency dashboard hides this split. The slow branch is predictable: it is every query on a colliding article number.


5. The Control: A Deterministic Statute-Binding Router

§5.1 to §5.4 are the specification as written in April 2026. §5.5 records the reference implementation, legal-rag-router, and where it departs from the specification. It covers UK legislation.

5.1 Principle

The vector database, embedding model and generation tier stay as they are. One deterministic routing step is added between the query and retrieval.

It runs three operations on local CPU (about 7 ms in total, By design, modeled from published component benchmarks):

  1. Entity extraction reads article numbers, statute names and abbreviations from the query.
  2. A collision check looks the article number up in a collision index.
  3. The router then attaches a metadata filter to the database query, or asks the user which statute they mean.

5.2 Before and After

Before: Unfiltered Retrieval in the Audited System

graph LR
    Q([User Query:<br/>'Art. 42 CdC']) --> Embed[Dense Embedding]
    Embed --> HNSW[HNSW Search<br/>Milvus]
    HNSW --> Mixed[Mixed Chunks:<br/>CdC + LGT + ET]
    Mixed --> ColBERT[ColBERT Rerank<br/>Scores ≈ Equal]
    ColBERT --> Agent[ReAct Agent<br/>30-32 Steps]
    Agent --> Wrong[Wrong Statute<br/>Confident Hallucination]

    style Mixed fill:#000000,stroke:#dc2626,stroke-width:2px
    style Wrong fill:#000000,stroke:#dc2626,stroke-width:2px
    style Agent fill:#000000,stroke:#f57c00,stroke-width:2px

After: Router Inserted, as Specified

graph TD
    Q([User Query]) --> Router

    subgraph SBR ["Statute-Binding Router (~7ms, Local CPU)"]
        Router[spaCy + DistilBERT<br/>Entity Extraction] --> Parse[Extract:<br/>article_num, statute_keyword]
        Parse --> Check{Collision<br/>Detected?}
    end

    Check -->|No Collision| Bind[Bind Metadata Filter<br/>to Vector Query]
    Check -->|Collision + Keyword Found| Bind
    Check -->|Collision + No Keyword| Intercept[Intercept → Ask User:<br/>'By Art. 42, did you mean<br/>CdC, LGT, or ET?']

    Intercept -->|User Selects| Bind
    Bind --> DB[(Vector DB<br/>Hard-Filtered<br/>statute_id = 'CdC')]
    DB --> ColBERT2[ColBERT Rerank<br/>Clean Candidates]
    ColBERT2 --> LLM[LLM Generation<br/>Correct Evidence]
    LLM --> Correct([Correct Response])

    style SBR fill:#000000,stroke:#1a73e8,stroke-width:2px
    style Correct fill:#000000,stroke:#16a34a,stroke-width:2px
    style Intercept fill:#000000,stroke:#f57c00,stroke-width:2px

5.3 The Metadata Filter

Once the statute is bound, from the query or from the user's answer, the database query carries a hard predicate:

Filter: {statute_id=“CdC”,  article_num=“42”}\text{Filter: } \{\texttt{statute\_id} = \text{``CdC''}, \;\texttt{article\_num} = \text{``42''}\}

It is applied before candidate generation, not after retrieval, so the reranker only sees chunks from the bound statute.

5.4 The Collision Index

A pre-computed table from article numbers to the statutes that contain them:

CollisionIndex:N→P(StatuteID)\text{CollisionIndex}: \mathbb{N} \to \mathcal{P}(\text{StatuteID})

CollisionIndex(42)={CdC,LGT,ET,Ley 15/1988}\text{CollisionIndex}(42) = \{\text{CdC}, \text{LGT}, \text{ET}, \text{Ley 15/1988}\}

If ∣CollisionIndex(n)∣>1|\text{CollisionIndex}(n)| > 1, the number is ambiguous and the router resolves it before retrieval. The index is built by the same extraction that tags the corpus, so query and corpus use the same vocabulary.

5.5 The Reference Implementation: legal-rag-router

azterizm/legal-rag-router implements the control. 0.1.0 was released on PyPI on 2 October 2026 and is listed on /tools. The figures below were measured on 0.1.0. 0.1.1 (17 October 2026) adds "as at" and "as of" dates and changes no binding against 0.1.0 over 102,184 queries.

The scope is UK legislation. It binds UK Public General Acts and UK Statutory Instruments to provision level: 134,219 instruments, index snapshot 28 September 2026. The index is built from legislation.gov.uk, a public source versioned by date, and published with checksums. A third party can download or rebuild it and re-run the sealed batteries without relying on my copy of the law. Scottish, Welsh and Northern Ireland legislation and EU law are reported out of coverage. Spain (BOE) is the next jurisdiction on the roadmap. The three Spanish probes and the acceptance test in §6.3 have not been run against it.

What changed from specification to build:

5.5 The Reference Implementation: legal-rag-router: Specification (§5.1–5.4), As built (UK) (Data Table)
Specification (§5.1–5.4) As built (UK)
Entity extraction with spaCy + DistilBERT A deterministic citation grammar over a pre-built index. No model in the path; the package has no runtime dependencies and makes no network calls.
Collision index: article number → statutes Title, abbreviation, chapter-number and SI-number lookup tables over the whole UK statute book. "section 1 of the Companies Act" returns ROUTE_AMBIGUOUS with the candidate Acts (1976, 1985, 1989, 2006 …) and a clarification question.
Hard metadata filter on statute_id, article_num partition_filter() returns a boolean filter over instrument_id and coordinate, and raises for any status other than ROUTE_BOUNDED, so there is no unfiltered fallback.
Intercept → ask the user ROUTE_AMBIGUOUS → ASK_USER. A cited Act that does not exist → REFUSE, with the index date it was checked against.
binding_confidence in the audit record No confidence score. Each result carries one of six statuses, a next action, the bound coordinates, the index snapshot and a reason. Query text is not logged by default.
~7 ms routing, By design, modeled p50 0.14 ms, p99 2.10 ms, Measured, reference implementation (below).
Temporal validity out of scope (§10.3) Out of scope for the router. A repealed instrument is bound and flagged repealed; point-in-time text such as "as it stood in 2012", and since 0.1.1 "as at" and "as of" dates, is passed through for a separate layer. One such layer, for three sections of the Employment Rights Act 1996, is measured in the agentic thrashing case study, §5.5.

Measured, reference implementation, UK legislation, sealed run of 29 September 2026: 2,112 labelled queries, hashed before the router ran on them. 1,030 are held-out real citations: taken from legislation and set aside before development; index snapshot 28 September 2026; Python 3.11, macOS arm64.

5.5 The Reference Implementation: legal-rag-router: Measure, Result (Data Table)
Measure Result
Same section number in different Acts, bound to the wrong Act (the UK counterpart of Artículo 42) 0 of 40
Held-out real citations bound wrongly 0 of 1,030
Invented Acts bound 0 of 68 (all 68 refused)
Held-out real citations refused 82 of 1,030, the cost of refusing instead of guessing

Measured, reference implementation, routing latency, 30 September 2026, full UK index, 306,336 calls × 3 passes: p50 0.14 ms and p99 2.10 ms on Apple M4; p50 0.50 ms and p99 9.21 ms on an Intel i5-3570 (2012). Both are under Gate 5's 25 ms p99 (§7). The design target of p99 under 2 ms is missed on both. The tail is refusals, which rank suggested titles.

Measured, reference implementation, coverage sweep, full UK index, snapshot 28 September 2026: 100,000 real citations from the statute book, each routed in the clause where a drafter wrote it.

5.5 The Reference Implementation: legal-rag-router: Outcome, Share of 100,000 (Data Table)
Outcome Share of 100,000
Bound correctly 80.02%
Missed (not recognised) 8.54%. 6,914 of the 8,541 misses are a bare N/N number such as 1999/3434, which the router does not bind on purpose
Refused (real law) 6.33%
Asked the user to choose 4.37%
Bound to another instrument 7 (0.007%). All 7 are wrong link targets in the source text

Measured, reference implementation, discovery on queries that cite nothing, sealed test set of 91 drafted queries, 29 September 2026: the right provision is first for 51% and in the top 10 for 88%. Discovery offers candidates and binds none.

Measured, reference implementation against Gemini 3.8 Flash (high), the same 2,112 queries, 1 October 2026: Gemini routing on its own bound the wrong instrument for 102 of 1,897 real citations, changed its answer between repeats on 34 of 200 rows, and took 13.5 s at p50. Used as a citation parser in front of the router's index, it was the most accurate pipeline measured: 1,972 of 2,112 rows as expected, with 0 wrong and 0 invented bindings. The model supplies recall; the index decides what is bound. Report: comparison-uk.md.

These figures measure the binding step, which §10.1 names as the residual risk, on UK citations. They do not test §8's claim inside a vector store, and they say nothing about Spanish law. Method, seals and reports are in the repository.


6. Projected Latency and Cost

6.1 Projected Latency

With one statute bound, the agent receives no conflicting evidence and has nothing to loop on (§8).

Tbefore=Tembed+THNSW+TColBERT+TthrashT_{\text{before}} = T_{\text{embed}} + T_{\text{HNSW}} + T_{\text{ColBERT}} + T_{\text{thrash}}

Tbefore≈0.2s+0.1s+0.3s+180s=180.6sT_{\text{before}} \approx 0.2\text{s} + 0.1\text{s} + 0.3\text{s} + 180\text{s} = 180.6\text{s}

Tafter=Trouter+Tembed+THNSW_filtered+TColBERT+TLLMT_{\text{after}} = T_{\text{router}} + T_{\text{embed}} + T_{\text{HNSW\_filtered}} + T_{\text{ColBERT}} + T_{\text{LLM}}

Tafter≈0.007s+0.2s+0.05s+0.15s+2s=2.4sT_{\text{after}} \approx 0.007\text{s} + 0.2\text{s} + 0.05\text{s} + 0.15\text{s} + 2\text{s} = 2.4\text{s}

TbeforeTafter=180.62.4≈75× (modeled latency reduction)\boxed{\frac{T_{\text{before}}}{T_{\text{after}}} = \frac{180.6}{2.4} \approx 75\times \text{ (modeled latency reduction)}}

Filtered search and reranking are modeled as faster because the filter shrinks the candidate set.

Tip

Almost all of the 75× comes from removing TthrashT_{\text{thrash}}. The other terms move by tenths of a second. The question to test is whether binding the statute stops the agent looping, which §8 argues and this section does not measure. If the router binds the wrong statute (§10), the user gets a fast wrong answer, which is why the audit record is part of the control.

6.2 Projected Cost

6.2 Projected Cost: Metric, Before (Unfiltered), After (Statute-Bound), Provenance (Data Table)
Metric Before (Unfiltered) After (Statute-Bound) Provenance
ReAct steps per collision query 29–32 0 Inferred → By design (no conflicting evidence to resolve)
LLM calls per collision query 30+ (agent reasoning) 1 (generation only) Inferred → By design
Per-query cost (collision) ~$0.09 ~$0.003 By design, modeled (both)
Monthly OPEX at 2,500 collision queries/day $6,750 $225 By design, modeled, assumes α\alpha, QQ from §4.2
Monthly savings $6,525 By design, modeled
Routing overhead 0ms (no router exists) 7ms By design, modeled from component benchmarks
Routing cost $0 ~$0 (local CPU) By design, no per-token billing on local inference

6.3 Accuracy

The left column is what the live system did. The right column is what the control is designed to do, and why.

6.3 Accuracy: Metric, Measured, audited system, Under the specified control, Basis for the Right Column (Data Table)
Metric Measured, audited system Under the specified control Basis for the Right Column
Accuracy on collision battery 0/3 (0%) Not run By design. Re-running this battery is the acceptance test
Wrong-statute retrieval rate 3 of 3 on the collision batch 0% By design. The index predicate cannot return a chunk whose statute_id ≠ the bound value (§8)
Fabricated-text rate 2/3 queries Lower, above zero By design, modeled. Correct evidence removes the cause seen here; the generator can still invent text
Repealed-law citation rate 1/3 queries (Ley 62/2003, repealed by Ley 16/2007) Needs a separate control Not addressed. Temporal validity is a corpus versioning problem (§10.3)

7. Deployment Specification

The router is specified for a shadow migration with no downtime.

Phase 1: Shadow Routing

Production runs unchanged. All production traffic is mirrored asynchronously to a staging instance with the router. No user sees staging output. Staging logs:

  • whether the router detected a collision
  • whether the query named a resolvable statute
  • whether the staging answer matched production
  • the latency difference

Phase 2: Acceptance Gates

At least 72 hours, with cutover gated on these thresholds:

Phase 2: Acceptance Gates: , Gate, Threshold to Proceed, If the Gate Fails (Data Table)
# Gate Threshold to Proceed If the Gate Fails
1 Collision false-positive rate: router flags a collision that does not exist < 0.1% The collision index has a spurious entry; check index generation against the corpus
2 Collision false-negative rate: router misses a real collision 0% on the full article-number mapping The index is incomplete; regenerate it from the full extraction
3 Statute-binding precision: bound statute matches the user's intent where intent is clear ≥ 99% Extraction is the weak link; expand the abbreviation gazetteer (§10.1)
4 Interception rate: share of queries sent to the user to choose Within a ceiling agreed with product Too high adds friction; too low risks silent wrong binding. A product decision
5 Routing latency, p99 < 25ms Profile the extraction pipeline and index lookup
6 Accuracy delta: staging correct where production was wrong Positive and rising over the soak If not positive, §8 does not hold for this corpus and the migration stops.

Note

Gate 2 is set at 0% because the collision index is a pre-computed mapping over a finite corpus. A missed collision is a data-generation bug with a mechanical fix. Gate 3 is a statistical target, because extraction from free text is statistical. The two gates separate what the design guarantees from what it achieves on average.

Phase 3: Cutover

A feature flag moves routing to the staging pipeline. The unfiltered path is removed after a bake period. Rollback is one flag change.

The filter assumes statute_id and article_num already exist in the vector database schema. That is a standard field set for a statutory corpus, but unverified for the audited product. Without them a re-index is needed, and the "no database rebuild" line in §9 does not hold.


8. Why a Larger Embedding Model Does Not Fix This

Every By design claim here rests on this argument. It is about the representation, so it needs no traffic or benchmark. A reader who rejects it has reason to reject the specification.

The Embedding Argument (Informal)

A dense model maps text to one vector v∈Rd\mathbf{v} \in \mathbb{R}^d. Similarity between query q\mathbf{q} and document d\mathbf{d} is:

sim(q,d)=q⋅d∥q∥⋅∥d∥\text{sim}(\mathbf{q}, \mathbf{d}) = \frac{\mathbf{q} \cdot \mathbf{d}}{\|\mathbf{q}\| \cdot \|\mathbf{d}\|}

Two documents that share the anchor "Artículo 42" share a large component along it:

dCdC=α⋅eArt42+β⋅ecommercial\mathbf{d}_{\text{CdC}} = \alpha \cdot \mathbf{e}_{\text{Art42}} + \beta \cdot \mathbf{e}_{\text{commercial}}

dLGT=α⋅eArt42+γ⋅etax\mathbf{d}_{\text{LGT}} = \alpha \cdot \mathbf{e}_{\text{Art42}} + \gamma \cdot \mathbf{e}_{\text{tax}}

With α≫β,γ\alpha \gg \beta, \gamma, the α\alpha term dominates both similarities:

sim(q,dCdC)≈sim(q,dLGT)≈α2∥q∥⋅∥d∥\text{sim}(\mathbf{q}, \mathbf{d}_{\text{CdC}}) \approx \text{sim}(\mathbf{q}, \mathbf{d}_{\text{LGT}}) \approx \frac{\alpha^2}{\|\mathbf{q}\| \cdot \|\mathbf{d}\|}

The terms that tell the statutes apart, β⋅ecommercial\beta \cdot \mathbf{e}_{\text{commercial}} and γ⋅etax\gamma \cdot \mathbf{e}_{\text{tax}}, are small next to α\alpha.

More dimensions do not change this. The shared anchor still carries most of the dot product. A single vector cannot express "Art. 42 of the CdC only" as a hard condition.

Note

This is an argument about a dominant shared term, not a proof with bounds on α/β\alpha/\beta. It predicts the score collapse that §3 measured. A model trained to weight statute names more heavily would raise β\beta. The claim is that this is a tuning race against a boolean condition that already wins, not that the race cannot be won.

Why the Metadata Filter Holds

The filter works outside the embedding space, as a condition on the index:

Candidates={d∈D∣statute_id(d)=“CdC”∧article_num(d)=“42”}\text{Candidates} = \{d \in \mathcal{D} \mid \texttt{statute\_id}(d) = \text{``CdC''} \wedge \texttt{article\_num}(d) = \text{``42''}\}

It is a boolean condition, not a score. Lexical overlap cannot move it.

Metadata filtering∈O(1) precision.Dense similarity∈O ⁣(1collision count) precision.\boxed{\text{Metadata filtering} \in O(1) \text{ precision.} \quad \text{Dense similarity} \in O\!\left(\frac{1}{\text{collision count}}\right) \text{ precision.}}

This is the basis of the 0% claim. If statute_id(d)≠“CdC”\texttt{statute\_id}(d) \neq \text{``CdC''}, then d∉Candidatesd \notin \text{Candidates}. It is not ranked lower; it is outside the set. For a correctly bound query the wrong-statute rate is zero, whatever the corpus size, model or traffic.

The remaining risk is the binding step: whether the router picks the right statute_id. That risk is statistical, and §10 covers it.


9. Conclusion

A legal-tech platform marketed a sub-1% hallucination rate. On black-box probes against Spanish statutory law, it returned the wrong statute on 3 of 3 article-collision queries: tax liability quoted as commercial code, an invented sub-clause, and a repealed statute cited as current. A solicitor relying on any of them would breach SRA Principle 2. This half is Measured, audited system.

The cause was the retrieval tier. Dense search and ColBERT reranking both score colliding articles almost equally, so the model received a mixed evidence set. The agent then looped for minutes and answered from the wrong law.

The control is one added step: a router that binds the statute and a boolean filter that restricts retrieval to it, before any vector similarity is computed (§8). This half is By design. A reference implementation exists for UK legislation, legal-rag-router (§5.5). It has not been deployed in front of the audited system and does not yet cover Spanish law.

Results

Results: Metric, Measured, audited system, Target state (specification), Provenance of Target (Data Table)
Metric Measured, audited system Target state (specification) Provenance of Target
Fabricated citation rate (cross-statute queries) 3 of 3, wrong law cited with full confidence 0% wrong-statute retrieval By design, follows from §8
Retrieval auditability No record of which statute was selected or why Full: every query records its binding decision By design. The UK implementation's per-result record is in §5.5
Collision test pass rate 0/3 Not run By design. This battery is the acceptance test; the UK implementation (§5.5) does not cover Spanish law
Compliance with sub-1% hallucination guarantee Broken on every collision query Defensible for this failure class By design, disambiguation only
Enterprise procurement readiness (TPRM) Fails the retrieval accuracy section Answers the retrieval accuracy section By design: control, audit record, measurable objective
Latency (secondary) 30s – 5 min (agent thrashing) ~2.4s (75× reduction) By design, modeled, §6.1
Monthly OPEX (secondary) ~$6,750 wasted compute ~$225 By design, modeled, assumes QQ, α\alpha from §4.2
Database rebuild required No Assumed, if the schema fields exist (§7)
Deployment risk Shadow migration, no downtime By design, protocol in §7, not run

This table compares the audited system with the specification. The reference implementation's results are on UK legislation, so they are stated separately.

Reference Implementation (UK)

legal-rag-router 0.1.0 implements the binding step and the filter for UK legislation; 0.1.1 changes no binding. Measured, reference implementation, blind sealed run, 29 September 2026, 2,112 labelled queries, index snapshot 28 September 2026: 0 of 40 same-numbered sections bound to the wrong Act; 0 of 1,030 held-out real citations bound wrongly; 0 of 68 invented Acts bound; 82 of 1,030 real citations refused. Routing latency p99 2.10 ms on Apple M4, 30 September 2026. Details in §5.5; listed on /tools.

Lessons

  1. An output check does not see a retrieval failure. Most AI compliance frameworks, including the NIST AI RMF and ISO 42001, place controls on the output: hallucination detection, filtering, human review. A system can pass all of them and still fail every probe of a predictable class of retrieval queries. If the retriever returns tax law for a commercial-law question, the model summarises tax law. An audit without retrieval probes misses this.

  2. Dense embeddings cannot hold a hard identity condition. When one signal, such as "Artículo 42", is shared by different statutes, similarity scores converge whatever the model or dimension. A metadata filter works outside the embedding space and does not degrade with the number of collisions.

  3. An agent without retrieval constraints amplifies the error. A ReAct loop fetches more context when unsure. If retrieval is the source of the error, more context adds more of it: more steps, more compute, a confident wrong answer. Binding the statute first removes the loop.

  4. The control is small. Specified as a Python service with spaCy and a lookup table, about 7 ms modeled. Built for UK legislation as a dependency-free library with no model in the path: p99 2.10 ms on Apple M4, and no rebuild where chunks already carry instrument and provision identifiers. It binds the citations it can read and refuses or asks on the rest.

  5. New corpora break pipeline assumptions. Embed, search, rerank, generate assumes similarity tracks document identity. That holds for unique identifiers such as French codes (L233-3) and fails for shared ones such as Spanish Artículo 42. A RAG system entering a new legal domain needs a check for identifier collision first.

  6. An aggregate hallucination rate does not cover a predictable class. The vendor's sub-1% figure may hold over all traffic and still fail every collision query tried. Regulated buyers need a control and an audit record per known failure mode.

  7. Label how each claim is supported. The strongest claim here, 0% wrong-statute retrieval, has no measurement behind it; it follows from a boolean predicate. The largest numbers, 75× and $6,525 a month, are the weakest, because they rest on assumptions. One unlabelled results table would hide that difference.


10. Residual Risks and Falsifiers

The filter is the reliable part. The risk is in the binding step before it, and one of the three failures seen in §3 is outside this control.

10.1 The Binding Step Is Statistical

§8 holds for a correctly bound query. Extracting statute_id from free text carries normal extraction error:

  • Statute names have abbreviations and variants. "CdC", "C. de C.", "Código de Comercio", "el código mercantil" and "mercantil" all mean the same statute. Recall depends on how complete the gazetteer is.
  • Queries can be colloquial or in another language. Catalan, Galician and Basque statute names, and mixed Spanish and English, sit outside a gazetteer built from official titles.
  • A wrong binding is worse than a loop. If the router binds the wrong statute, the filter returns clean chunks from the wrong law and the user gets a fast, well-grounded wrong answer with no sign of doubt. The audit record makes this traceable; Gate 3 in §7 is there to catch it before cutover.

Measured, reference implementation, UK legislation (§5.5): 0 of 1,030 held-out real citations were bound wrongly, and 7 of 100,000 citations in the sweep, all 7 from wrong link targets in the source text. The cost of refusing instead of guessing is 82 of 1,030 held-out citations refused (8.0%) and 8.54% of the sweep not recognised. UK citations usually carry a title and a year. Spanish citations such as "art. 42 CdC" or a bare "Artículo 42" carry less, so the Spanish rates are unknown.

10.2 Interception Has a Product Cost

Every collision query without a resolvable statute name becomes a question to the user. That is the human oversight of EU AI Act Art. 14, and it is friction. Where the ceiling sits is a product decision.

10.3 Temporal Validity

Test 3 cited Ley 62/2003, repealed by Ley 16/2007. The router does not address whether a provision is in force. That needs a separate control: corpus versioning with in_force_from and repealed_by metadata and a date predicate alongside the statute predicate. It extends the same filtering approach, but this specification does not deliver it, which leaves one of the three observed failures outside the control.

A point-in-time control of this kind has since been built and measured for three sections of the UK Employment Rights Act 1996, on a different audited system (agentic thrashing case study, §5.5). Repeal of Spanish statutes is still open.

10.4 What Would Falsify the Core Claim

10.4 What Would Falsify the Core Claim: Observation, What It Would Mean (Data Table)
Observation What It Would Mean
Filtered retrieval returns a chunk with statute_id ≠ bound value The filter is not applied before search, or the index metadata is wrong. An implementation defect; the 0% claim fails until fixed.
Binding precision stays under ~99% on real traffic Extraction, not retrieval, is the limit. The control still helps; the headline guarantee is out of reach in practice.
The agent still loops on correctly bound queries Looping has a second cause, and the §6.1 latency model is wrong.
Chunk-level statute_id is unreliable in the corpus Metadata quality is the real problem, a re-index is needed, and the "no database rebuild" line fails.

Regulatory Applicability

These are design-level mappings: how the control would meet each requirement once built. They are not attestations of a certified deployment.

Regulatory Applicability: Framework, Requirement, How This Architecture Would Satisfy It (Data Table)
Framework Requirement How This Architecture Would Satisfy It
EU AI Act (Art. 14) Human oversight of high-risk AI Ambiguous queries go to the user to choose before any model runs
EU AI Act (Art. 15) Accuracy, robustness, cybersecurity Deterministic routing removes a predictable accuracy failure
SRA Principle 2 Uphold public trust in the profession Fabricated legal text is not presented as an authoritative citation
SRA Principle 7 Act in best interests of each client The correct statute is retrieved before advice is generated
ABA Model Rule 1.1 Competent representation The lawyer receives statute-bound citations with a recoverable decision record
NIST AI RMF (Govern 1.1) Risk management processes Collision index and audit record form a documented, measurable retrieval control
ISO 42001 (A.6.2.6) AI system quality objectives 0% wrong-statute retrieval as a monitored quality objective

Audit Trail

Every query through the router writes a structured record. Below is a specimen of the specified schema, an illustration of the format, not an extract from a running system:

{
  "query_id": "q-20260415-0042",
  "timestamp": "2026-04-15T14:32:07Z",
  "extracted_article": "42",
  "extracted_statute_keyword": "CdC",
  "collision_detected": true,
  "collision_set": ["CdC", "LGT", "ET", "Ley_15_1988"],
  "resolution_method": "keyword_match",
  "bound_statute": "CdC",
  "binding_confidence": 0.97,
  "metadata_filter_applied": {"statute_id": "CdC", "article_num": "42"},
  "routing_latency_ms": 6.8,
  "human_disambiguation_required": false
}

The schema covers the TPRM questions on decision traceability (which statute was selected and why), anomaly detection (collision rate over time), incident review (the routing decision behind any reported answer) and continuous monitoring (input to a retrieval integrity dashboard).

binding_confidence is in the specification because a wrong binding is the main residual risk (§10.1). The built router records a status and a reason instead (§5.5).


TL;DR

Measured, audited system, April 2026: a live legal-RAG platform that markets sub-1% hallucination returned the wrong statute on 3 of 3 article-collision probes: tax law quoted as commercial code, an invented sub-clause, and a repealed statute cited as current. Dense search and ColBERT cannot separate statutes that share an article number, and the model cannot correct evidence it was never given.

The control (By design) is a router that binds the statute, and a boolean metadata filter that restricts retrieval to it, before any embedding is compared. Wrong-statute retrieval is then zero for a correctly bound query (§8). Latency and cost figures are modeled. The remaining risk is extraction (§10). Temporal validity needs a separate control (§10.3).

A reference implementation for UK legislation is built as legal-rag-router (on /tools). Measured, reference implementation, sealed run of 29 September 2026: 0 of 40 same-numbered sections bound to the wrong Act, 0 of 1,030 held-out citations bound wrongly, 0 of 68 invented Acts bound. Over 100,000 citations from the statute book, 7 bound to another instrument (§5.5). The Spanish probes have not been re-run.

The Impact

A deterministic router binds the query to one statute before vector search, and a boolean metadata filter restricts retrieval to it. Every query records which statute was bound and why. Built for UK legislation as legal-rag-router and measured on a sealed battery.