<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Lineage on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/lineage/</link><description>Recent content in Lineage on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 01 Jun 2026 06:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/lineage/index.xml" rel="self" type="application/rss+xml"/><item><title>ISO/IEC 42001: the operations manual for the AI system — how the AIMS fits over the on-premise LLM platform described in this blog</title><link>https://blog.lo0.es/en/posts/iso-42001-aims-on-premise-llm-platform/</link><pubDate>Mon, 01 Jun 2026 06:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/iso-42001-aims-on-premise-llm-platform/</guid><description>&lt;blockquote>
&lt;p>This post closes an asymmetry the blog had been accumulating: we have described in detail the technical platform (&lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">seven stack layers&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/greenfield-deployment-phases-on-premise-llm-platform/">seven deployment phases&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/">five maturity levels&lt;/a>), the operational pipeline (&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">six LLMOps stages&lt;/a>), the data pieces (&lt;a href="https://blog.lo0.es/en/posts/rag-corpus-curation-active-librarian/">corpus curation&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">versioning&lt;/a>), the eval / safety pieces (&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">evals&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">guardrails&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/llm-guard-anatomy-scanners-langfuse-vllm-litellm/">LLM Guard&lt;/a>) and the observe pieces (&lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">OTel GenAI tracing&lt;/a>). What had not appeared is the governance layer a regulated client asks for on top of all that. ISO/IEC 42001 is that layer.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>ISO/IEC 42001:2023 is the first certifiable international standard that defines how an AI system is managed. It is not a technical standard (it does not say &amp;ldquo;use this inference engine&amp;rdquo; or &amp;ldquo;this safety threshold&amp;rdquo;): it is a management standard, a cousin of ISO 27001 and ISO 9001. From both it inherits the Annex SL structure, seven mandatory clauses running through context, leadership, planning, support, operation, performance evaluation and improvement, and it adds an Annex A with 38 AI-specific controls in 9 sections: policies, internal organisation, resources, impact assessment, lifecycle, data, information to interested parties, use, third parties. The thesis of this post is that the technical architecture described in this blog directly covers between 60% and 80% of the A controls with no extra work. The LLMOps pipeline materialises A.6, versioning and curation materialise A.7, guardrails and evals materialise A.9, OTel tracing materialises A.8. The rest is governance discipline that does not appear in the code (a written AI policy, impact assessments per system, a stakeholder register, role decisions between provider/producer/customer, mandatory documentation), and that is precisely what separates a real certification from performative compliance. The post maps the correspondence control by control, cross-references it with the EU AI Act (seven articles directly aligned with 42001: 9, 10, 11, 12, 13, 14, 17), with NIS2 (asset register, incident notification, supply chain) and with ENS (Esquema Nacional de Seguridad, Spain&amp;rsquo;s national security framework, RD 311/2022, Basic/Medium/High categories), lists the seven mandatory minimum documents an auditor asks for, presents the blog&amp;rsquo;s multi-tenant chatbot as a live 42001 checklist, and closes with the five usual traps (confusing 42001 with EU AI Act compliance, over-documenting without measuring, ignoring A.5 until the audit, assuming 27001 covers the AI part, thinking certification is a one-off project rather than a living system).&lt;/p>
&lt;h2 id="the-analogy-the-aircraft-operations-manual">The analogy: the aircraft operations manual&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="ISO 42001 as an aircraft operations manual audited by EASA">
&lt;style>
.i-air{fill:#7aafff;stroke:#444;stroke-width:1.4;rx:8}
.i-man{fill:#ffd76b;stroke:#444;stroke-width:1.4;rx:8}
.i-aud{fill:#a8e6a3;stroke:#444;stroke-width:1.4;rx:8}
.i-ops{fill:#ff8a4c;stroke:#444;stroke-width:1.4;rx:8}
.il{font:600 13px sans-serif;fill:#222}
.is{font:400 11px sans-serif;fill:#555}
.in{font:italic 11px sans-serif;fill:#555}
.iar{stroke:#666;stroke-width:1.6;fill:none;marker-end:url(#mi1)}
&lt;/style>
&lt;defs>&lt;marker id="mi1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;rect x="20" y="20" width="160" height="60" class="i-air"/>
&lt;text x="100" y="44" text-anchor="middle" class="il">AI system&lt;/text>
&lt;text x="100" y="62" text-anchor="middle" class="is">on-premise LLM platform&lt;/text>
&lt;text x="100" y="76" text-anchor="middle" class="is">(the aircraft)&lt;/text>
&lt;rect x="220" y="20" width="180" height="60" class="i-ops"/>
&lt;text x="310" y="40" text-anchor="middle" class="il">Technical operations&lt;/text>
&lt;text x="310" y="58" text-anchor="middle" class="is">LLMOps pipeline + guardrails +&lt;/text>
&lt;text x="310" y="72" text-anchor="middle" class="is">tracing + retrain (the flights)&lt;/text>
&lt;rect x="440" y="20" width="180" height="60" class="i-man"/>
&lt;text x="530" y="40" text-anchor="middle" class="il">Operations manual&lt;/text>
&lt;text x="530" y="58" text-anchor="middle" class="is">policies + impact assessment +&lt;/text>
&lt;text x="530" y="72" text-anchor="middle" class="is">roles + lineage (ISO 42001)&lt;/text>
&lt;rect x="660" y="20" width="140" height="60" class="i-aud"/>
&lt;text x="730" y="44" text-anchor="middle" class="il">Auditor&lt;/text>
&lt;text x="730" y="62" text-anchor="middle" class="is">certification body&lt;/text>
&lt;text x="730" y="76" text-anchor="middle" class="is">(EASA / Aenor / BSI)&lt;/text>
&lt;path class="iar" d="M180,50 L220,50"/>
&lt;path class="iar" d="M400,50 L440,50"/>
&lt;path class="iar" d="M620,50 L660,50"/>
&lt;rect x="20" y="130" width="780" height="80" class="i-man"/>
&lt;text x="410" y="152" text-anchor="middle" class="il">The 7 Annex SL clauses — the manual's mandatory table of contents&lt;/text>
&lt;text x="410" y="172" text-anchor="middle" class="is">4 Context · 5 Leadership · 6 Planning · 7 Support · 8 Operation · 9 Evaluation · 10 Improvement&lt;/text>
&lt;text x="410" y="190" text-anchor="middle" class="is">Inherited from Annex SL — the same skeleton as ISO 27001 and 9001, which allows management systems to be integrated&lt;/text>
&lt;rect x="20" y="230" width="780" height="80" class="i-aud"/>
&lt;text x="410" y="252" text-anchor="middle" class="il">The 38 Annex A controls — the manual's AI-specific procedures&lt;/text>
&lt;text x="410" y="272" text-anchor="middle" class="is">A.2 Policies · A.3 Org · A.4 Resources · A.5 Impact · A.6 Lifecycle · A.7 Data · A.8 Info to parties · A.9 Use · A.10 Third parties&lt;/text>
&lt;text x="410" y="290" text-anchor="middle" class="is">What separates 42001 from 27001/9001: every control is born from an AI-specific risk (bias, opacity, drift, data supply)&lt;/text>
&lt;text x="410" y="340" text-anchor="middle" class="in">The aircraft flies with its pilots; the manual is audited by the authority. If the manual is incomplete, the aircraft does not certify however well it flies.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>A modern aircraft, an A350, a Boeing 787, a drone certified for urban delivery, does not fly because it has good engines. It flies because the organisation that operates it has an Operations Manual approved by the aviation authority (EASA in Europe, the FAA in the US, AESA in Spain as a delegated body). The manual does not contain the engine blueprints, which the manufacturer certifies; it contains the procedures: who is captain on each flight, which checklist is run before each take-off, which periodic inspections happen at 100, 500 and 2,000 flight hours, which external suppliers are authorised to touch which components, what is documented after each incident, what to do when a new alert appears on the panel. The authority does not sit in on every flight: it reads the manual, randomly audits the traceability of past flights against the manual, and if everything adds up, it keeps the certification in place.&lt;/p>
&lt;p>An AI system in production, the multi-tenant chatbot from the &lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">forensic post&lt;/a>, a copilot for lawyers, a credit scoring system, is exactly the same. It flies because the model is good, the LLMOps pipeline is well built, the guardrails catch the bad cases. But it certifies because the organisation operating it has an AIMS (AI Management System) described in an auditable manual. ISO/IEC 42001 is that manual: its mandatory table of contents (Annex SL, seven clauses) and its catalogue of AI-specific controls (Annex A, 38 controls). The auditor does not sit next to the MLOps engineer: they read the AI policy, review the impact assessments of the most recently deployed systems, check that incident retrain is documented, verify third-party contracts, audit a sample of Langfuse traces cross-checked against &lt;code>dataset_hash&lt;/code> and &lt;code>prompt_id&lt;/code>. And if everything adds up, they certify.&lt;/p>
&lt;p>The analogy matters because it frames the right question: 42001 does not certify the model or the code. It certifies the way of operating the complete system. A team can have the best OSS stack in the world and fail the audit because it has no written AI policy and no documented decision about which role (provider vs producer vs customer) it occupies towards its clients. And the reverse: a team with a modest model but with operations-manual discipline can certify without acrobatics.&lt;/p>
&lt;h2 id="isoiec-42001-in-15-seconds">ISO/IEC 42001 in 15 seconds&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Publication&lt;/strong>: December 2023, ISO/IEC JTC 1/SC 42 (the ISO/IEC subcommittee for AI).&lt;/li>
&lt;li>&lt;strong>Status in 2026&lt;/strong>: an active standard, certifiable by accredited bodies (BSI, AENOR, TÜV, Bureau Veritas, A-LIGN, Schellman). Not yet formally recognised as a harmonised standard under the EU AI Act, but it provides the management base to lean on.&lt;/li>
&lt;li>&lt;strong>Compatibility&lt;/strong>: it shares the Annex SL structure with ISO 9001 (quality), 27001 (information security), 27701 (privacy), 22301 (continuity), 20000-1 (IT services). Organisations with integrated management systems (IMS) add it with 20-40% of the effort it would take to implement from scratch.&lt;/li>
&lt;li>&lt;strong>Applicability&lt;/strong>: any organisation that develops, provides, deploys or uses AI systems. It is not limited to developers: a company that consumes a hosted LLM and integrates it into its own product is within scope.&lt;/li>
&lt;li>&lt;strong>Certification&lt;/strong>: a 3-year cycle with an initial audit (Stage 1: documentation review + Stage 2: on-site audit) and annual surveillance audits. Typical cost: 15,000-60,000 € for the initial one depending on size; 6,000-20,000 € per annual surveillance.&lt;/li>
&lt;/ul>
&lt;p>What 42001 does not do:&lt;/p>
&lt;ul>
&lt;li>It does not say which models to use or which thresholds to apply.&lt;/li>
&lt;li>It does not certify the individual model (that is done by specific evaluations such as a NIST AI RMF profile or EU AI Act technical documentation).&lt;/li>
&lt;li>It does not replace the EU AI Act or the GDPR: it is complementary. Implementing it well makes legal compliance easier but does not guarantee it.&lt;/li>
&lt;li>It is not a technical standard for explainability or robustness (those are ISO/IEC 25059, 24029, 23894 and others in the SC 42 family).&lt;/li>
&lt;/ul>
&lt;h2 id="distinction-from-neighbouring-frameworks">Distinction from neighbouring frameworks&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Framework&lt;/th>
&lt;th>Nature&lt;/th>
&lt;th>Scope&lt;/th>
&lt;th>Certifiable&lt;/th>
&lt;th>Overlap with 42001&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>ISO/IEC 42001:2023&lt;/strong>&lt;/td>
&lt;td>Management standard&lt;/td>
&lt;td>AIMS for any AI system&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>EU AI Act&lt;/strong> (Reg. 2024/1689)&lt;/td>
&lt;td>Binding legal regulation&lt;/td>
&lt;td>AI systems in the EU, risk-categorised&lt;/td>
&lt;td>No (it is law)&lt;/td>
&lt;td>Arts 9, 10, 11, 12, 13, 14, 17&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>NIS2&lt;/strong> (Dir. 2022/2555)&lt;/td>
&lt;td>Cybersecurity directive&lt;/td>
&lt;td>Essential/important entities&lt;/td>
&lt;td>Via the national scheme&lt;/td>
&lt;td>Asset register, incident, supply chain&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ENS&lt;/strong> (RD 311/2022)&lt;/td>
&lt;td>Spanish security regulation&lt;/td>
&lt;td>Public sector and its suppliers&lt;/td>
&lt;td>Yes (B/M/A categories)&lt;/td>
&lt;td>Traceability, incident management&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ISO/IEC 27001&lt;/strong>&lt;/td>
&lt;td>Management standard&lt;/td>
&lt;td>Information security&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Annex SL structure + Annex A overlap&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ISO/IEC 27701&lt;/strong>&lt;/td>
&lt;td>Management standard&lt;/td>
&lt;td>Privacy (extends 27001)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>PII in training data&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>NIST AI RMF 1.0&lt;/strong>&lt;/td>
&lt;td>Voluntary framework&lt;/td>
&lt;td>AI risk management&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Conceptually aligned, not identical&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ISO/IEC 23894&lt;/strong>&lt;/td>
&lt;td>Technical standard&lt;/td>
&lt;td>AI risk management&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Input to A.5 (impact assessment)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>ISO/IEC 5259&lt;/strong>&lt;/td>
&lt;td>Family&lt;/td>
&lt;td>Data quality for AI&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Input to A.7 (data)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Three distinctions that matter operationally&lt;/strong> and are a recurring source of confusion with clients:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>ISO 42001 ≠ EU AI Act compliance&lt;/strong>. Holding the 42001 certification makes it easier to demonstrate articles 9-17 of the European Regulation, but the Regulation requires things 42001 does not cover directly (CE marking of high-risk systems, registration in the European database, declaration of conformity, specific post-market monitoring). Implementing 42001 first and then filling the AI Act gaps is the standard route.&lt;/li>
&lt;li>&lt;strong>ISO 27001 is not enough&lt;/strong>. 27001 covers confidentiality, integrity and availability of information. The AI side is missing: bias, opacity, model drift, training corpus quality, human evaluation, impact on affected persons. 42001 is a complement, not a substitute. Organisations that already have 27001 in place have an advantage because they share half the documentation.&lt;/li>
&lt;li>&lt;strong>NIS2 ≠ AI safety&lt;/strong>. NIS2 requires registering critical assets, notifying incidents within 24 h, and managing the digital supply chain. AI systems can be within NIS2 scope if they form part of the critical asset (an LLM serving customer support in a financial entity is), but NIS2 does not audit model quality. 42001 does.&lt;/li>
&lt;/ol>
&lt;h2 id="the-seven-clauses-annex-sl-the-mandatory-table-of-contents">The seven clauses (Annex SL): the mandatory table of contents&lt;/h2>
&lt;p>The seven clauses, from clause 4 to clause 10, are common to all modern management standards (Annex SL, also called the &amp;ldquo;High Level Structure&amp;rdquo;). That means an organisation with ISO 9001 or 27001 already in place recognises the structure. Clauses 1-3 are introductory (scope, normative references, terms).&lt;/p>
&lt;h3 id="clause-4--context-of-the-organisation">Clause 4 — Context of the organisation&lt;/h3>
&lt;p>Identify the external context (applicable regulation, customer expectations, social risks) and the internal context (strategy, culture, capabilities). Identify the interested parties and their expectations: customers, regulators, affected persons, employees, suppliers. Define the scope of the AIMS: which AI systems are in and which are out.&lt;/p>
&lt;p>The usual gap: organisations that say &amp;ldquo;all our AI systems are in scope&amp;rdquo; without having enumerated them. The auditor asks for the list. Without a list, there is no scope.&lt;/p>
&lt;h3 id="clause-5--leadership">Clause 5 — Leadership&lt;/h3>
&lt;p>Top management must approve and publish an AI policy, assign roles and responsibilities (typically AI lead, AI risk owner, data officer), and demonstrate commitment through resources, communication and oversight. The policy is an auditable document and must be made available to staff and interested parties.&lt;/p>
&lt;p>The usual gap: a generic AI policy copied off the internet, with no measurables and no concrete objectives. The auditor asks how compliance with it is measured. Without metrics, the policy is theatre.&lt;/p>
&lt;h3 id="clause-6--planning">Clause 6 — Planning&lt;/h3>
&lt;p>Identify risks and opportunities of the AIMS (not of the individual model). Define measurable AI objectives, with deadlines and owners. Plan changes to the AIMS.&lt;/p>
&lt;p>The usual gap: confusing AIMS risks (what happens if we do not document correctly?) with model risks (what happens if the model is biased?). The first belongs here; the second goes to A.5.&lt;/p>
&lt;h3 id="clause-7--support">Clause 7 — Support&lt;/h3>
&lt;p>&lt;strong>Resources&lt;/strong>: human, technical, financial, infrastructure. Competence of personnel (documented training). Awareness of the policy among personnel. Internal and external communication. Documented information (the backbone of the management system: policy, procedures, records, evidence).&lt;/p>
&lt;p>The usual gap: documentation scattered across Confluence/Notion/Drive with no version control and no recorded approvals. The auditor asks about the last change: who approved it? when? on what grounds?&lt;/p>
&lt;h3 id="clause-8--operation">Clause 8 — Operation&lt;/h3>
&lt;p>The most operational clause. It requires:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Operational planning and control&lt;/strong>: how the AI system&amp;rsquo;s lifecycle is managed day to day. → Covered in the blog by the &lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">six-stage LLMOps pipeline&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Impact assessment&lt;/strong> (linked to A.5).&lt;/li>
&lt;li>&lt;strong>AI system lifecycle management&lt;/strong> (linked to A.6).&lt;/li>
&lt;li>&lt;strong>Data for AI systems&lt;/strong> (linked to A.7).&lt;/li>
&lt;/ul>
&lt;p>This is the clause that materialises in controls A.5, A.6 and A.7. On its own it adds no new requirements: it links to Annex A.&lt;/p>
&lt;h3 id="clause-9--performance-evaluation">Clause 9 — Performance evaluation&lt;/h3>
&lt;p>&lt;strong>Monitoring, measurement, analysis, evaluation&lt;/strong>. Internal audits (planned, with criteria, scope, frequency, recorded results). Management review (typically quarterly or half-yearly, with a mandatory agenda: inputs, evidence, decisions, actions).&lt;/p>
&lt;p>The usual gap: there is OTel tracing + Langfuse + Grafana and plenty of data, but there is no formal management review agenda with documented minutes. The auditor asks for the minutes. Without minutes, there is no review.&lt;/p>
&lt;h3 id="clause-10--improvement">Clause 10 — Improvement&lt;/h3>
&lt;p>&lt;strong>Nonconformity and corrective action&lt;/strong>: when something fails, it is recorded, root cause is analysed, a correction is agreed, effectiveness is verified. Continual improvement: the system evolves deliberately.&lt;/p>
&lt;p>The usual gap: Jira tickets with technical post-mortems but no formal &amp;ldquo;ISO nonconformity&amp;rdquo; record closed out with an effectiveness check. They are two distinct artefacts, even if they can be integrated.&lt;/p>
&lt;h2 id="the-38-annex-a-controls-the-ai-specific-catalogue">The 38 Annex A controls: the AI-specific catalogue&lt;/h2>
&lt;p>Unlike Annex SL (which is common), Annex A is 42001&amp;rsquo;s AI-specific signature. The 38 controls are organised into 9 sections (A.2 to A.10; A.1 is the introduction) covering the AI-specific risks: opacity, bias, drift, corpus quality, impact on affected persons, third-party dependency. Each control has an objective (what it aims to achieve) and implementation guidance in Annex B.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Section&lt;/th>
&lt;th>Focus&lt;/th>
&lt;th># controls&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>A.2&lt;/td>
&lt;td>AI-related policies&lt;/td>
&lt;td>2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A.3&lt;/td>
&lt;td>Internal organisation&lt;/td>
&lt;td>3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A.4&lt;/td>
&lt;td>Resources for AI systems&lt;/td>
&lt;td>6&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A.5&lt;/td>
&lt;td>Impact assessment&lt;/td>
&lt;td>5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A.6&lt;/td>
&lt;td>AI system lifecycle&lt;/td>
&lt;td>4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A.7&lt;/td>
&lt;td>Data for AI systems&lt;/td>
&lt;td>5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A.8&lt;/td>
&lt;td>Information for interested parties&lt;/td>
&lt;td>4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A.9&lt;/td>
&lt;td>Use of AI systems&lt;/td>
&lt;td>3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>A.10&lt;/td>
&lt;td>Third parties and customer relationships&lt;/td>
&lt;td>4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Total&lt;/strong>&lt;/td>
&lt;td>—&lt;/td>
&lt;td>&lt;strong>38&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>What follows is the section-by-section mapping of the controls onto material we have already covered in the blog. The editorial intent is to show which gaps remain once the technical architecture is in place, so that the road to certification does not start from zero.&lt;/p>
&lt;h2 id="cross-mapping-38-controls--blog-posts">Cross-mapping: 38 controls ↔ blog posts&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Mapping of ISO 42001 Annex A controls onto the blog's on-premise LLM architecture">
&lt;style>
.m-cov{fill:#a8e6a3;stroke:#444;stroke-width:1.4;rx:6}
.m-par{fill:#ffd76b;stroke:#444;stroke-width:1.4;rx:6}
.m-gap{fill:#f4b8b8;stroke:#444;stroke-width:1.4;rx:6}
.m-hdr{fill:#7aafff;stroke:#444;stroke-width:1.4;rx:8}
.ml{font:600 12px sans-serif;fill:#222}
.ms{font:400 10px sans-serif;fill:#444}
.mn{font:italic 10px sans-serif;fill:#555}
&lt;/style>
&lt;rect x="20" y="20" width="780" height="40" class="m-hdr"/>
&lt;text x="410" y="42" text-anchor="middle" class="ml">ISO 42001 Annex A mapped onto the blog's on-premise LLM architecture&lt;/text>
&lt;text x="410" y="55" text-anchor="middle" class="ms">green = covered by code/architecture · yellow = partial · red = governance gap&lt;/text>
&lt;rect x="20" y="80" width="780" height="50" class="m-par"/>
&lt;text x="50" y="100" class="ml">A.2 Policies (2)&lt;/text>
&lt;text x="50" y="116" class="ms">A.2.2 AI policy · A.2.3 Alignment with existing policies&lt;/text>
&lt;text x="500" y="100" class="ml">Status: PARTIAL&lt;/text>
&lt;text x="500" y="116" class="ms">The blog's editorial discipline teaches the angle; a formal written policy per organisation is missing.&lt;/text>
&lt;rect x="20" y="140" width="780" height="50" class="m-gap"/>
&lt;text x="50" y="160" class="ml">A.3 Internal organisation (3)&lt;/text>
&lt;text x="50" y="176" class="ms">A.3.2 Roles and responsibilities · A.3.3 Incident reporting · A.3.4 Stakeholders&lt;/text>
&lt;text x="500" y="160" class="ml">Status: GAP&lt;/text>
&lt;text x="500" y="176" class="ms">Not technical. Requires an organisational decision: AI lead, risk owner, AI committee.&lt;/text>
&lt;rect x="20" y="200" width="780" height="50" class="m-cov"/>
&lt;text x="50" y="220" class="ml">A.4 Resources (6)&lt;/text>
&lt;text x="50" y="236" class="ms">Data, tooling, system, human, financial resources + documentation&lt;/text>
&lt;text x="500" y="220" class="ml">Status: COVERED&lt;/text>
&lt;text x="500" y="236" class="ms">Seven deployment phases + five maturity levels + seven layers + OSS catalogue.&lt;/text>
&lt;rect x="20" y="250" width="780" height="50" class="m-par"/>
&lt;text x="50" y="270" class="ml">A.5 Impact assessment (5)&lt;/text>
&lt;text x="50" y="286" class="ms">AI impact process · documentation · alignment with risks · individuals · society&lt;/text>
&lt;text x="500" y="270" class="ml">Status: PARTIAL&lt;/text>
&lt;text x="500" y="286" class="ms">ISO/IEC 23894 gives the method; a formal per-system impact assessment procedure is missing.&lt;/text>
&lt;rect x="20" y="300" width="780" height="50" class="m-cov"/>
&lt;text x="50" y="320" class="ml">A.6 Lifecycle (4)&lt;/text>
&lt;text x="50" y="336" class="ms">Objectives · design · verification and validation · operation and monitoring · documentation&lt;/text>
&lt;text x="500" y="320" class="ml">Status: COVERED&lt;/text>
&lt;text x="500" y="336" class="ms">Six-stage pipeline + request anatomy + continuous fine-tuning + retrain.&lt;/text>
&lt;rect x="20" y="350" width="780" height="50" class="m-cov"/>
&lt;text x="50" y="370" class="ml">A.7 Data (5)&lt;/text>
&lt;text x="50" y="386" class="ms">Quality · acquisition · provenance · preparation · privacy&lt;/text>
&lt;text x="500" y="370" class="ml">Status: COVERED&lt;/text>
&lt;text x="500" y="386" class="ms">Data versioning + RAG corpus curation + Presidio + LLM Guard Vault.&lt;/text>
&lt;rect x="20" y="400" width="780" height="50" class="m-cov"/>
&lt;text x="50" y="420" class="ml">A.8 Information to parties (4)&lt;/text>
&lt;text x="50" y="436" class="ms">System documentation · information on use · incident communication · external reporting&lt;/text>
&lt;text x="500" y="420" class="ml">Status: COVERED&lt;/text>
&lt;text x="500" y="436" class="ms">OTel GenAI tracing + Langfuse + chunk→trace lineage + guardrail spans.&lt;/text>
&lt;rect x="20" y="450" width="780" height="40" class="m-cov"/>
&lt;text x="50" y="470" class="ml">A.9 Use (3)&lt;/text>
&lt;text x="50" y="484" class="ms">Responsible use processes · use objectives · intended use&lt;/text>
&lt;text x="500" y="470" class="ml">Status: COVERED&lt;/text>
&lt;text x="500" y="484" class="ms">Guardrails + evals + LLM Guard + incident-driven retrain.&lt;/text>
&lt;rect x="20" y="500" width="780" height="40" class="m-cov"/>
&lt;text x="50" y="520" class="ml">A.10 Third parties (4)&lt;/text>
&lt;text x="50" y="534" class="ms">Allocation of responsibilities · supplier · customer · third-party&lt;/text>
&lt;text x="500" y="520" class="ml">Status: COVERED&lt;/text>
&lt;text x="500" y="534" class="ms">OSS vs hyperscalers + OSS catalogue + sovereignty + lock-in analysis.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h3 id="a2--ai-policies-2-controls-partial">A.2 — AI policies (2 controls): PARTIAL&lt;/h3>
&lt;ul>
&lt;li>A.2.2 AI policy: the organisation must have a documented AI policy, approved by management, reviewed periodically, communicated and available. It covers principles, scope and commitments.&lt;/li>
&lt;li>A.2.3 Alignment with other policies: the AI policy is not an orphan, it aligns with existing security, privacy, quality and ethics policies.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Gap&lt;/strong>: this is not a code matter. The AI policy is a document that the organisation&amp;rsquo;s management approves and signs. The blog teaches a neutral, technical editorial stance (no hype, sovereignty, OSS by default under ENS/NIS2), but that is not any specific organisation&amp;rsquo;s AI policy. Each client has to draft and sign their own.&lt;/p>
&lt;p>&lt;strong>Minimum template&lt;/strong>: 1-2 pages with: principles (transparency, human oversight, fairness, accountability, sustainability), scope (which systems), measurable commitments (annual review, impact assessment before deployment, team training), governance (who approves what).&lt;/p>
&lt;h3 id="a3--internal-organisation-3-controls-gap">A.3 — Internal organisation (3 controls): GAP&lt;/h3>
&lt;ul>
&lt;li>A.3.2 AI roles and responsibilities: roles defined, non-overlapping, communicated. Typically: AI lead, AI risk owner, data steward, AI ethics officer (which may be a single person in small organisations).&lt;/li>
&lt;li>A.3.3 Reporting of AI incidents/concerns: a channel for anyone (internal or external) to report a problem with an AI system, with documented follow-up.&lt;/li>
&lt;li>A.3.4 Identification of stakeholders: a maintained list of stakeholders (customers, affected persons, regulators, partners) and their expectations.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Gap&lt;/strong>: also not technical. An organisational decision. The usual way to cover it is to appoint an AI lead (which may be the CIO, the CTO or a new role depending on size), reuse the security reporting channel (typically already in place because of 27001) by extending it to AI, and maintain a live stakeholder register.&lt;/p>
&lt;h3 id="a4--resources-6-controls-covered">A.4 — Resources (6 controls): COVERED&lt;/h3>
&lt;ul>
&lt;li>A.4.2 Documented information: documentation of the AIMS.&lt;/li>
&lt;li>A.4.3 Data resources: identification and management of the data available for training, evaluation and operation.&lt;/li>
&lt;li>A.4.4 Tooling resources: development, validation and monitoring tools.&lt;/li>
&lt;li>A.4.5 System resources: hardware, infrastructure, compute.&lt;/li>
&lt;li>A.4.6 Human resources: competent personnel.&lt;/li>
&lt;li>A.4.7 Financial resources: budget.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Covered by the blog&lt;/strong> in the three architectural posts:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">Anatomy of the stack: seven layers&lt;/a>: A.4.5 system resources, A.4.4 tooling.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/greenfield-deployment-phases-on-premise-llm-platform/">Seven deployment phases&lt;/a>: A.4.5 + A.4.7 (budget implicitly).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/">Five maturity levels&lt;/a>: A.4.5 + A.4.6 (team maturity).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">OSS tooling catalogue&lt;/a>: A.4.4 tooling.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">Data versioning with DVC and lakeFS&lt;/a>: A.4.3 data resources.&lt;/li>
&lt;/ul>
&lt;h3 id="a5--impact-assessment-5-controls-partial">A.5 — Impact assessment (5 controls): PARTIAL&lt;/h3>
&lt;ul>
&lt;li>A.5.2 AI impact assessment process: a documented impact assessment procedure.&lt;/li>
&lt;li>A.5.3 Documentation of AI impact assessments: records of the assessments carried out.&lt;/li>
&lt;li>A.5.4 Alignment with AI risk treatment: the impact assessment&amp;rsquo;s decisions feed risk treatment.&lt;/li>
&lt;li>A.5.5 Impacts on individuals: specific dimensions covering affected persons (rights, discrimination, privacy).&lt;/li>
&lt;li>A.5.6 Societal impacts: dimensions covering society (information, social rights).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Partial&lt;/strong>: the method exists in the ISO/IEC SC 42 family. ISO/IEC 23894:2023 is the technical standard for AI risk management and NIST AI RMF 1.0 is the freely usable American equivalent. But the organisation has to write its procedure and run it per system before deployment. It is not code, it is discipline.&lt;/p>
&lt;p>&lt;strong>Minimum template&lt;/strong> for the impact assessment (3-5 pages per system):&lt;/p>
&lt;ol>
&lt;li>Description of the system (what it does, who it serves, underlying model and stack).&lt;/li>
&lt;li>Stakeholders identified.&lt;/li>
&lt;li>Potential impacts (intended and unintended) on people, groups and society.&lt;/li>
&lt;li>Fairness and robustness metrics applied, with thresholds and results.&lt;/li>
&lt;li>Mitigations applied (guardrails, evals, human oversight, rate limiting).&lt;/li>
&lt;li>Accepted residual risks, with signed justification.&lt;/li>
&lt;li>Review cadence (typically annual, or on substantial change).&lt;/li>
&lt;/ol>
&lt;h3 id="a6--ai-system-lifecycle-4-controls-covered">A.6 — AI system lifecycle (4 controls): COVERED&lt;/h3>
&lt;ul>
&lt;li>A.6.2.2 Objectives for responsible development of AI: responsible development objectives defined per system.&lt;/li>
&lt;li>A.6.2.3 Processes for responsible AI design and development: design and development procedures.&lt;/li>
&lt;li>A.6.2.4 AI system requirements and specifications: formal specification of the system.&lt;/li>
&lt;li>A.6.2.5 Verification and validation: V&amp;amp;V before and during operation.&lt;/li>
&lt;li>A.6.2.6 Deployment: deployment procedures.&lt;/li>
&lt;li>A.6.2.7 Operation and monitoring: continuous operation and monitoring.&lt;/li>
&lt;li>A.6.2.8 Documentation: lifecycle documentation.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Covered by the blog&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage LLMOps pipeline&lt;/a>: the complete master map of the lifecycle.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">Anatomy of an LLM request&lt;/a>: the forensic version of how it runs in production.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">Continuous fine-tuning in production&lt;/a>: the A.6.2.3 + A.6.2.5 + A.6.2.6 + A.6.2.7 discipline in real operation.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals: the layer after tracing&lt;/a>: A.6.2.5 verification and validation.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain&lt;/a>: A.6.2.7 operation + continuous iteration.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/modern-alignment-dpo-kto-orpo-simpo/">Modern alignment: DPO, KTO, ORPO, SimPO&lt;/a>: A.6.2.3 responsible design.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/mlops-for-llms-landscape-2026/">MLOps landscape 2026&lt;/a>: the tooling landscape.&lt;/li>
&lt;/ul>
&lt;h3 id="a7--data-for-ai-systems-5-controls-covered">A.7 — Data for AI systems (5 controls): COVERED&lt;/h3>
&lt;ul>
&lt;li>A.7.2 Data for development and enhancement of AI: data management policy and procedures for development and improvement.&lt;/li>
&lt;li>A.7.3 Acquisition of data: acquisition procedures (origin, authorisation, quality).&lt;/li>
&lt;li>A.7.4 Quality of data for AI systems: measurable quality criteria.&lt;/li>
&lt;li>A.7.5 Data provenance: data lineage.&lt;/li>
&lt;li>A.7.6 Data preparation: preparation procedures (chunking, anonymisation, labelling).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Covered by the blog&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/rag-corpus-curation-active-librarian/">RAG corpus curation: the active librarian&lt;/a>: A.7.4 + A.7.5 + A.7.6 in detail (five layers: schema, dedup, PII, anti-contamination, lineage).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">Data versioning: DVC and lakeFS&lt;/a>: A.7.2 + A.7.5 (the four data artefacts versioned with lineage).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/reranker-hybrid-retrieval-five-chunks-llm-reads/">Reranker and hybrid retrieval&lt;/a>: A.7.6 preparation + filtering.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-guard-anatomy-scanners-langfuse-vllm-litellm/">LLM Guard&lt;/a>: A.7.6 runtime anonymisation with Vault.&lt;/li>
&lt;/ul>
&lt;h3 id="a8--information-for-interested-parties-4-controls-covered">A.8 — Information for interested parties (4 controls): COVERED&lt;/h3>
&lt;ul>
&lt;li>A.8.2 System documentation and information for users: technical documentation available.&lt;/li>
&lt;li>A.8.3 External reporting: the capacity to report to authorities where applicable.&lt;/li>
&lt;li>A.8.4 Communication of incidents to users: notification to users when there is an incident.&lt;/li>
&lt;li>A.8.5 Information for interested parties: information for other stakeholders.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Covered by the blog&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">LLM tracing with OpenTelemetry GenAI&lt;/a>: A.8.2 per-request traceability, A.8.3 the capacity to extract forensic reporting.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/prompt-versioning-contract-five-word-change/">Prompt versioning with Langfuse and MLflow&lt;/a>: A.8.2 documented versioning.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety in LLMs&lt;/a>: A.8.4, &lt;code>gen_ai.guardrail.*&lt;/code> spans as the basis for incident notification.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-guard-anatomy-scanners-langfuse-vllm-litellm/">LLM Guard&lt;/a>: A.8.4 incident events for retrain.&lt;/li>
&lt;/ul>
&lt;h3 id="a9--use-of-ai-systems-3-controls-covered">A.9 — Use of AI systems (3 controls): COVERED&lt;/h3>
&lt;ul>
&lt;li>A.9.2 Processes for responsible use of AI: responsible use procedures.&lt;/li>
&lt;li>A.9.3 Objectives for responsible use of AI: objectives.&lt;/li>
&lt;li>A.9.4 Intended use of AI systems: documentation of the intended use.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Covered by the blog&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety in LLMs&lt;/a>: A.9.2 + A.9.3 (the four lines of defence).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-guard-anatomy-scanners-langfuse-vllm-litellm/">LLM Guard&lt;/a>: A.9.2 operational detail.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals: the layer after tracing&lt;/a>: A.9.3 measurement of objectives.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain&lt;/a>: A.9.2 closed loop.&lt;/li>
&lt;/ul>
&lt;h3 id="a10--third-parties-and-customer-relationships-4-controls-covered">A.10 — Third parties and customer relationships (4 controls): COVERED&lt;/h3>
&lt;ul>
&lt;li>A.10.2 Allocation of responsibilities: distribution of responsibilities between AI roles.&lt;/li>
&lt;li>A.10.3 Suppliers: procedures for AI suppliers.&lt;/li>
&lt;li>A.10.4 Customers: procedures towards customers.&lt;/li>
&lt;li>A.10.5 Third parties: procedures for third parties.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Covered by the blog&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/parallel-catalogue-llmops-stages-open-source-hyperscalers/">The parallel catalogue: OSS vs hyperscalers&lt;/a>: A.10.3 supplier assessment with lock-in and contractual sovereignty analysis.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">The OSS catalogue for LLMOps&lt;/a>: A.10.5 third-party inventory (OSS components with licences and governance).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">Anatomy of an LLM request&lt;/a>: A.10.2 + A.10.4 in the multi-tenant case.&lt;/li>
&lt;/ul>
&lt;h2 id="the-roles-defined-by-the-standard">The roles defined by the standard&lt;/h2>
&lt;p>ISO/IEC 22989:2022 (AI vocabulary, complementary to 42001) defines six roles. Each organisation has to decide which ones it occupies and document it:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Role&lt;/th>
&lt;th>Definition&lt;/th>
&lt;th>Main responsibility&lt;/th>
&lt;th>Example&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>AI provider&lt;/strong>&lt;/td>
&lt;td>Organisation that provides the AI system to others&lt;/td>
&lt;td>Makes the system available&lt;/td>
&lt;td>OpenAI provides GPT-5 via API&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>AI producer&lt;/strong>&lt;/td>
&lt;td>Organisation that develops the AI system&lt;/td>
&lt;td>Design, development, validation&lt;/td>
&lt;td>Meta produces Llama 4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>AI customer&lt;/strong>&lt;/td>
&lt;td>Organisation that acquires the AI system&lt;/td>
&lt;td>Selection, integration, oversight&lt;/td>
&lt;td>A consultancy integrating an LLM into its own product&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>AI partner&lt;/strong>&lt;/td>
&lt;td>Organisation collaborating with another AI role&lt;/td>
&lt;td>Shared&lt;/td>
&lt;td>A GPU hardware manufacturer&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>AI subject&lt;/strong>&lt;/td>
&lt;td>Person or group affected by the system&lt;/td>
&lt;td>Recipient of the impact&lt;/td>
&lt;td>The chatbot&amp;rsquo;s end user&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Relevant authority&lt;/strong>&lt;/td>
&lt;td>Regulator with jurisdiction&lt;/td>
&lt;td>External oversight&lt;/td>
&lt;td>AEPD, CNMC, EU AI Act authorities&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>An organisation can occupy several roles at once, which changes the applicable controls. A common pattern in consultancy is: producer + customer + provider towards the end client. The A.10 responsibilities are modulated by the roles.&lt;/p>
&lt;p>&lt;strong>Example role mapping&lt;/strong> for the multi-tenant chatbot from the forensic post:&lt;/p>
&lt;ul>
&lt;li>Base model manufacturer (Llama 4): AI producer of the base model.&lt;/li>
&lt;li>OSS stack operator (the consultancy): AI producer of the LoRA adapter + AI provider of the chatbot to its clients + AI customer of Meta&amp;rsquo;s base model.&lt;/li>
&lt;li>End client (the insurer): AI customer of the chatbot + AI provider of the customer support service.&lt;/li>
&lt;li>The policyholder: AI subject.&lt;/li>
&lt;li>AEPD + EU AI Act authority: relevant authority.&lt;/li>
&lt;/ul>
&lt;p>Each box in the table generates different obligations. The consultancy, being producer of the adapter, must document A.6 (lifecycle) and A.7 (data) for the adapter. Being provider of the chatbot, it must document A.10.4 (customers). Being customer of the base model, it must document A.10.3 (suppliers) and validate that Meta does its part.&lt;/p>
&lt;h2 id="impact-levels-and-proportionality">Impact levels and proportionality&lt;/h2>
&lt;p>42001 does not demand the same rigour of every system. Clause 6.1.2 and control A.5 introduce impact as a modulator. The standard does not define strict categories (unlike the EU AI Act, which does define &amp;ldquo;prohibited / high risk / limited risk / minimal&amp;rdquo;), but it recommends using levels according to severity and probability.&lt;/p>
&lt;p>Industry practice in 2026 aligns the 42001 levels with the EU AI Act categories:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>42001 level&lt;/th>
&lt;th>EU AI Act&lt;/th>
&lt;th>Examples&lt;/th>
&lt;th>Control depth&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>High&lt;/strong>&lt;/td>
&lt;td>High risk (Annex III)&lt;/td>
&lt;td>Credit scoring, HR, health, critical infrastructure&lt;/td>
&lt;td>Exhaustive impact assessment, mandatory human oversight, continuous monitoring, adversarial evals, detailed logging, half-yearly management review&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Medium&lt;/strong>&lt;/td>
&lt;td>Limited risk&lt;/td>
&lt;td>Customer service chatbots that do not automate decisions, productivity assistants&lt;/td>
&lt;td>Standard impact assessment, complete guardrails, annual review&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Low&lt;/strong>&lt;/td>
&lt;td>Minimal risk&lt;/td>
&lt;td>Spam filters, non-personalised content recommendations&lt;/td>
&lt;td>Lightweight impact assessment, basic controls&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>This proportionality is operationally key: implementing 42001 at maximum rigour for a low-risk system is waste; relaxing it on a high-risk one is non-compliance.&lt;/p>
&lt;h2 id="the-seven-minimum-aims-documents">The seven minimum AIMS documents&lt;/h2>
&lt;p>An auditor at Stage 1 (documentation review) asks for between seven and ten documents. The seven essentials:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>AI policy&lt;/strong> (clause 5.2 + A.2.2). 1-2 pages. Approved by management, dated, versioned.&lt;/li>
&lt;li>&lt;strong>AIMS scope&lt;/strong> (clause 4.3). List of AI systems in scope, inclusion criteria.&lt;/li>
&lt;li>&lt;strong>Stakeholder register&lt;/strong> (clause 4.2 + A.3.4). A maintained list with expectations.&lt;/li>
&lt;li>&lt;strong>AIMS risk register&lt;/strong> (clause 6.1). Risks of the management system, not of each model.&lt;/li>
&lt;li>&lt;strong>Impact assessment procedure&lt;/strong> (A.5.2) + records of assessments carried out (A.5.3). The procedure plus one or more assessments done.&lt;/li>
&lt;li>&lt;strong>AI lifecycle procedure&lt;/strong> (A.6.2), which can literally read &amp;ldquo;see the six-stage LLMOps pipeline&amp;rdquo; with references to technical runbooks.&lt;/li>
&lt;li>&lt;strong>Data management procedure&lt;/strong> (A.7.2), covering acquisition, quality, provenance, preparation, anonymisation.&lt;/li>
&lt;/ol>
&lt;p>Common additional documents:&lt;/p>
&lt;ol start="8">
&lt;li>&lt;strong>Responsible use policy&lt;/strong> (A.9.2) with permitted and non-permitted use types.&lt;/li>
&lt;li>&lt;strong>AI third-party management procedure&lt;/strong> (A.10.3, A.10.5) with assessment criteria for AI suppliers.&lt;/li>
&lt;li>Internal audit plan + management review agenda (clauses 9.2 + 9.3).&lt;/li>
&lt;/ol>
&lt;p>For an organisation with a mature &lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">OSS stack&lt;/a>, documents 6 and 7 are references to technical artefacts that already exist (pipeline runbooks, DVC configurations, the PII policy in LLM Guard). The real documentary effort sits in documents 1, 2, 3, 4 and 5.&lt;/p>
&lt;h2 id="applied-case-the-blogs-multi-tenant-chatbot--42001-checklist">Applied case: the blog&amp;rsquo;s multi-tenant chatbot → 42001 checklist&lt;/h2>
&lt;p>We take the system described in the &lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">forensic post&lt;/a>, a multi-tenant customer support chatbot for insurers on an on-premise OSS stack, and walk it as a 42001 auditor would.&lt;/p>
&lt;p>&lt;strong>Clause 4 — Context&lt;/strong>. The AIMS scope includes the chatbot and excludes the internal HR system (a different AI). Stakeholders identified: client insurers, affected policyholders, the AEPD, the EU AI Act authority (once it enters into force on 2 Aug 2026), the supplier Meta (base model), the hardware supplier NVIDIA. → Documented.&lt;/p>
&lt;p>&lt;strong>Clause 5 — Leadership&lt;/strong>. AI policy signed by the CEO, in force. Roles assigned: AI lead (CTO), AI risk owner (CISO), data steward (Head of Data), quarterly AI ethics committee. → Documented.&lt;/p>
&lt;p>&lt;strong>Clause 6 — Planning&lt;/strong>. AIMS risk register: incomplete documentation, team churn, dependency on a single GPU supplier, EU AI Act regulatory change. AIMS objectives for 2026: 42001 certification before Q4, EU AI Act high-risk compliance before 2 Aug. → Documented.&lt;/p>
&lt;p>&lt;strong>Clause 7 — Support&lt;/strong>. Resources: a &lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">4×H100 SXM&lt;/a> cluster + the &lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">seven stack layers&lt;/a>. Competence: 2 MLE + 2 SRE + 1 part-time AI ethics, all with documented training. Communication: AI policy on the intranet + handbook. → Documented.&lt;/p>
&lt;p>&lt;strong>Clause 8 — Operation&lt;/strong>. Operating procedures = the &lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">six-stage LLMOps pipeline&lt;/a>. Impact assessment run before deployment + annual review + review on substantial change (defined as: base model change, major adapter change, expansion to a new tenant). → Documented.&lt;/p>
&lt;p>&lt;strong>Clause 9 — Evaluation&lt;/strong>. Monitoring: &lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">Langfuse + Tempo + VictoriaMetrics + Grafana&lt;/a>. Mandatory dashboard metrics: F1 per guardrail category over real traffic, statistical drift, RAG faithfulness, refusal rate. Quarterly internal audit with written criteria. Half-yearly management review with signed minutes. → Documented.&lt;/p>
&lt;p>&lt;strong>Clause 10 — Improvement&lt;/strong>. Incident-driven retrain tickets mapped as nonconformities when severity ≥ HIGH. Documented root cause analysis. Effectiveness verified at the next eval gate. → Documented.&lt;/p>
&lt;p>&lt;strong>Annex A — By section&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>A.2 (Policies): AI policy + responsible use policy. → Documented.&lt;/li>
&lt;li>A.3 (Organisation): roles assigned, reporting channel, stakeholder register. → Documented.&lt;/li>
&lt;li>A.4 (Resources): &lt;a href="https://blog.lo0.es/en/posts/greenfield-deployment-phases-on-premise-llm-platform/">seven deployment phases&lt;/a> + &lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">OSS catalogue&lt;/a> + training plan + annual budget. → Documented.&lt;/li>
&lt;li>A.5 (Impact): procedure + per-system assessments + fairness metrics applied. → Documented.&lt;/li>
&lt;li>A.6 (Lifecycle): &lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">LLMOps pipeline&lt;/a> + &lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">continuous fine-tuning&lt;/a> + &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">retrain&lt;/a>. → Documented.&lt;/li>
&lt;li>A.7 (Data): &lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">data versioning&lt;/a> + &lt;a href="https://blog.lo0.es/en/posts/rag-corpus-curation-active-librarian/">RAG corpus curation&lt;/a> + &lt;a href="https://blog.lo0.es/en/posts/llm-guard-anatomy-scanners-langfuse-vllm-litellm/">LLM Guard Vault&lt;/a> + Presidio. → Documented.&lt;/li>
&lt;li>A.8 (Information to parties): &lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">OTel tracing&lt;/a> + Langfuse + &lt;code>gen_ai.guardrail.*&lt;/code> spans + notification to tenants within SLA. → Documented.&lt;/li>
&lt;li>A.9 (Use): &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">guardrails&lt;/a> + &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">evals&lt;/a> + responsible use policy. → Documented.&lt;/li>
&lt;li>A.10 (Third parties): &lt;a href="https://blog.lo0.es/en/posts/parallel-catalogue-llmops-stages-open-source-hyperscalers/">OSS vs hyperscalers&lt;/a> with lock-in analysis + Meta contract for the base model + contracts with tenants. → Documented.&lt;/li>
&lt;/ul>
&lt;p>Result of the walkthrough: certifiable. The typical gaps (A.2.2 written policy, A.3 roles, A.5 impact assessment procedure) are covered as formal documents. The operational clauses (8, 9, 10) lean on the blog&amp;rsquo;s technical architecture. The distance between &amp;ldquo;having the architecture&amp;rdquo; and &amp;ldquo;having certification&amp;rdquo; is measured in documentary discipline, not in code.&lt;/p>
&lt;h2 id="cross-mapping-with-the-eu-ai-act-nis2-and-ens">Cross-mapping with the EU AI Act, NIS2 and ENS&lt;/h2>
&lt;h3 id="eu-ai-act-reg-20241689--seven-directly-aligned-articles">EU AI Act (Reg. 2024/1689) — seven directly aligned articles&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>EU AI Act article&lt;/th>
&lt;th>Topic&lt;/th>
&lt;th>Aligned 42001 control&lt;/th>
&lt;th>Applies to&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Art. 9&lt;/td>
&lt;td>Risk management system&lt;/td>
&lt;td>A.5 + clause 6&lt;/td>
&lt;td>High-risk systems&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Art. 10&lt;/td>
&lt;td>Data and data governance&lt;/td>
&lt;td>A.7 (all)&lt;/td>
&lt;td>High-risk systems&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Art. 11&lt;/td>
&lt;td>Technical documentation&lt;/td>
&lt;td>A.6 + A.4.2&lt;/td>
&lt;td>High-risk systems&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Art. 12&lt;/td>
&lt;td>Record-keeping (logs)&lt;/td>
&lt;td>A.8.2 + OTel tracing&lt;/td>
&lt;td>High-risk systems&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Art. 13&lt;/td>
&lt;td>Transparency to deployers&lt;/td>
&lt;td>A.8.5 + A.10.4&lt;/td>
&lt;td>High-risk systems&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Art. 14&lt;/td>
&lt;td>Human oversight&lt;/td>
&lt;td>A.9.2 + documented oversight&lt;/td>
&lt;td>High-risk systems&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Art. 17&lt;/td>
&lt;td>Quality management system&lt;/td>
&lt;td>Clauses 4-10&lt;/td>
&lt;td>High-risk providers&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The main obligations for high-risk systems enter into application on 2 August 2026. Implementing 42001 now builds the management base that deadline demands.&lt;/p>
&lt;p>What is missing for EU AI Act compliance that 42001 does not cover:&lt;/p>
&lt;ul>
&lt;li>CE conformity of high-risk systems (declaration of conformity, marking, registration in the EU database).&lt;/li>
&lt;li>The specific post-market monitoring required by Art. 72.&lt;/li>
&lt;li>Serious incident reporting to authorities within legal deadlines (not only to users).&lt;/li>
&lt;li>Transparency obligations towards users for limited-risk systems (Art. 50): chatbots, deepfakes, generated content.&lt;/li>
&lt;li>The Art. 5 prohibitions (social scoring, manipulation, real-time biometrics with exceptions).&lt;/li>
&lt;/ul>
&lt;h3 id="nis2-dir-20222555--three-pillars-with-overlap">NIS2 (Dir. 2022/2555) — three pillars with overlap&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Asset register&lt;/strong> (Art. 21.2.f): AI systems within NIS2 scope must be in the asset inventory. → Overlaps with A.4 + clause 4.3 (scope).&lt;/li>
&lt;li>&lt;strong>Incident notification&lt;/strong> (Art. 23): significant incidents are notified within 24 h (initial alert) + 72 h (detailed report). → Overlaps with A.3.3 (reporting) + clause 10 (improvement).&lt;/li>
&lt;li>&lt;strong>Supply chain security&lt;/strong> (Art. 21.2.d): security assessment of the digital supply chain. → Overlaps with A.10.3 (suppliers).&lt;/li>
&lt;/ul>
&lt;p>For essential NIS2 entities that also use AI systems, 42001 covers the AI-specific part that NIS2 requires by inference but does not spell out.&lt;/p>
&lt;h3 id="ens-rd-3112022">ENS (RD 311/2022)&lt;/h3>
&lt;p>The Spanish Esquema Nacional de Seguridad already expressly addresses AI in its Annex II (ENS controls). The Basic/Medium/High categories align with the 42001 impact levels. The ENS controls on traceability (op.exp.8), activity logging (op.exp.10) and incident management (op.exp.7) are covered by the same technical artefacts as A.8 and A.5 of 42001. An organisation certified at ENS High category with AI systems is a reasonable effort away from adding 42001.&lt;/p>
&lt;h2 id="the-five-usual-certification-traps">The five usual certification traps&lt;/h2>
&lt;p>&lt;strong>Trap 1 — Confusing 42001 with EU AI Act compliance.&lt;/strong> Passing the 42001 audit does not imply conformity with the European Regulation. They are distinct universes with a 60-70% overlap. The trap surfaces when the client asks for CE marking of the high-risk system and the organisation produces only the 42001 certificate.&lt;/p>
&lt;p>&lt;strong>Trap 2 — Over-documenting.&lt;/strong> Two-hundred-page manuals with procedures copied from templates, with no measurables and no evidence of application. The auditor asks for the last execution of the procedure; with no records, the procedures are ornament. The practical rule: prefer short documents referencing live technical artefacts to long self-contained ones.&lt;/p>
&lt;p>&lt;strong>Trap 3 — Under-measuring.&lt;/strong> Defining AIMS objectives with no operational metrics. &amp;ldquo;Improve model quality&amp;rdquo; is a null objective; &amp;ldquo;F1 per guardrail category ≥ 0.85 over real traffic, measured weekly, reviewed quarterly in the management review&amp;rdquo; is an auditable objective. The blog has insisted on this in every post on &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">evals&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">guardrails&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">retrain&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Trap 4 — Ignoring A.5 until audit day.&lt;/strong> The impact assessment is the most underrated control and the first one the auditor asks for. Without per-system assessments carried out before deployment, there is no way to demonstrate A.5. The trap surfaces when there is no longer time to produce credible retrospective assessments.&lt;/p>
&lt;p>&lt;strong>Trap 5 — Assuming 27001 covers the AI part.&lt;/strong> Organisations that already have 27001 sometimes think &amp;ldquo;we have half of it done&amp;rdquo;. That is true for Annex SL (structure) and for A.5/A.6/A.7 of 27001 (not of 42001) as far as infosec goes. It is false for A.5 of 42001 (impact assessment), A.7 of 42001 (AI-specific data quality), A.9 (responsible use) and A.10.4 (AI customers). You have to add, not assume.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-posts">What we have not covered (upcoming posts)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Concrete templates&lt;/strong> of the seven mandatory documents, with drafting examples and metrics. Material for a post along the lines of &amp;ldquo;The AIMS manual in 7 documents&amp;rdquo; with a reference frame.&lt;/li>
&lt;li>&lt;strong>Detailed article-by-article mapping to the EU AI Act&lt;/strong> with the checklist of technical evidence derivable from the blog&amp;rsquo;s OSS stack. Especially Arts 11 (technical documentation), 14 (human oversight) and 72 (post-market monitoring).&lt;/li>
&lt;li>&lt;strong>The combined ENS High category + 42001 case&lt;/strong>: which ENS controls are covered by which AIMS artefacts, avoiding duplication.&lt;/li>
&lt;li>&lt;strong>NIST AI RMF 1.0 vs 42001 comparison&lt;/strong>: many international clients ask for both. How the same artefacts are recycled to satisfy the two frameworks.&lt;/li>
&lt;li>&lt;strong>42001 for LLM agents and MCP&lt;/strong>: the new dimensions that emerge when the AI system is agentic (excessive agency, tool use, graduated autonomy). The &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">guardrails&lt;/a> post introduced line 3 (tool GR); 42001 has open gaps in this terrain and SC 42 is working on addenda.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>ISO/IEC 42001:2023&lt;/strong> — &lt;em>Information technology — Artificial intelligence — Management system&lt;/em>. ISO. &lt;a href="https://www.iso.org/standard/81230.html">https://www.iso.org/standard/81230.html&lt;/a>.&lt;/li>
&lt;li>&lt;strong>ISO/IEC 22989:2022&lt;/strong> — &lt;em>Information technology — Artificial intelligence — Artificial intelligence concepts and terminology&lt;/em>. Define los roles AI provider/producer/customer/partner/subject.&lt;/li>
&lt;li>&lt;strong>ISO/IEC 23894:2023&lt;/strong> — &lt;em>Information technology — Artificial intelligence — Guidance on risk management&lt;/em>. Insumo de A.5.&lt;/li>
&lt;li>&lt;strong>ISO/IEC 38507:2022&lt;/strong> — &lt;em>Governance implications of the use of AI by organizations&lt;/em>. Complemento de gobierno.&lt;/li>
&lt;li>&lt;strong>ISO/IEC 5259&lt;/strong> — &lt;em>Data quality for analytics and machine learning&lt;/em> (familia). Insumo de A.7.&lt;/li>
&lt;li>&lt;strong>EU AI Act (Regulation 2024/1689)&lt;/strong> — texto consolidado en EUR-Lex. Entrada en vigor de obligaciones de alto riesgo: 2 ago 2026.&lt;/li>
&lt;li>&lt;strong>NIS2 (Directive 2022/2555)&lt;/strong> — texto consolidado en EUR-Lex.&lt;/li>
&lt;li>&lt;strong>ENS — Real Decreto 311/2022&lt;/strong> — Esquema Nacional de Seguridad, BOE-A-2022-7191.&lt;/li>
&lt;li>&lt;strong>NIST AI RMF 1.0&lt;/strong> (2023) — &lt;a href="https://www.nist.gov/itl/ai-risk-management-framework">https://www.nist.gov/itl/ai-risk-management-framework&lt;/a>.&lt;/li>
&lt;li>&lt;strong>EUR-Lex EU AI Act consolidated text&lt;/strong> — &lt;a href="https://eur-lex.europa.eu/eli/reg/2024/1689">https://eur-lex.europa.eu/eli/reg/2024/1689&lt;/a>.&lt;/li>
&lt;li>A-LIGN / BSI / Schellman — blogs sobre experiencia de auditoría 42001 con casos reales 2024-2025.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/hardening-secrets-sovereign-llm-stack-defence-in-depth/">Hardening and secrets in the sovereign LLM stack: defence in depth&lt;/a> — the operational security controls that hold up the AIMS.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage LLMOps pipeline&lt;/a> — the operating procedure that materialises A.6 lifecycle with no extra work.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">Anatomy of an LLM request in production&lt;/a> — the forensic case walked through as a 42001 checklist in the &amp;ldquo;applied case&amp;rdquo; section of this post.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">Seven layers of the on-premise LLM inference stack&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/greenfield-deployment-phases-on-premise-llm-platform/">seven deployment phases&lt;/a> — direct material for A.4 resources.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/">Five maturity levels of the platform&lt;/a> — how to justify the proportionality of the controls against the existing maturity level.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">Data versioning with DVC and lakeFS&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/rag-corpus-curation-active-librarian/">RAG corpus curation&lt;/a> — A.7 data covered in detail.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">LLM tracing with OpenTelemetry GenAI&lt;/a> — A.8 information to interested parties through standardised traceability.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety in LLMs&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/llm-guard-anatomy-scanners-langfuse-vllm-litellm/">LLM Guard&lt;/a> — A.9 responsible use.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals: the layer after tracing&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/llm-as-judge-exam-marker-not-oracle/">LLM-as-judge&lt;/a> — A.6.2.5 verification and validation.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain: closing the feedback → dataset → adapter loop&lt;/a> — clause 10 continual improvement + the incident-driven loop that feeds formal nonconformities.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/parallel-catalogue-llmops-stages-open-source-hyperscalers/">The parallel catalogue: OSS vs hyperscalers&lt;/a> — A.10.3 supplier assessment with a structural analysis of lock-in and contractual sovereignty; a direct input to the AI supplier register.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">The OSS catalogue for LLMOps&lt;/a> — A.10.5 inventory of OSS third parties with documented licence, governance and maturity.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/mlops-for-llms-landscape-2026/">LLM-specific MLOps in 2026: the landscape&lt;/a> — the operational context in which the AIMS runs and is audited.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/eu-ai-act-technical-dossier-on-premise-llm-architecture/">EU AI Act: the technical dossier, article by article&lt;/a> — the sister post on Regulation (EU) 2024/1689; it moves from the management system down to the directly applicable legal obligations, with deadlines, penalties and a control-to-article mapping.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/technical-controls-ens-iso-42001-eu-ai-act-cross-mapping/">Technical controls: the cross-mapping of ENS × ISO 42001 × EU AI Act&lt;/a> — the third post of the governance trilogy; it goes down to the detail of the 25 technical controls common to the three frameworks, with the master table of triple compliance and the labelling of evidence.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/incident-response-runbooks-llm-inference-kafka-keep/">Incident response runbooks for LLMs with Keep + Kafka&lt;/a> — the operational materialisation of clause 10 (continual improvement) and the WORM trace A.8.2 demands: every incident opens a nonconformity, triggers a postmortem, updates the runbook and is recorded in the Kafka &lt;code>audit.actions&lt;/code> topic.&lt;/p>
&lt;/li>
&lt;/ul></description></item><item><title>RAG corpus curation: the active librarian who decides what goes in, what goes out and what gets signed</title><link>https://blog.lo0.es/en/posts/rag-corpus-curation-active-librarian/</link><pubDate>Mon, 25 May 2026 11:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/rag-corpus-curation-active-librarian/</guid><description>&lt;blockquote>
&lt;p>This is the &lt;strong>curation&lt;/strong> layer inside stage 1 (Data) of the &lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">six-stage LLMOps pipeline&lt;/a>. It complements the other Data posts: &lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">dataset versioning&lt;/a> for the four versionable artefacts, &lt;a href="https://blog.lo0.es/en/posts/postgresql-qdrant-rag-ingestion-postman-syncing-two-worlds/">PostgreSQL + Qdrant ingestion in microservices&lt;/a> for the outbox + CDC pattern, and &lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">RAG over Kafka and a datalake&lt;/a> for streaming transport. This post is not about moving data: it is about what to do with data before you let a model read it.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>A RAG system serving mediocre answers is rarely the retriever&amp;rsquo;s fault, nor the model&amp;rsquo;s. The root cause is usually the corpus: three nearly identical versions of the same PDF that make top-k return the same thing three times, an old manual nobody removed that contradicts the current one, a free-text field with customer numbers that the model quotes verbatim, a scanned PDF with dirty OCR that the chunker cut in the middle of a sentence. None of that is fixed by changing the model, the embedder, the reranker or the prompt. It is fixed by curating the corpus. This post takes apart the five operational layers of curation (schema-validated ingest, three-level deduplication, PII anonymisation measured with precision/recall, anti-contamination against the golden eval set, chunk→trace lineage), the minimum maths needed to avoid fooling yourself, the 2026 stack (Presidio, Unstructured, Argilla, LangChain text splitters, OpenLineage, Marquez, Great Expectations), the seven traps that reduce the stage to theatre, and the on-premise hardware to sustain all of it without sending anything sensitive to external APIs.&lt;/p>
&lt;h2 id="the-analogy-the-active-librarian">The analogy: the active librarian&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="RAG corpus curation as an active librarian">
&lt;style>
.lbox{fill:#f8f8f8;stroke:#444;stroke-width:1.4;rx:8}
.lhead{fill:#ff8a4c;stroke:#444;stroke-width:1.4;rx:8}
.lstage{fill:#7aafff;stroke:#444;stroke-width:1.4;rx:8}
.lblt{font:600 13px sans-serif;fill:#222}
.lsub{font:400 11px sans-serif;fill:#555}
.larr{stroke:#666;stroke-width:1.5;fill:none;marker-end:url(#mc1)}
.lgate{fill:#ffd76b;stroke:#444;stroke-width:1.6;rx:6}
.lrej{fill:#f4b8b8;stroke:#a44;stroke-width:1.4;rx:6}
&lt;/style>
&lt;defs>&lt;marker id="mc1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;rect x="20" y="20" width="180" height="60" class="lhead"/>
&lt;text x="110" y="44" text-anchor="middle" class="lblt">Document arrives&lt;/text>
&lt;text x="110" y="62" text-anchor="middle" class="lsub">PDF, HTML, ticket, manual&lt;/text>
&lt;text x="110" y="76" text-anchor="middle" class="lsub">(book offered to the librarian)&lt;/text>
&lt;rect x="240" y="20" width="160" height="60" class="lstage"/>
&lt;text x="320" y="44" text-anchor="middle" class="lblt">1 · Schema check&lt;/text>
&lt;text x="320" y="62" text-anchor="middle" class="lsub">Great Expectations&lt;/text>
&lt;text x="320" y="76" text-anchor="middle" class="lsub">required fields + types&lt;/text>
&lt;rect x="440" y="20" width="160" height="60" class="lstage"/>
&lt;text x="520" y="44" text-anchor="middle" class="lblt">2 · Dedup&lt;/text>
&lt;text x="520" y="62" text-anchor="middle" class="lsub">exact + near + semantic&lt;/text>
&lt;text x="520" y="76" text-anchor="middle" class="lsub">hash + MinHash + cosine&lt;/text>
&lt;rect x="640" y="20" width="120" height="60" class="lstage"/>
&lt;text x="700" y="44" text-anchor="middle" class="lblt">3 · PII&lt;/text>
&lt;text x="700" y="62" text-anchor="middle" class="lsub">Presidio + recall&lt;/text>
&lt;text x="700" y="76" text-anchor="middle" class="lsub">measured vs golden&lt;/text>
&lt;path class="larr" d="M200,50 L240,50"/>
&lt;path class="larr" d="M400,50 L440,50"/>
&lt;path class="larr" d="M600,50 L640,50"/>
&lt;rect x="100" y="130" width="220" height="60" class="lstage"/>
&lt;text x="210" y="154" text-anchor="middle" class="lblt">4 · Anti-contamination&lt;/text>
&lt;text x="210" y="172" text-anchor="middle" class="lsub">cross-check against golden eval set&lt;/text>
&lt;text x="210" y="186" text-anchor="middle" class="lsub">reject token-by-token overlaps&lt;/text>
&lt;rect x="360" y="130" width="220" height="60" class="lstage"/>
&lt;text x="470" y="154" text-anchor="middle" class="lblt">5 · Lineage emit&lt;/text>
&lt;text x="470" y="172" text-anchor="middle" class="lsub">OpenLineage event with source,&lt;/text>
&lt;text x="470" y="186" text-anchor="middle" class="lsub">hash, schema_version, embedder&lt;/text>
&lt;rect x="600" y="130" width="160" height="60" class="lgate"/>
&lt;text x="680" y="154" text-anchor="middle" class="lblt">Gate&lt;/text>
&lt;text x="680" y="172" text-anchor="middle" class="lsub">passes all 5 layers →&lt;/text>
&lt;text x="680" y="186" text-anchor="middle" class="lsub">index in the vector store&lt;/text>
&lt;path class="larr" d="M700,86 L210,124"/>
&lt;path class="larr" d="M700,86 L470,124"/>
&lt;path class="larr" d="M700,86 L680,124"/>
&lt;rect x="80" y="240" width="280" height="60" class="lbox"/>
&lt;text x="220" y="264" text-anchor="middle" class="lblt">Accept → living corpus&lt;/text>
&lt;text x="220" y="282" text-anchor="middle" class="lsub">chunks with metadata, embeddings computed,&lt;/text>
&lt;text x="220" y="296" text-anchor="middle" class="lsub">indexed with dataset_hash in metadata&lt;/text>
&lt;rect x="400" y="240" width="280" height="60" class="lrej"/>
&lt;text x="540" y="264" text-anchor="middle" class="lblt">Reject → auditable quarantine&lt;/text>
&lt;text x="540" y="282" text-anchor="middle" class="lsub">rejection reason + diff vs previous version&lt;/text>
&lt;text x="540" y="296" text-anchor="middle" class="lsub">available for human review&lt;/text>
&lt;path class="larr" d="M580,196 L220,236"/>
&lt;path class="larr" d="M700,196 L540,236"/>
&lt;text x="390" y="338" text-anchor="middle" class="lsub" style="font-style:italic;">The active librarian: pass 5 layers or stay out. Nothing is accepted because "it was already there".&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>A serious librarian does not accept books by the kilo. When someone offers a new volume:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>They look at the spine, the ISBN and the stamp&lt;/strong>: is it legible? Is it catalogued correctly? Does it belong to a recognised collection? Without valid metadata, it does not get in. This is the &lt;strong>schema check&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>They check whether they already hold a copy&lt;/strong>: is it exactly the same book? Is it a later edition of the same one? Is it a translated version of something already on the shelves? If so, they decide explicitly what to do (replace, archive the old one, withdraw both from lending). This is &lt;strong>dedup&lt;/strong> at its three levels.&lt;/li>
&lt;li>&lt;strong>They flag what is restricted&lt;/strong>: if the book contains identifiable personal data, some pages cannot be lent as they are, so they must be redacted, anonymised or moved to the reserved section. This is &lt;strong>PII anonymisation&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>They verify it is not this year&amp;rsquo;s final exam book&lt;/strong>: if it is, it stays out of the public collection until the syllabus changes, because if students can consult it the exam stops measuring what it is meant to measure. This is &lt;strong>anti-contamination&lt;/strong> against the golden eval set.&lt;/li>
&lt;li>&lt;strong>They write it into the register&lt;/strong>: this book, this edition, this provenance, this date, this person who approved the entry. This is &lt;strong>lineage&lt;/strong>.&lt;/li>
&lt;/ol>
&lt;p>If the book passes all five, it joins the collection. If it fails any of them, it goes to an &lt;strong>auditable quarantine shelf&lt;/strong> with the reason for rejection. The difference between a good collection and a mediocre one is not size: it is how much discipline you apply across the five layers, every day, to every new book that arrives.&lt;/p>
&lt;p>A RAG corpus is exactly that. The only differences are scale (thousands or millions of documents per month) and the fact that the &amp;ldquo;readers&amp;rdquo; are LLMs that cannot tell a duplicate from a reinforced truth, nor a PII value from a synthetic example, nor a contaminated fragment from an authentic one.&lt;/p>
&lt;h2 id="the-four-data-artefacts-and-where-the-rag-corpus-fits">The four data artefacts and where the RAG corpus fits&lt;/h2>
&lt;p>Before going down to the five layers it is worth being clear about which corpus we are curating. The Data stage of the pipeline manages four distinct artefacts, each with its own discipline. The &lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">data versioning post&lt;/a> lists them; here I reorder them from a curation perspective:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Artefact&lt;/th>
&lt;th>Who consumes it&lt;/th>
&lt;th>Dominant curation&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Training dataset&lt;/strong>&lt;/td>
&lt;td>Tune (fine-tuning the model or an adapter)&lt;/td>
&lt;td>aggressive dedup + quality filters + label balancing&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>RAG corpus&lt;/strong>&lt;/td>
&lt;td>Deploy (retrieval at request time)&lt;/td>
&lt;td>the 5 layers in this post&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Golden eval set&lt;/strong>&lt;/td>
&lt;td>Eval (promotion gates)&lt;/td>
&lt;td>strict hold-out + stratification + maintenance driven by incidents&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Retrain enriched dataset&lt;/strong>&lt;/td>
&lt;td>Retrain (closing the loop)&lt;/td>
&lt;td>production feedback + human triage&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;strong>RAG corpus&lt;/strong> is the most volatile of the four and the one most exposed to the end user: every answer the system serves literally contains fragments of it. A duplicate in the training dataset degrades learning but ends up buried in the weights; a duplicate in the RAG corpus shows up in today&amp;rsquo;s answer and tomorrow&amp;rsquo;s. That justifies the extra discipline that follows.&lt;/p>
&lt;h2 id="layer-1--schema-validated-ingest">Layer 1 — Schema-validated ingest&lt;/h2>
&lt;p>Every piece entering the corpus has to arrive with &lt;strong>structured metadata validated against a schema&lt;/strong>. This is not bureaucracy: it is the only way to make the following layers (dedup, PII, lineage) work without friction.&lt;/p>
&lt;p>The canonical pattern is to define a schema in JSON Schema or Pydantic that every document must satisfy:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">class&lt;/span> &lt;span class="nc">CorpusDocument&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">BaseModel&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">source_system&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="c1"># e.g. &amp;#34;confluence&amp;#34;, &amp;#34;salesforce&amp;#34;, &amp;#34;manual_pdf&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">source_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="c1"># unique ID in the source system&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">version&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="c1"># document version (semver or date)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">language&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="c1"># ISO 639-1: &amp;#34;es&amp;#34;, &amp;#34;en&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">title&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">body&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">captured_at&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">datetime&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">captured_by&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="c1"># pipeline or human&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sensitivity&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">Literal&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;public&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;internal&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;restricted&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">schema_version&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="c1"># version of the schema itself, not of the document&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Any piece that fails this contract is rejected at ingest and never reaches the following layers. Validation is done with &lt;strong>Great Expectations&lt;/strong> (declarative suites), &lt;strong>Pandera&lt;/strong> (more pythonic, integrates with pandas) or &lt;strong>Soda&lt;/strong> (oriented towards continuous data quality). The choice is a matter of style; what matters is:&lt;/p>
&lt;ul>
&lt;li>Validation suites live in code and are versioned with the pipeline, not in a notebook off to one side.&lt;/li>
&lt;li>Rejection generates an auditable event (quarantine) with the specific schema failure as its reason, not a log line lost in stdout.&lt;/li>
&lt;li>The schema itself is versioned. When it changes, previous documents are reprocessed or explicit backward compatibility is maintained.&lt;/li>
&lt;/ul>
&lt;p>The &lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">RAG over Kafka post&lt;/a> covers the &lt;strong>Schema Registry&lt;/strong> pattern (Confluent Schema Registry or Apicurio) that materialises this in streaming: every message on the topic is validated against the registered schema before propagating downstream. For batch or pull, Great Expectations is the equivalent.&lt;/p>
&lt;p>&lt;strong>Common trap&lt;/strong>: leaving the &lt;code>body&lt;/code> field free with no further validation. You have to tighten it up with minimum/maximum length (a PDF that yields 12 characters after extraction is almost certainly broken), valid encoding (UTF-8 with no control characters), proportion of alphanumeric characters (dirty OCR returns a soup of symbols). These are simple rules that filter 80% of the noise with no need for AI.&lt;/p>
&lt;h2 id="layer-2--three-level-deduplication">Layer 2 — Three-level deduplication&lt;/h2>
&lt;p>The most expensive and most silent mistake in a RAG corpus is the duplicate. A document that appears three times in the corpus makes retrieval&amp;rsquo;s top-k return it three times, wasting two slots and reinforcing a single source. The LLM reads it as if three independent sources agreed, when in fact it is the same thing repeated.&lt;/p>
&lt;p>Deduplication is done at three levels, in this order by cost:&lt;/p>
&lt;h3 id="level-a--exact-dedup-sha-256-hash">Level A — Exact dedup (SHA-256 hash)&lt;/h3>
&lt;p>Compute the hash of the normalised content (trim, lower-case where applicable, remove redundant whitespace) and compare it against an index of already ingested hashes. On a match, discard or replace. Cost: \(O(1)\) per document. It catches literal duplicates (the same PDF uploaded twice, two byte-for-byte copies of the same HTML).&lt;/p>
&lt;h3 id="level-b--near-duplicate-minhash--lsh">Level B — Near-duplicate (MinHash + LSH)&lt;/h3>
&lt;p>Nearly identical documents with minor differences (a different header, an updated date, one version in Castilian and another in Galician with minimal changes). The canonical algorithm is &lt;strong>MinHash with Locality-Sensitive Hashing (LSH)&lt;/strong>, which approximates Jaccard similarity over shingles of k tokens. For n documents, comparing all against all is \(O(n^2)\), which is unworkable for large corpora. LSH cuts the cost to \(O(n)\) most-likely buckets.&lt;/p>
&lt;p>A typical threshold is Jaccard ≥ 0.80 over shingles of 5 tokens. The standard libraries are &lt;code>datasketch&lt;/code> (Python, MIT) or &lt;code>dedup&lt;/code> (Python, MIT). A numerical example: for 1 M short documents (300 tokens each), &lt;code>datasketch.MinHashLSH&lt;/code> with 128 permutations and threshold 0.8 takes about 2 GB of RAM and processes the full corpus in around 30 minutes on a modern CPU. The fraction of duplicates found in a real enterprise corpus is usually between 5% and 25%; removing them cuts storage and improves retrieval quality at the same time.&lt;/p>
&lt;h3 id="level-c--semantic-dedup-cosine-over-embeddings">Level C — Semantic dedup (cosine over embeddings)&lt;/h3>
&lt;p>Documents saying the same thing in different words, such as paraphrases, translations or rewritten versions, are not caught by MinHash. This is where semantic similarity comes in: compute the embedding of each document and compare the cosine between pairs.&lt;/p>
&lt;p>The problem is quadratic cost: for n documents, computing every pair is \(O(n^2)\). For n = 1 M and 768-dimensional embeddings (a typical model such as &lt;code>BAAI/bge-base-en-v1.5&lt;/code>), that is 5×10^11 dot products, which is unworkable. The solution is the same idea as LSH but over dense vectors: &lt;strong>HNSW&lt;/strong> (Hierarchical Navigable Small World) or &lt;strong>IVF&lt;/strong> (Inverted File) to build an approximate search index. For each new document you run a k-NN query against the index and examine only the k nearest neighbours.&lt;/p>
&lt;p>A sensible threshold for treating something as a semantic duplicate: &lt;strong>cosine ≥ 0.95&lt;/strong>. Below 0.95 the documents are related but distinct; above it, they are almost always the same information rewritten. The exact threshold is calibrated by observing precision/recall over a human-annotated sample; 100 pairs confirmed by a reviewer is reasonable for setting it.&lt;/p>
&lt;p>A numerical example: with &lt;code>qdrant&lt;/code> or &lt;code>pgvector&lt;/code> as the HNSW index and k=10 neighbours per query, deduplicating 1 M documents against the existing corpus takes on the order of 2-4 hours on an RTX 4090 (including embedding computation). If the embedder is self-hosted with vLLM, the cost per token is negligible against compute time.&lt;/p>
&lt;h3 id="policy-on-what-to-do-with-a-duplicate">Policy on what to do with a duplicate&lt;/h3>
&lt;p>Detection is not enough, you have to decide. Three common policies, in order of complexity:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Drop&lt;/strong>: discard the more recent one, keep the older one. Simple, with no extra lineage.&lt;/li>
&lt;li>&lt;strong>Replace&lt;/strong>: discard the old one, index the new one. More volatility but it reflects the update.&lt;/li>
&lt;li>&lt;strong>Merge with provenance&lt;/strong>: mark the new one as a &amp;ldquo;shadow&amp;rdquo; of the old one, keep both in lineage but index only one. Better for regulated audit.&lt;/li>
&lt;/ul>
&lt;p>The policy has to be explicit and applied uniformly, not an ad-hoc decision per document.&lt;/p>
&lt;h2 id="layer-3--pii-anonymisation-with-measured-precisionrecall">Layer 3 — PII anonymisation with measured precision/recall&lt;/h2>
&lt;p>This layer is the one that most easily turns into theatre. The typical mistake: install Presidio, run it over the corpus, assume the output is clean. Without measuring the detector&amp;rsquo;s precision and recall against an annotated golden set, you know nothing.&lt;/p>
&lt;p>A PII detector can fail in two ways:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>False negative&lt;/strong> (low recall): it does not detect a national ID written as &amp;ldquo;12345678-A&amp;rdquo; because your model was trained on the &lt;code>12345678A&lt;/code> format without a hyphen. The RAG serves personal data unredacted.&lt;/li>
&lt;li>&lt;strong>False positive&lt;/strong> (low precision): it redacts a number from a configuration manual thinking it is a phone number. The RAG loses useful information.&lt;/li>
&lt;/ul>
&lt;p>Both are problems; regulation (GDPR, ENS, NIS2) penalises the first, and user experience degrades with the second. The acceptable ratio depends on the domain. In medical data, practically zero false negatives is non-negotiable; in internal technical documentation you can tolerate more recall in exchange for less precision.&lt;/p>
&lt;p>The standard metric is &lt;strong>F1 over an annotated golden set&lt;/strong>:&lt;/p>
\[
\text{precision} = \frac{TP}{TP + FP}, \quad \text{recall} = \frac{TP}{TP + FN}, \quad F_1 = 2 \cdot \frac{\text{precision} \cdot \text{recall}}{\text{precision} + \text{recall}}
\]
&lt;p>To build the PII golden set, annotate around 200 documents by hand with every entity marked (national ID, IBAN, email, phone, address, personal name). Then run the detector and compute the metrics per category, not just aggregated, because a global F1 of 0.90 can hide a recall of 0.55 on IBANs.&lt;/p>
&lt;p>&lt;strong>2026 stack&lt;/strong> for this layer:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Microsoft Presidio&lt;/strong> (MIT, Microsoft): the most complete OSS option. Configurable detectors, recognises around 50 entities by default, extensible with your own regex patterns or with fine-tuned NER models.&lt;/li>
&lt;li>&lt;strong>spaCy NER&lt;/strong> (MIT, Explosion AI): a base for custom detectors; useful when Presidio does not cover a domain entity.&lt;/li>
&lt;li>&lt;strong>Llama Guard 4&lt;/strong> (Llama Community License, Meta): a safety classifier that also detects PII in one pass, an option when you already have a GPU for inference and prefer a single pass.&lt;/li>
&lt;li>&lt;strong>DataFog&lt;/strong> (Apache 2.0): a more recent alternative, specialised in streaming pipelines.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Recommended hybrid pattern&lt;/strong>: Presidio for rule-based + regex detection (fast, deterministic) → Llama Guard as a second pass over what Presidio did not flag (an ensemble that raises recall without killing throughput). This is measured and reported as aggregate and per-category F1 on every release of the detector.&lt;/p>
&lt;p>&lt;strong>Common fallacy&lt;/strong>: trusting that a detector with F1 0.95 &amp;ldquo;is very good&amp;rdquo;. If you have 1 M documents and each contains on average 1 PII entity, F1 0.95 means 50,000 mishandled entities (between false positives and negatives). With sensitive data you have to design so that false negatives go to human quarantine, not to the public corpus.&lt;/p>
&lt;h2 id="layer-4--anti-contamination-against-the-golden-eval-set">Layer 4 — Anti-contamination against the golden eval set&lt;/h2>
&lt;p>If the RAG corpus contains fragments of the golden eval set, Eval metrics measure memorisation. The model returns the exact answer because it has it literally in its context, not because it generalised anything. Deploy then promotes models that shine in the exam and fail in production.&lt;/p>
&lt;p>This layer is the easiest to implement and the easiest to forget:&lt;/p>
&lt;ol>
&lt;li>The &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">golden eval set&lt;/a> has its own versioned hash.&lt;/li>
&lt;li>Before indexing any new document into the RAG corpus, run a token-by-token overlap check (or by shingles, similar to MinHash) against the golden set.&lt;/li>
&lt;li>If overlap exceeds a threshold (typically ≥ 30% of 5-token n-grams), the document is not indexed. It stays in quarantine flagged as &amp;ldquo;contamination risk vs golden_v12&amp;rdquo;.&lt;/li>
&lt;li>A human reviews the rejections. Sometimes they are false positives (a short quotation, a boilerplate sentence). Sometimes they are real contamination that a supplier introduced without realising.&lt;/li>
&lt;/ol>
&lt;p>The deeper reason: the RAG corpus and the golden set are enemies by design. The golden set measures how well the system generalises to questions it has not seen. If those questions are in the RAG, the system &amp;ldquo;sees&amp;rdquo; them on every query. The metric stops measuring generalisation.&lt;/p>
&lt;p>This check is computationally trivial, a hash join over n-grams. The difficulty is keeping it up: every time the golden set changes (monthly or quarterly), the full corpus has to be re-validated against the new golden set. Without that discipline, contamination sneaks in through the back door when someone updates the golden set with real cases that the RAG was already serving.&lt;/p>
&lt;h2 id="layer-5--end-to-end-lineage-from-document-to-trace">Layer 5 — End-to-end lineage: from document to trace&lt;/h2>
&lt;p>The last layer is the one that closes the auditable chain. Every chunk indexed in the vector store carries metadata that lets you answer the forensic question:&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;The system generated this answer on 14 March at 16:23. Which exact document did the quoted fragment come from? When did that document enter the corpus? Which version of the embedder processed it? Who approved its ingest?&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>Without lineage, that question is unanswerable. With lineage done properly, it is four queries.&lt;/p>
&lt;p>The canonical pattern:&lt;/p>
&lt;ul>
&lt;li>Every indexed chunk carries in its metadata: &lt;code>source_system&lt;/code>, &lt;code>source_id&lt;/code>, &lt;code>document_version&lt;/code>, &lt;code>chunk_index&lt;/code>, &lt;code>embedder_version&lt;/code>, &lt;code>dataset_hash&lt;/code>, &lt;code>ingested_at&lt;/code>, &lt;code>ingested_by&lt;/code>, &lt;code>schema_version&lt;/code>.&lt;/li>
&lt;li>Every RAG answer in production emits a trace span that includes the retrieved &lt;code>chunk_id&lt;/code> values.&lt;/li>
&lt;li>The central tracing system (&lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">Langfuse, Phoenix or OpenLLMetry&lt;/a>) joins &lt;code>chunk_id&lt;/code> → chunk metadata → document metadata → corpus &lt;code>dataset_hash&lt;/code> → embedder version → and so on.&lt;/li>
&lt;/ul>
&lt;p>The tools that standardise this glue are &lt;strong>OpenLineage&lt;/strong> (Apache 2.0, LF AI &amp;amp; Data) and &lt;strong>Marquez&lt;/strong> (Apache 2.0, its server implementation). They define a lineage event schema that interoperates between systems; an ingest job emits an event &amp;ldquo;produced corpus_v12.3 from source X with embedder bge-base-v1.5&amp;rdquo;; a retrieval job emits &amp;ldquo;consumed corpus_v12.3 with query Q produced response R&amp;rdquo;. The graph is reconstructed automatically.&lt;/p>
&lt;p>This layer is the only way to pass real audits under regulations such as the EU AI Act, GDPR or ENS, where traceability of which data went into which answer is a requirement, not an option. Without it, the answer &amp;ldquo;we do not know which document this came from&amp;rdquo; is not acceptable, and it is the default answer if lineage is not built from day one.&lt;/p>
&lt;h2 id="the-maths-that-matter">The maths that matter&lt;/h2>
&lt;p>Beyond dedup thresholds and PII F1 scores, there are three mathematical pieces that any serious team ends up using.&lt;/p>
&lt;p>&lt;strong>Chunk size vs retrieval quality&lt;/strong>. Chunk size affects retrieval quality in a non-monotonic way: chunks that are too small fragment ideas (retrieval returns a piece without context), and chunks that are too large dilute the signal (the embedding mixes several topics and similarity drops). The empirical sweet spot for technical text in 2026 is between &lt;strong>256 and 768 tokens per chunk&lt;/strong>, with &lt;strong>15-25% overlap&lt;/strong> between contiguous chunks to preserve continuity.&lt;/p>
&lt;p>Numerically, for a corpus of 1 M documents with a mean length of 2,000 tokens, chunking at 512 tokens with overlap 100 gives \(\frac{2000}{512 - 100} \approx 5\) chunks per document, a total of roughly 5 M indexed chunks. With 768-dimensional embeddings in &lt;code>float32&lt;/code>, that occupies \(5 \cdot 10^6 \cdot 768 \cdot 4 \approx 15\) GB of vector memory, manageable in any modern vector store.&lt;/p>
&lt;p>&lt;strong>PII golden set coverage&lt;/strong>. To know whether the annotated PII golden set is representative enough, compute the proportion of categories covered: if your golden set of 200 documents has 5 IBAN examples and production has 12,000 IBANs per day, the measured F1 on IBANs is statistical noise. Rule of thumb: &lt;strong>a minimum of 30 examples per category&lt;/strong> for per-category metrics to mean anything.&lt;/p>
&lt;p>&lt;strong>Cost of re-embedding when rotating the model&lt;/strong>. Changing the embedder invalidates the whole index. For a corpus of 5 M chunks with a model such as &lt;code>BAAI/bge-base-en-v1.5&lt;/code> (768 dim, around 110 M parameters) served in vLLM on 1× H100, throughput is on the order of 8,000-15,000 chunks/second. Full re-embedding: about 5-10 minutes. For a larger embedder (&lt;code>bge-large&lt;/code>, 1024 dim, around 335 M parameters), a factor of 3× worse, around 15-30 minutes. The bottleneck is usually vector store I/O, not GPU compute. The &lt;strong>dual-index&lt;/strong> pattern, keeping the old index serving while the new one is built and doing an atomic swap at the end, avoids downtime and allows rollback.&lt;/p>
&lt;h2 id="applied-to-typical-on-premise-hardware">Applied to typical on-premise hardware&lt;/h2>
&lt;p>For an on-premise deployment that keeps all curation in-house without sending data to external APIs:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>RTX 4090 (24 GB)&lt;/strong>: covers layer 1 (the schema check with Great Expectations is CPU-bound), layer 2 levels A and B (hash + MinHash are CPU-bound), and layer 2 level C semantic dedup with a &lt;code>bge-base&lt;/code>-class embedder (8-15k chunks/s, enough for corpora of up to 5-10 M chunks in hours). For Presidio in NER mode (layer 3) it runs comfortably. It is the sensible GPU for the whole curation pipeline on a mid-size corpus.&lt;/li>
&lt;li>&lt;strong>Generic 4×H100 SXM configuration (320 GB total, NVLink)&lt;/strong>: only needed if the corpus exceeds around 50 M chunks or if you want frequent re-embeddings with large models (&lt;code>bge-large&lt;/code>, &lt;code>e5-mistral&lt;/code>). In practice, two GPUs serve the embedder at TP=2 with throughput above 50k chunks/s, and the other two go to the PII judge (Llama Guard 4) or to serving the main inference model. Capacity for corpora of hundreds of millions of chunks.&lt;/li>
&lt;/ul>
&lt;p>The stubborn arithmetic: with a 4090, corpus curation is an overnight job; with 4×H100, it is minutes. The decision depends on corpus size and on how often you rotate the embedder or the PII rules.&lt;/p>
&lt;h2 id="the-seven-traps-that-kill-this-stage">The seven traps that kill this stage&lt;/h2>
&lt;p>&lt;strong>Trap 1 — No validated schema at ingest.&lt;/strong> Malformed documents reach the chunker, the chunker slices them into nonsense, garbage embeddings enter the index. The RAG answer quotes incoherent text and nobody knows why.&lt;/p>
&lt;p>&lt;strong>Trap 2 — Dedup only at exact hash level.&lt;/strong> The corpus fills up with paraphrases and translations of the same document. Retrieval&amp;rsquo;s top-k returns the same source 3 times. The LLM reads it as three confirmations.&lt;/p>
&lt;p>&lt;strong>Trap 3 — PII detector without precision/recall measurement.&lt;/strong> Everyone assumes Presidio &amp;ldquo;works&amp;rdquo;. IBANs in non-standard formats slip through. The RAG serves personal data.&lt;/p>
&lt;p>&lt;strong>Trap 4 — Golden eval set contaminated with the corpus.&lt;/strong> Eval metrics measure memorisation. Promotions approve models that fail in real production.&lt;/p>
&lt;p>&lt;strong>Trap 5 — No lineage down to the chunk.&lt;/strong> The question &amp;ldquo;where did this quotation come from?&amp;rdquo; has no answer. Regulatory audit fails. Incidents cannot be investigated.&lt;/p>
&lt;p>&lt;strong>Trap 6 — Maintenance as a one-off event.&lt;/strong> The corpus is curated once when the system is initialised, and afterwards everyone assumes it is fine. Six months later the documents are out of date, the new PII rules are not applied retrospectively, dedup has not been re-run after adding new sources. The corpus degrades silently.&lt;/p>
&lt;p>&lt;strong>Trap 7 — Quarantine without human review.&lt;/strong> Rejected documents go into a table nobody looks at. False positives pile up, real contamination cases go uninvestigated, the team&amp;rsquo;s trust in curation erodes and the pressure to &amp;ldquo;relax the thresholds&amp;rdquo; begins.&lt;/p>
&lt;p>All seven are operational, not technical. Corpus curation does not break because of an algorithm bug: it breaks because discipline slackens. It is the exact equivalent of the kind of degradation that kills Eval suites, and in both cases the symptom is the same: the metrics improve or hold steady while the real experience gets worse.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-posts">What we have not covered (upcoming posts)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Vector store versioning proper&lt;/strong>: an embedding index is not versioned like a raw dataset because it depends on the embedding model. Changing the embedder rewrites the whole index. It is a different animal with its own patterns (index branching, selective reembedding, recall-aware ANN parameters).&lt;/li>
&lt;li>&lt;strong>Streaming corpus updates with CDC&lt;/strong>: when the corpus has to be updated in near-real time from an OLTP system. The &lt;a href="https://blog.lo0.es/en/posts/postgresql-qdrant-rag-ingestion-postman-syncing-two-worlds/">Postgres + Qdrant ingestion post&lt;/a> covers the mechanics; still pending is the pattern for selectively invalidating chunks that depend on deleted rows.&lt;/li>
&lt;li>&lt;strong>Multi-tenant corpus isolation&lt;/strong>: how to set up a shared corpus versus one with per-tenant namespaces, with ACLs over individual chunks. Especially relevant for multi-customer RAG under data sovereignty constraints.&lt;/li>
&lt;li>&lt;strong>Federated corpus&lt;/strong>: corpora distributed across silos that the system queries without centralising the content. An emerging pattern for companies with several sites and cross-border restrictions.&lt;/li>
&lt;li>&lt;strong>Reranking-aware curation&lt;/strong>: how curation discipline changes when there is a reranker (Cohere Rerank, ColBERTv2, BGE-Reranker) reordering the top-k after retrieval. Some duplicates you would tolerate without a reranker become intolerable when the reranker pushes them up the ranking.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/end-to-end-document-ingestion-pdf-to-indexed-chunk/">End-to-end document ingestion: from PDF to indexed chunk&lt;/a> — how curation materialises in the ingestion pipeline, from PDF to indexed chunk.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage LLMOps pipeline&lt;/a> — Stage 1 (Data) and why curation is the most underrated sub-task in the whole chain.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/embeddings-2026-dense-sparse-multivector-families/">Embeddings in 2026: the three families, the model zoo and the decision that matters&lt;/a> — which cartographer processes the chunks curated here to turn them into searchable vectors; embedder selection criteria (bge-m3 / Snowflake Arctic / Jina / Nomic / ColBERT) and storage cost.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/ontologies-knowledge-graphs-llmops-linnaean-nomenclature/">Ontologies and knowledge graphs in LLMOps&lt;/a> — the Linnaean nomenclature without which curation is left with ad-hoc categories; how the chunks in this post are typed against a TBox (FIBO / SNOMED / ENS / schema.org), validated with SHACL and enriched with metadata queryable in SPARQL.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">Anatomy of an LLM request in production, May 2026&lt;/a> — the forensic tour crosses the corpus and the retrieved chunks; here are the criteria any chunk had to pass to be in production.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">Data versioning: DVC, lakeFS and the reproducible golden dataset challenge&lt;/a> — the four data artefacts and why they are versioned separately. The RAG corpus is one of the four.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/postgresql-qdrant-rag-ingestion-postman-syncing-two-worlds/">PostgreSQL + Qdrant in the ingestion stage&lt;/a> — the microservices pattern that moves documents from source to vector store. The curation in this post plugs in between ingest and indexer.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">RAG over Kafka and a datalake&lt;/a> — streaming transport. Schema Registry materialises layer 1.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals for LLMs: the layer after tracing&lt;/a> — the golden eval set is the RAG corpus&amp;rsquo;s &amp;ldquo;enemy by design&amp;rdquo;; layer 4 (anti-contamination) materialises the discipline between the two.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/prompt-versioning-contract-five-word-change/">Prompt versioning: the contract that stops a five-word change sinking your system&lt;/a> — the &lt;code>prompt_id&lt;/code> travelling in the trace is the counterpart of the corpus &lt;code>dataset_hash&lt;/code> in lineage.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain: closing the loop between the production incident and the adapter that fixes it&lt;/a> — the retrain enriched corpus also needs the five layers, with additional emphasis on human feedback.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">The OSS catalogue for LLMOps in six stages&lt;/a> — entries for Presidio, Unstructured, Argilla, Great Expectations, OpenLineage.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety in LLMs&lt;/a> — the curation in this post is prevention at ingest; guardrails are mitigation at runtime when something slips through. Line 2 (retrieval GR) filters chunks with indirect prompt injection before they enter the LLM&amp;rsquo;s context.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/iso-42001-aims-on-premise-llm-platform/">ISO/IEC 42001: the operations manual for an AI system&lt;/a> — the five curation layers in this post directly cover controls A.7.3 (acquisition), A.7.4 (quality), A.7.5 (provenance) and A.7.6 (preparation) of the AIMS Annex A.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Presidio&lt;/strong>: &lt;a href="https://microsoft.github.io/presidio/">https://microsoft.github.io/presidio/&lt;/a> — official docs, list of supported entities, guide to extending it with custom NER.&lt;/li>
&lt;li>&lt;strong>OpenLineage&lt;/strong>: &lt;a href="https://openlineage.io/">https://openlineage.io/&lt;/a> — event schema spec and libraries per language.&lt;/li>
&lt;li>&lt;strong>Marquez&lt;/strong>: &lt;a href="https://marquezproject.ai/">https://marquezproject.ai/&lt;/a> — server implementation of OpenLineage.&lt;/li>
&lt;li>&lt;strong>datasketch (MinHash + LSH)&lt;/strong>: &lt;a href="https://ekzhu.com/datasketch/">https://ekzhu.com/datasketch/&lt;/a> — the reference Python library for near-duplicate deduplication at scale.&lt;/li>
&lt;li>&lt;strong>Great Expectations&lt;/strong>: &lt;a href="https://docs.greatexpectations.io/">https://docs.greatexpectations.io/&lt;/a> — declarative data quality suites.&lt;/li>
&lt;li>&lt;strong>Unstructured&lt;/strong>: &lt;a href="https://docs.unstructured.io/">https://docs.unstructured.io/&lt;/a> — parsing and normalisation of heterogeneous documents (PDF, HTML, DOCX, eml) before chunking.&lt;/li>
&lt;li>&lt;strong>Argilla&lt;/strong>: &lt;a href="https://docs.argilla.io/">https://docs.argilla.io/&lt;/a> — human annotation UI for building the PII golden set and other calibration sets.&lt;/li>
&lt;li>&lt;strong>Llama Guard 4&lt;/strong>: Meta technical paper, multimodal safety classifier — useful as a second PII detection layer.&lt;/li>
&lt;li>&lt;strong>GDPR, EU AI Act, ENS, NIS2&lt;/strong> — the regulatory frameworks whose compliance depends, in practice, on the discipline of layers 3 (PII) and 5 (lineage). The CEN/CENELEC technical standards for conformity assessment of GenAI systems under the EU AI Act are still pending final publication.&lt;/li>
&lt;/ul></description></item><item><title>Data versioning for LLMOps: DVC, lakeFS and the reproducible golden dataset challenge</title><link>https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/</link><pubDate>Fri, 22 May 2026 11:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>The Data stage of the &lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">six-stage LLMOps pipeline&lt;/a> has a silent link on which everything else depends: &lt;strong>versioning the datasets&lt;/strong> with the same discipline the code is versioned with. It is not optional. An LLM system in production consumes at least four distinct types of dataset (training/fine-tuning, RAG corpus, golden eval set, enriched dataset from the Retrain loop) and each has its own demands. Git solves code but fails on data for two technical reasons (size and a useless binary diff) and one operational one (it does not propagate lineage as far as the weights bucket of the trained model). The two dominant OSS tools, &lt;strong>DVC&lt;/strong> and &lt;strong>lakeFS&lt;/strong>, &lt;strong>unified in November 2025&lt;/strong> under a single organisation with a roadmap oriented towards LLM training and RAG datalakes; they are still complementary projects (file-level vs whole-bucket branching) but now under common governance. The productive pattern the market has consolidated on: identify each artefact with an immutable &lt;code>(dataset_id, version)&lt;/code>, propagate the pair as far as the experiment tracking (MLflow / W&amp;amp;B), version the dataset&amp;rsquo;s schema too (not just its content), apply a strict holdout to the golden eval set so you are not measuring memorisation, and maintain bidirectional traceability &lt;code>dataset_version ↔ model_version ↔ deployment ↔ trace_id&lt;/code>. Without this, the promise of &amp;ldquo;we can audit which model answered what&amp;rdquo; collapses at the first serious incident.&lt;/p>
&lt;h2 id="you-are-here-data-with-a-cross-cutting-effect-on-tune-eval-and-retrain">You are here: Data (with a cross-cutting effect on Tune, Eval and Retrain)&lt;/h2>
&lt;p>This post goes into the detail of the versioning link inside stage &lt;strong>1 · Data&lt;/strong>. Versioning belongs operationally to Data, but the artefacts it produces travel to Tune (training set), Eval (golden set) and Retrain (enriched dataset). That is why the diagram marks Data as active and shows a cross-cutting band indicating end-to-end lineage.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 135" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Data with cross-cutting lineage">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff8a4c;stroke-width:3}.idle{fill:#f4f4f4}.cross{fill:#ffe9d6;stroke-width:1.4;stroke:#c66;rx:6}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#444}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#dvm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#dvm)}&lt;/style>
&lt;defs>&lt;marker id="dvm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">You are here: DATA · dataset versioning with lineage to the production trace&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box active"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box idle"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;rect x="30" y="98" width="735" height="25" class="cross"/>
&lt;text x="397" y="115" text-anchor="middle" class="sm">Dataset lineage: training set → Tune · golden set → Eval · enriched set → Retrain (back to Data)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-master-analogy-batch-traceability-in-a-serious-factory">The master analogy: batch traceability in a serious factory&lt;/h2>
&lt;p>A serious pharmaceutical factory does not produce without &lt;strong>batch traceability&lt;/strong>. Every box of pills carries a printed batch number; that batch is associated with manufacturing dates, with the specific batches of each raw material used, with the quality tests it passed, and with the technicians who signed off each step. If a patient reports an adverse effect, the factory can rewind within hours: this package → this batch → these raw materials → this shift → this production line → this quality control result. Without that chain, the incident is a permanent mystery.&lt;/p>
&lt;p>A serious LLM system works the same way. The &amp;ldquo;package&amp;rdquo; is the answer a user saw in production. The &amp;ldquo;batch&amp;rdquo; is the combination of model, adapter, prompt, context and configuration that generated it. And the &amp;ldquo;raw materials&amp;rdquo; are the datasets: the training set the base model was trained on, the adapter&amp;rsquo;s fine-tuning dataset, the RAG corpus that feeds retrieval, the golden eval set that validates promotion. If a customer says &lt;em>&amp;ldquo;what data was the model trained on that answered X to my question Y on 14 March?&amp;rdquo;&lt;/em>, without batch traceability the answer is &lt;em>&amp;ldquo;we do not know&amp;rdquo;&lt;/em>. And that, with a customer under compliance pressure, kills the contract.&lt;/p>
&lt;p>Git versions the recipe (the code). Data versioning versions the ingredients. Without both, there is no auditable factory.&lt;/p>
&lt;h2 id="the-four-artefacts-worth-versioning-with-differentiated-demands">The four artefacts worth versioning (with differentiated demands)&lt;/h2>
&lt;p>Not all datasets are versioned the same way or at the same frequency. The typical LLM system in production handles four artefacts worth governing separately.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Artefact&lt;/th>
&lt;th>What it is&lt;/th>
&lt;th>Typical size&lt;/th>
&lt;th>New-version frequency&lt;/th>
&lt;th>Who consumes it&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Training / fine-tuning dataset&lt;/strong>&lt;/td>
&lt;td>Input/output pairs (or conversations) that train the adapter or the model.&lt;/td>
&lt;td>10⁴ – 10⁷ examples · 1 – 100 GB&lt;/td>
&lt;td>Per Tune experiment&lt;/td>
&lt;td>Trainer (Axolotl, TRL, Unsloth)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>RAG corpus&lt;/strong>&lt;/td>
&lt;td>Indexed documents that feed retrieval.&lt;/td>
&lt;td>10⁵ – 10⁹ chunks · 10 GB – 10 TB&lt;/td>
&lt;td>Almost continuous (streaming ingest)&lt;/td>
&lt;td>Indexer + vector store&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Golden eval set&lt;/strong>&lt;/td>
&lt;td>Curated examples with an expected answer to measure quality.&lt;/td>
&lt;td>10² – 10⁴ examples · MB&lt;/td>
&lt;td>Per product release&lt;/td>
&lt;td>Eval gates in CI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Enriched retrain dataset&lt;/strong>&lt;/td>
&lt;td>Cases where the system failed + the human correction.&lt;/td>
&lt;td>Hundreds to thousands per quarter&lt;/td>
&lt;td>Per &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">retrain&lt;/a> cycle&lt;/td>
&lt;td>Next Tune&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>All four have common requirements (immutable identity, lineage, schema) and relevant differences:&lt;/p>
&lt;ul>
&lt;li>The &lt;strong>training set&lt;/strong> tends to be large, stable per experiment, and the cost of an error is a lost experiment (expensive but bounded).&lt;/li>
&lt;li>The &lt;strong>RAG corpus&lt;/strong> is enormous, in continuous change, and its versioning is handled through periodic snapshots of the index (not of the raw text). Usually lakeFS or bucket branches; DVC is not the best fit.&lt;/li>
&lt;li>The golden eval set is small but critical: errors here contaminate the whole promotion chain. Here the rigidity of the versioning matters more than anywhere else.&lt;/li>
&lt;li>The &lt;strong>enriched retrain dataset&lt;/strong> is incremental by nature: each Retrain cycle contributes a delta over the previous one. The new version does not overwrite; it inherits and adds.&lt;/li>
&lt;/ul>
&lt;p>Confusing them, treating the RAG corpus as if it were the training set, or the golden eval as if it were just another dataset, is the source of half the operational problems in data versioning.&lt;/p>
&lt;h2 id="why-git-is-not-enough">Why Git is not enough&lt;/h2>
&lt;p>The obvious question: if Git already solves code, why does it not solve data too? Three reasons, two technical and one operational.&lt;/p>
&lt;p>&lt;strong>Reason 1: size.&lt;/strong> A Git repository with a 50 GB dataset becomes unmanageable. &lt;code>git clone&lt;/code> pulls the entire history; &lt;code>git status&lt;/code> walks every file; the pack file in &lt;code>.git/objects&lt;/code> bloats to up to twice the size of the dataset. Git LFS solves the first part (the binary leaves the pack) but introduces its own complexity without addressing the other two reasons.&lt;/p>
&lt;p>&lt;strong>Reason 2: useless binary diff.&lt;/strong> Git assumes text diffs are useful. When a column changes in an 8 GB parquet, the diff is opaque, because the file is binary, compressed and columnar. You cannot do code review on a dataset change the way you do on a function change. You need a semantic diff: how many rows changed, which columns changed, which distribution moved. No native Git gives you that.&lt;/p>
&lt;p>&lt;strong>Reason 3: lineage that crosses repository boundaries.&lt;/strong> This is the most important and the most subtle. The training dataset lives in a bucket. The trainer code lives in a Git repo. The trained model is published to a model registry. Inference in production generates traces in an observability system. Connecting &lt;code>dataset_v3 → adapter_v7 → deployment_d2 → trace t_x9&lt;/code> requires propagating identifiers &lt;strong>across four different systems&lt;/strong>, not inside one repo. Git has no opinion on this.&lt;/p>
&lt;p>Data versioning tools (DVC, lakeFS, Pachyderm, Quilt) exist because they solve the three problems at once: they hang the data outside the Git repo, they offer some form of semantic diff, and they expose stable identities that can be propagated towards experiment tracking and the model registry.&lt;/p>
&lt;h2 id="dvc-vs-lakefs-before-the-unification">DVC vs lakeFS before the unification&lt;/h2>
&lt;p>Until November 2025, the two dominant OSS tools coexisted as complementary approaches.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Axis&lt;/th>
&lt;th>DVC&lt;/th>
&lt;th>lakeFS&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Mental model&lt;/td>
&lt;td>&amp;ldquo;Git for data&amp;rdquo;&lt;/td>
&lt;td>&amp;ldquo;Branching for the data lake&amp;rdquo;&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Granularity&lt;/td>
&lt;td>Individual file&lt;/td>
&lt;td>Whole bucket (with per-branch namespacing)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Storage&lt;/td>
&lt;td>Remote-agnostic (S3, GCS, Azure, MinIO, SSH)&lt;/td>
&lt;td>S3-compatible (S3, MinIO, Ceph)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Workflow&lt;/td>
&lt;td>&lt;code>dvc add&lt;/code> + &lt;code>dvc push&lt;/code> + &lt;code>dvc.yaml&lt;/code> pipelines&lt;/td>
&lt;td>&lt;code>lakectl commit&lt;/code> + branches/merges over the bucket&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Diff&lt;/td>
&lt;td>File hash + external metadata&lt;/td>
&lt;td>Object-level diff + commit log&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Strong cases&lt;/td>
&lt;td>Discrete training datasets, model files, reproducible pipelines&lt;/td>
&lt;td>Large RAG corpora, branching a shared data lake, parallel experiments without duplicating data&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Git integration&lt;/td>
&lt;td>Deep (the &lt;code>.dvc&lt;/code> files are committed to Git)&lt;/td>
&lt;td>Tangential (lakeFS lives alongside)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Who operates it&lt;/td>
&lt;td>MLE team&lt;/td>
&lt;td>Data engineering team&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>In practice, many teams used them at the same time: DVC for the discrete datasets that fed an experiment (they fit in a Git repo thanks to the indirection of the &lt;code>.dvc&lt;/code> pointers), and lakeFS for the large RAG corpus bucket they wanted to branch without duplicating terabytes.&lt;/p>
&lt;h2 id="what-changed-with-the-november-2025-acquisition">What changed with the November 2025 acquisition&lt;/h2>
&lt;p>&lt;a href="https://medium.com/the-modern-scientist/reproducible-ai-versioning-models-prompts-and-data-96dd0337af65">lakeFS acquired DVC&lt;/a> in November 2025. The operational consequence as of May 2026 is modest but relevant:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>There is (as yet) no technical merger of the projects.&lt;/strong> DVC is still DVC and lakeFS is still lakeFS. The current CLIs, formats and workflows have not changed.&lt;/li>
&lt;li>&lt;strong>An explicit combined roadmap towards LLM training and RAG datalakes.&lt;/strong> The merged organisation has stated specific priorities: consistent branching between the dataset and the trained model, native integrations with MLflow / W&amp;amp;B / Langfuse, support for the typical LLM formats (jsonl, parquet with embedded tokenisation), and branch-aware vector indexing.&lt;/li>
&lt;li>&lt;strong>Convergence expected in 2026-2027.&lt;/strong> The market anticipates a single registry with two operating modes (file-level + bucket-branching) under a unified CLI. As of today, teams still combine both.&lt;/li>
&lt;/ul>
&lt;p>The practical reading for 2026: &lt;strong>adopt DVC for discrete training/eval datasets and lakeFS for the RAG corpus&lt;/strong>, but design the lineage so that a future unified registry can absorb both without re-versioning everything. Concretely: use stable identifiers (&lt;code>dataset_id&lt;/code>, &lt;code>version&lt;/code>, &lt;code>commit_hash&lt;/code>) that can be propagated regardless of the tool.&lt;/p>
&lt;h2 id="the-operational-pattern-four-hop-lineage">The operational pattern: four-hop lineage&lt;/h2>
&lt;p>Once you accept that datasets have to be versioned, the question is not &amp;ldquo;which tool&amp;rdquo; but &amp;ldquo;which chain of identifiers connects production to the source data&amp;rdquo;. The pattern the market has consolidated on has four hops:&lt;/p>
&lt;pre tabindex="0">&lt;code>(dataset_id, dataset_version)
│ versioned in DVC or lakeFS
▼
(model_id, model_version)
│ registered in MLflow / W&amp;amp;B with the dataset as input
▼
(deployment_id, prompt_version)
│ registered in the model registry + prompt registry
▼
(trace_id)
│ emitted by the inference engine with OTel
▼
answer visible to the user
&lt;/code>&lt;/pre>&lt;p>Each arrow is a metadata write that crosses the boundary between two systems. If a single arrow is missing, the lineage breaks and the promise of auditability evaporates.&lt;/p>
&lt;p>A concrete example of the flow, using DVC + MLflow:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Data stage: version the dataset&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">dvc add data/finetune_v3.jsonl
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add data/finetune_v3.jsonl.dvc data/.gitignore
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;data: finetune dataset v3&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">dvc push &lt;span class="c1"># uploads the binary to the remote (MinIO/S3)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Tune stage: train while recording lineage&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mlflow run train.py &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -P &lt;span class="nv">dataset_id&lt;/span>&lt;span class="o">=&lt;/span>finetune &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -P &lt;span class="nv">dataset_version&lt;/span>&lt;span class="o">=&lt;/span>v3 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -P &lt;span class="nv">dataset_hash&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>dvc get-url data/finetune_v3.jsonl &lt;span class="p">|&lt;/span> sha256sum&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># The run records: input dataset + model output&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Eval stage: validate while recording lineage&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mlflow run eval.py &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -P &lt;span class="nv">model_id&lt;/span>&lt;span class="o">=&lt;/span>adapter_customer_v7 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -P &lt;span class="nv">golden_set_id&lt;/span>&lt;span class="o">=&lt;/span>customer_support &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -P &lt;span class="nv">golden_set_version&lt;/span>&lt;span class="o">=&lt;/span>v12
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Deploy stage: the deployment inherits the dataset + golden ids&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Every trace in Observe carries model_version + prompt_version&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># which rewind back to dataset_version&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The equivalent version with lakeFS over the RAG corpus:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Branch for the new corpus embeddings&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lakectl branch create lakefs://corpus/embed-2026q2 --source main
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Index the corpus on that branch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python index_corpus.py --branch embed-2026q2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Validate before merging to main&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python eval_retrieval.py --branch embed-2026q2 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --metric recall@10 --threshold 0.78
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># If it passes, merge (changes the corpus that serves production)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lakectl commit lakefs://corpus/embed-2026q2 -m &lt;span class="s2">&amp;#34;embed: corpus 2026q2&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">lakectl merge lakefs://corpus/embed-2026q2 lakefs://corpus/main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The virtue of the second flow: while the new corpus is being validated, the production system keeps serving from &lt;code>main&lt;/code> with no interference. The parallel branch works as a real staging area over the whole bucket.&lt;/p>
&lt;h2 id="schema-contracts-data-versioning-without-them-is-an-illusion">Schema contracts: data versioning without them is an illusion&lt;/h2>
&lt;p>Versioning a dataset&amp;rsquo;s content without versioning its &lt;strong>schema&lt;/strong> is a frequent mistake. The problem: a versioned dataset with an implicit schema still breaks silently when a producer (the ingestion team, the annotation team, an ad-hoc script) changes a field.&lt;/p>
&lt;p>A concrete case: a customer support golden eval set, 1000 examples, an &lt;code>expected_output&lt;/code> field originally &lt;code>string&lt;/code>. Someone decides they need to capture several valid answers and changes the field to &lt;code>list[string]&lt;/code>. The eval loader accepts both formats by chance (Python is lax) but the downstream LLM judge receives a different object. The eval keeps passing but now it measures something else.&lt;/p>
&lt;p>Productive pattern: the dataset is versioned with DVC/lakeFS and its schema is versioned with a &lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">Schema Registry&lt;/a> (Confluent or Apicurio) or, in less mature systems, with a JSON Schema embedded alongside the dataset. CI blocks any PR that breaks the contract without a version bump.&lt;/p>
&lt;p>A minimal schema for a golden eval entry (illustrative):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">$schema&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://json-schema.org/draft/2020-12/schema&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">$id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://example.org/schemas/golden_eval_entry/v3.json&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">object&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">required&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">example_id, input, expected_outputs, rubric, segment]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">properties&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">example_id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type: string, format&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">uuid}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">input&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">object&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">required&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">user_query, retrieved_context]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">properties&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">user_query&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">string}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retrieved_context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type: array, items&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">string}}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expected_outputs&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">array&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">minItems&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">items&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">string}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rubric&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">object&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">required&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">must_include, must_not_include, format]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">properties&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">must_include&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type: array, items&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">string}}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">must_not_include&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type: array, items&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">string}}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">format&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">enum&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">text, json, markdown]}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">segment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">string}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">difficulty&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">enum&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">easy, medium, hard]}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">added_at&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type: string, format&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">date-time}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">curated_by&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">string}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Operational rules:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Explicit forward/backward compatibility&lt;/strong>: adding an optional field is backward-compatible; removing a required one is breaking. The policy is enforced with a compatibility check in CI.&lt;/li>
&lt;li>&lt;strong>The schema version embedded&lt;/strong> in every row of the dataset (a &lt;code>_schema_version&lt;/code> field). The loader validates that the version matches what the consuming code expects.&lt;/li>
&lt;li>&lt;strong>The schema registry as the single source of truth&lt;/strong>, not as an optional copy of the JSON Schema in four repos.&lt;/li>
&lt;/ul>
&lt;p>Without this level of discipline, &amp;ldquo;we have data versioning&amp;rdquo; means &amp;ldquo;we store the bytes but we do not control what they mean&amp;rdquo;.&lt;/p>
&lt;h2 id="golden-eval-set-the-most-critical-version">Golden eval set: the most critical version&lt;/h2>
&lt;p>Of the four artefacts, the &lt;strong>golden eval set&lt;/strong> is the one that demands the most rigour. A failure here contaminates the whole promotion chain: if the eval lies, the gates approve models that should not pass.&lt;/p>
&lt;p>Three extra disciplines over the golden set:&lt;/p>
&lt;p>&lt;strong>Annotation with measured quality.&lt;/strong> Each example is labelled by a human, and a percentage (10-20 %) is annotated by two independent people. The &lt;strong>inter-annotator agreement&lt;/strong> (Cohen&amp;rsquo;s kappa or pairwise F1) is measured and published; a golden set with kappa &amp;lt; 0.7 is measuring human noise, not model behaviour. Argilla and Label Studio provide the mechanics; what matters is the discipline, not the tool.&lt;/p>
&lt;p>&lt;strong>A strict holdout against contamination.&lt;/strong> The golden set must &lt;strong>never&lt;/strong> enter the training set. The concrete mechanism: hash each golden set &lt;code>input&lt;/code> (sha256 normalised by lowercasing + stripping trivial punctuation) → check in CI against all the training set hashes. If there is an intersection, CI blocks until it is resolved. Without this check, the model passes the eval by memorisation, not by capability. The consequence in production is disastrous: the &amp;ldquo;validated&amp;rdquo; model fails on cases analogous to the golden set that it had not memorised.&lt;/p>
&lt;p>&lt;strong>Additive versioning, never destructive.&lt;/strong> When the golden set grows (each retrain cycle adds cases), &lt;code>golden_v3 = golden_v2 ∪ new_examples&lt;/code>. Never &lt;code>golden_v3 = a different new set&lt;/code>. Only that way can you compare two models trained months apart over the same base + the new delta. If you rewrite the golden set, you cannot tell whether the March model was worse than the May one or whether you were simply measuring different things.&lt;/p>
&lt;p>Summary table of the discipline per artefact:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Practice&lt;/th>
&lt;th>Training set&lt;/th>
&lt;th>RAG corpus&lt;/th>
&lt;th>Golden eval set&lt;/th>
&lt;th>Enriched retrain&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Immutable versioning&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes (snapshots)&lt;/td>
&lt;td>&lt;strong>Yes, critical&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Schema with a contract&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Recommended&lt;/td>
&lt;td>&lt;strong>Yes, critical&lt;/strong>&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Double annotation&lt;/td>
&lt;td>No&lt;/td>
&lt;td>Not applicable&lt;/td>
&lt;td>&lt;strong>Yes (10-20 %)&lt;/strong>&lt;/td>
&lt;td>Yes (10-20 %)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Holdout vs other datasets&lt;/td>
&lt;td>N/A&lt;/td>
&lt;td>N/A&lt;/td>
&lt;td>&lt;strong>Yes, hash check&lt;/strong>&lt;/td>
&lt;td>Yes (vs golden)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Drift check vs previous version&lt;/td>
&lt;td>Recommended&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Recommended&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Lineage to deployment&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="promotion-gates-the-dataset-is-promoted-like-the-model">Promotion gates: the dataset is promoted like the model&lt;/h2>
&lt;p>A candidate dataset (a freshly enriched &lt;code>golden_v13&lt;/code>, an &lt;code>enriched_retrain_2026_q2&lt;/code> resulting from the Retrain cycle) does not enter production just by being in the bucket. It goes through gates equivalent to the model&amp;rsquo;s or the prompt&amp;rsquo;s:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Schema validation&lt;/strong> — the contract is met. Blocked in CI if not.&lt;/li>
&lt;li>&lt;strong>Quality validation&lt;/strong> — a random 5-10 % sample reviewed by a human with a quality score ≥ 4/5. Blocked if the sample fails.&lt;/li>
&lt;li>&lt;strong>Holdout segregation check&lt;/strong> — for golden sets and enriched datasets, a hash check against all the other active datasets. Blocked if there is overlap.&lt;/li>
&lt;li>&lt;strong>Drift check vs previous version&lt;/strong> — a KS test over the distribution of the input embeddings, or simpler metrics (mean length, segment distribution, the ratio of each label). A warning if the drift is high with no documented cause; a block if it is very high.&lt;/li>
&lt;li>&lt;strong>Lineage check&lt;/strong> — the dataset explicitly declares which version it inherits from and what changed. Without that metadata, it does not go in.&lt;/li>
&lt;/ol>
&lt;p>Only when all five gates pass is the dataset tagged &lt;code>production-ready&lt;/code> and the downstream pipelines that depend on it unblocked (the next Tune, the next product release, the next eval cycle).&lt;/p>
&lt;h2 id="the-on-premise-stack-applied">The on-premise stack applied&lt;/h2>
&lt;p>On a generic infrastructure with an &lt;strong>RTX 4090&lt;/strong> (24 GB VRAM, a development / small-batch profile) and a &lt;strong>4×H100 SXM cluster&lt;/strong> (80 GB VRAM each, NVLink, training and production inference), data versioning fits without a dedicated GPU for the versioning itself, since versioning lives on CPU + storage, but it does touch the GPU for the drift checks that require embeddings.&lt;/p>
&lt;p>Typical topology:&lt;/p>
&lt;pre tabindex="0">&lt;code>┌────────────────────────────────────────────────────────────┐
│ Object store (MinIO or Ceph) │
│ buckets: /training-sets /corpus-rag │
│ /golden-evals /enriched-retrain │
└────────────────────────┬───────────────────────────────────┘
│
┌─────────────────┼──────────────────┐
│ │ │
┌───▼────┐ ┌────▼────┐ ┌────▼─────┐
│ DVC │ │ lakeFS │ │ MLflow │
│ remote │ │ branches│ │ Tracking │
└───┬────┘ └────┬────┘ └────┬─────┘
│ │ │
└─────────────────┴──────────────────┘
│
┌──────▼──────┐
│ CI/CD gates │
│ (Forgejo / │
│ GitLab) │
└──────┬──────┘
│
┌──────────┴───────────┐
│ │
┌─────▼──────┐ ┌─────▼─────┐
│ RTX 4090 │ │ 4×H100 │
│ (drift │ │ (training │
│ embeds, │ │ + │
│ validates)│ │ serving) │
└────────────┘ └───────────┘
&lt;/code>&lt;/pre>&lt;p>Operational notes:&lt;/p>
&lt;ul>
&lt;li>The &lt;strong>object store&lt;/strong> (MinIO or Ceph) serves both as the DVC remote and as lakeFS storage. One storage plane, two views.&lt;/li>
&lt;li>The &lt;strong>schema checks&lt;/strong> and &lt;strong>holdout hashing&lt;/strong> are fast CPU-bound tasks; the CI runner executes them without a GPU.&lt;/li>
&lt;li>The &lt;strong>embedding drift check&lt;/strong> needs an encoder; the RTX 4090 handles this without touching the production cluster. A small encoder (BGE-small, E5-small, ~100M parameters) processes 10⁴ examples in a few minutes.&lt;/li>
&lt;li>The &lt;strong>H100 cluster&lt;/strong> stays free for training and serving, with no contamination from versioning jobs.&lt;/li>
&lt;/ul>
&lt;h3 id="when-do-you-not-need-dvclakefs">When do you NOT need DVC/lakeFS?&lt;/h3>
&lt;p>There is an opposite position defended with figures in the &lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">continuous fine-tuning post&lt;/a>: for small systems with a single team, datasets &amp;lt; 1 GB and a handful of adapters, &lt;strong>Postgres + pgvector + an S3 bucket + hashed filenames&lt;/strong> are enough. The operational complexity of DVC/lakeFS does not pay for itself.&lt;/p>
&lt;p>The dividing line is reasonable:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>You do not need DVC/lakeFS&lt;/strong>: a single team, small datasets, few adapters, no multiple products sharing data.&lt;/li>
&lt;li>&lt;strong>You do need it&lt;/strong>: multiple teams, datasets &amp;gt; 10 GB, several products sharing a golden eval set, external compliance demanding batch traceability, or an institutionalised quarterly retrain cycle.&lt;/li>
&lt;/ul>
&lt;p>Adopting DVC + lakeFS before you need them is overhead. Adopting them six months late is losing six months of lineage irrecoverably.&lt;/p>
&lt;h2 id="seven-pitfalls-that-turn-data-versioning-into-theatre">Seven pitfalls that turn data versioning into theatre&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>Versioning the data but not the schemas.&lt;/strong> The content is versioned, the contract changes silently, the system breaks without the versioning capturing it. A Schema Registry is not optional; it is half the problem.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>The same S3 path overwritten.&lt;/strong> &amp;ldquo;Upload &lt;code>training.jsonl&lt;/code> to the bucket&amp;rdquo; and the next experiment rewrites the file. S3 versioning (if it is enabled) saves your skin, but without an immutable identifier propagated to MLflow you cannot rewind. The correct pattern: &lt;code>training_v3.jsonl&lt;/code> or &lt;code>training/2026q2/&amp;lt;sha&amp;gt;.jsonl&lt;/code>, never the same name.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>A golden eval set without a strict holdout.&lt;/strong> Without a hash check against training, the model memorises the eval and passes without having learned. It is the LLM equivalent of an exam the teacher announces in advance: everyone passes, nothing has been measured.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Not recording dataset → model lineage.&lt;/strong> When an incident requires knowing what data a certain model was trained on, the correct answer is a query to MLflow / W&amp;amp;B. If the answer is &amp;ldquo;let&amp;rsquo;s ask whoever trained it&amp;rdquo; (assuming they are still on the team), the lineage does not exist.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>DVC added six months late.&lt;/strong> Adopting versioning in month 1 = a nuisance. Adopting it in month 6 = an irrecoverable loss of six months of datasets that can no longer be reconstructed. The curse of &amp;ldquo;we&amp;rsquo;ll add it later&amp;rdquo;.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>lakeFS with branches that never get merged.&lt;/strong> Parallel branches over the corpus are useful for experimenting; kept indefinitely without merging, operations turn into a graveyard of half-updated branches. Explicit policy: merge or destroy within N weeks.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Schema validation only in production.&lt;/strong> The contract is validated when the dataset is already in production and the model already trained. By then, the incident has already happened. The validation has to be &lt;strong>in CI&lt;/strong>, before the merge, over the delta the PR introduces.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="a-datasets-cycle-on-one-screen">A dataset&amp;rsquo;s cycle on one screen&lt;/h2>
&lt;pre tabindex="0">&lt;code>┌─────────────────────────────────────────────────────────────┐
│ Producer (ingest / annotation / retrain loop) │
└────────────────┬────────────────────────────────────────────┘
│
▼ (commit a candidate version)
┌─────────────────────────┐
│ CI gates │
│ - Schema validation │
│ - Quality sampled │
│ - Holdout hash check │ ── fails → PR blocked
│ - Drift vs previous │
│ - Lineage declared │
└────────────┬────────────┘
│ passes
▼
┌─────────────────────────┐
│ DVC tag or lakeFS commit│
│ + MLflow registry │ ← immutable version
│ + Schema Registry │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ Downstream pipeline │
│ Tune / Eval / Deploy │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ Production trace │
│ → rewinds to dataset │
└─────────────────────────┘
&lt;/code>&lt;/pre>&lt;h2 id="what-we-have-not-covered">What we have not covered&lt;/h2>
&lt;p>At top level, this post leaves out:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Vector store versioning&lt;/strong> proper: an embedding index is not versioned like a raw dataset because it depends on the embedding model. Changing the embedder rewrites the whole index. It is a different animal and deserves separate treatment (recall, ANN parameters, index branching vs full re-embedding).&lt;/li>
&lt;li>&lt;strong>Standardised lineage tooling&lt;/strong> (OpenLineage, Marquez): how to emit and consume lineage events interoperably between systems.&lt;/li>
&lt;li>&lt;strong>Data quality frameworks&lt;/strong> (Great Expectations, Soda, Deequ): how to write suites of &amp;ldquo;expectations&amp;rdquo; over a dataset and enforce them on every version.&lt;/li>
&lt;li>&lt;strong>Privacy-preserving versioning&lt;/strong>: federated learning without centralising the dataset, differential privacy applied to the version that gets distributed.&lt;/li>
&lt;li>&lt;strong>Contamination between third-party golden sets&lt;/strong> (HumanEval, MMLU, etc.) and the training datasets of open models: the problem of &amp;ldquo;the model passes HumanEval because HumanEval is in its pretraining&amp;rdquo;.&lt;/li>
&lt;/ul>
&lt;p>Each one is worth a post of its own when the field justifies it.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage LLMOps pipeline&lt;/a> — the master map where this piece fits, the Data section.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain: closing the loop&lt;/a> — how the enriched dataset produced by Retrain returns to Data; this post details how to version it properly.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/prompt-versioning-contract-five-word-change/">Prompt versioning with Langfuse and MLflow&lt;/a> — the other cross-cutting piece of the lineage; the &lt;code>prompt_version&lt;/code> travels alongside the &lt;code>dataset_version&lt;/code> in every trace.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">Continuous fine-tuning in production&lt;/a> — argues for a minimalist stack (Postgres + pgvector + S3) without DVC/lakeFS for small systems; this post explains when you cross the line to the other side.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals for LLMs: the layer after tracing&lt;/a> — the main consumer of the golden eval set.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/mlops-for-llms-landscape-2026/">LLM-specific MLOps in 2026&lt;/a> — market context for the complete LLMOps stack.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/postgresql-qdrant-rag-ingestion-postman-syncing-two-worlds/">PostgreSQL + Qdrant for ingestion&lt;/a> — how the ingestion that precedes versioning is materialised.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/iso-42001-aims-on-premise-llm-platform/">ISO/IEC 42001: the AI system&amp;rsquo;s operations manual&lt;/a> — the versioning discipline described here materialises the five controls of Annex A.7 (data quality, acquisition, provenance, preparation) directly, with no additional work.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/eu-ai-act-technical-dossier-on-premise-llm-architecture/">EU AI Act: the technical file article by article&lt;/a> — Art. 10 (data and data governance) requires the four datasets versioned + bias analysis + lineage + representativeness. This post covers the technical mechanics; that one covers the legal obligations and the auditable checklist.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://dvc.org/doc">DVC documentation&lt;/a> — versioning workflows, pipelines and remotes.&lt;/li>
&lt;li>&lt;a href="https://docs.lakefs.io/">lakeFS documentation&lt;/a> — branching, merging and commits over the bucket.&lt;/li>
&lt;li>&lt;a href="https://medium.com/the-modern-scientist/reproducible-ai-versioning-models-prompts-and-data-96dd0337af65">lakeFS acquires DVC, November 2025&lt;/a> — announcement and combined roadmap.&lt;/li>
&lt;li>&lt;a href="https://docs.confluent.io/platform/current/schema-registry/index.html">Confluent Schema Registry&lt;/a> and &lt;a href="https://www.apicur.io/registry/">Apicurio&lt;/a> — schema contracts for streaming data.&lt;/li>
&lt;li>&lt;a href="https://openlineage.io/">OpenLineage&lt;/a> and &lt;a href="https://marquezproject.ai/">Marquez&lt;/a> — open standard for lineage events.&lt;/li>
&lt;li>&lt;a href="https://greatexpectations.io/">Great Expectations&lt;/a> — data quality expectations in CI.&lt;/li>
&lt;li>&lt;a href="https://mlflow.org/docs/latest/tracking.html">MLflow Tracking&lt;/a> — input datasets as first-class artefacts since MLflow 2.4.&lt;/li>
&lt;li>&lt;a href="https://www.pachyderm.com/">Pachyderm&lt;/a> and &lt;a href="https://quiltdata.com/">Quilt&lt;/a> — historical alternatives to DVC/lakeFS.&lt;/li>
&lt;li>On eval set contamination: &lt;em>&amp;ldquo;Stop Uploading Test Data in Plain Text&amp;rdquo;&lt;/em> (Magar &amp;amp; Schwartz, 2022) and later work on detecting contamination in pretraining corpora.&lt;/li>
&lt;/ul></description></item></channel></rss>