aiml
SearchMy learning
Menu

Lesson 6 of 36Module 3

Track passage sources

Bind an exact passage to its source path, byte hash and line span, then reject stale or altered records before displaying the text.

Prerequisites

The first two modules of Building Agentic RAG Applications; Python functions and JSON.

By the end, you can

  • Bind an exact passage to its source path, byte hash and line span, then reject stale or altered records before displaying the text.
  • Rebuild a source span before displaying its stored text.
  • Distinguish byte integrity from source authority and question relevance.
Runtime
Python 3.11+; local files only
Dependencies
Python standard library; no model or third-party packages

Lab code, tests and fixtures (ZIP, 1.1 MB)Quiz check your understanding

Progress is saved in this browser.

Bind an exact passage to its source path, byte hash and line span, then reject stale or altered records before displaying the text.

When you need this. A passage can retain a valid-looking source link after its text has changed. We will bind the displayed passage to exact local source bytes, rather than trusting the stored snippet alone.

The lab reuses the supplied original Python API summaries. It runs a local provenance check, with no retrieval, embeddings or generated answer. Review Build a RAG Eval Dataset for the separate question-to-passage relevance check.

Prerequisites. Use Python and the supplied local fixtures. The vector retrieval course remains the prerequisite for embedding and vector-search internals; this module does not repeat them.

Identity boundary. This ingestion lab starts a source-file and line-span contract rather than importing the earlier LangChain chunk IDs. Rebuild a downstream index from its versioned snapshot; do not attach old vectors or scores to a new ID. The later policy comparison binds old task targets only when source, version and exact text hashes identify one current passage. That binding transfers a label, not an old retrieval score.

Source file +version metadataRequestedline spanBind + verifysource bytesPassage +source referenceChanged sourceSourceErrormetadataline spanrecordsfailureSource file +version metadataRequestedline spanBind + verifysource bytesPassage +source referenceChanged sourceSourceErrormetadatainputrecordsfailure
The binder joins an exact line span to the source byte hash. Verification rereads those bytes and rebuilds the passage before returning its text. A changed source or altered passage is refused.

The names describe different layers of the same request:

Name Meaning in these labs
Document A source page or file, with its version and identity. The capstone’s documents are manifest-listed files.
Chunk or passage A searchable segment of a document. LangChain chunks, line-span passages and capstone paragraphs have different ID schemes.
Collection A group used for access policy. It is not a vector index or an answer context.
Index Search data derived from a corpus snapshot, such as an FTS table or stored vectors. Rebuilding it can change retrieval results.
Context The selected source text admitted into one model request after filtering and budget checks.

A source ID identifies the page; a passage ID identifies a segment within a particular snapshot. Keep those identifiers separate when binding a question’s expected source to a new index.

Name the file and exact line span

A source reference records where a passage came from. Here it contains a relative source path, source byte hash, inclusive one-based start and end lines, source ID, documentation version and URL. The passage also stores its exact text.

The binder reads UTF-8 without replacing undecodable bytes and preserves line endings in the selected span. A span outside the file or containing only whitespace raises SourceError. The fixture summary occupies one line; the tests also exercise multi-line spans.

Rebuild the passage before using it

The chunk ID hashes the path, source byte hash, line span and supplied source metadata. The verifier rereads the source and rebuilds the passage. It compares the complete record, including text and identifier, before returning the text.

Altered passage. The executed example changes the stored passage text while leaving the reference fields intact. Verification raises SourceError because the rebuilt record differs. The application must display the returned verified text rather than the untrusted stored snippet.

Changed source. Editing the file after binding also invalidates the old passage. Bind a new passage only after reviewing the source revision. A valid reference does not show that the passage answers a particular question.

Separate source integrity from source authority

The version and URL are supplied metadata. Binding them into an ID detects later changes to a stored record; it does not independently authenticate the URL or prove the claimed documentation version. An attacker able to replace both the source and all reference fields can build a self-consistent replacement.

These are span-reference IDs for this ingestion exercise. They deliberately do not claim compatibility with the earlier LangChain splitter’s chunk IDs. Keep the published eval targets unchanged. A production integration needs an explicit mapping and a reviewed corpus revision.

Run the local contract and inspect the result

Download the complete lab. The README, fixture metadata, implementation and tests travel together. No dependency installation or model server is required.

sources.pySource · pythonDownload
"""Bind passages to exact local source bytes. Python 3.11+, standard library only."""
from __future__ import annotations

from dataclasses import asdict, dataclass
import hashlib
import json
import os
import stat
from pathlib import Path, PurePosixPath


MAX_BYTES = 1_048_576


class SourceError(ValueError):
    """The requested source or passage cannot be verified."""


@dataclass(frozen=True)
class Passage:
    chunk_id: str
    source_path: str
    source_sha256: str
    start_line: int
    end_line: int
    text: str
    source_id: str = ""
    version: str = ""
    url: str = ""


def source_bytes(root: Path, source_path: str) -> bytes:
    """Allow relative POSIX paths that resolve inside the corpus root."""
    path = PurePosixPath(source_path)
    if not source_path or path.is_absolute() or ".." in path.parts or str(path) != source_path:
        raise SourceError("source path must be a normalized relative POSIX path")
    base = root.resolve()
    target = (base / source_path).resolve()
    if not target.is_relative_to(base):
        raise SourceError("source path escapes the corpus root")
    try:
        if not stat.S_ISREG(target.stat().st_mode):
            raise SourceError("source must be a regular file")
        with target.open("rb") as stream:
            if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
                raise SourceError("source must be a regular file")
            raw = stream.read(MAX_BYTES + 1)
        if len(raw) > MAX_BYTES:
            raise SourceError("source exceeds MAX_BYTES")
        return raw
    except OSError as error:
        raise SourceError("source file cannot be read") from error


def bind_passage(root: Path, source_path: str, start_line: int, end_line: int,
                 *, source_id: str = "", version: str = "", url: str = "") -> Passage:
    raw = source_bytes(root, source_path)
    try:
        lines = raw.decode("utf-8").splitlines(keepends=True)
    except UnicodeDecodeError as error:
        raise SourceError("source must be UTF-8") from error
    if (type(start_line) is not int or type(end_line) is not int
            or not 1 <= start_line <= end_line <= len(lines)):
        raise SourceError("line span must be 1-based, inclusive and inside the source")
    text = "".join(lines[start_line - 1:end_line])
    if not text.strip():
        raise SourceError("passage must contain non-whitespace text")
    digest = hashlib.sha256(raw).hexdigest()
    identity = json.dumps([source_path, digest, start_line, end_line, source_id, version, url], separators=(",", ":"))
    chunk_id = hashlib.sha256(identity.encode("utf-8")).hexdigest()
    return Passage(chunk_id, source_path, digest, start_line, end_line, text, source_id, version, url)


def verify_passage(root: Path, passage: Passage) -> str:
    """Reject stale or altered metadata/text, then return the verified passage."""
    current = bind_passage(root, passage.source_path, passage.start_line, passage.end_line,
                           source_id=passage.source_id, version=passage.version, url=passage.url)
    if current != passage:
        raise SourceError("passage does not match the current source snapshot")
    return current.text


def demo() -> dict:
    root = Path(__file__).parent / "fixtures"
    catalog = json.loads((root / "sources.json").read_text(encoding="utf-8"))
    source = catalog[0]
    passage = bind_passage(root, "docs/" + source["id"] + ".md", 1, 1,
                           source_id=source["id"], version=source["version"], url=source["url"])
    verified = verify_passage(root, passage)
    from dataclasses import replace
    try:
        verify_passage(root, replace(passage, text="Run unreviewed code.\n"))
    except SourceError as error:
        rejection = str(error)
    return {
        "execution_mode": "offline-source-verification",
        "model_called": False,
        "fixture_kind": "supplied original Python API summaries",
        "passage": asdict(passage),
        "verified_text": verified,
        "tampered_text_rejection": rejection,
    }


if __name__ == "__main__":
    print(json.dumps(demo(), indent=2, ensure_ascii=False))
Execute the local labTerminal · bash
python3 sources.py
python3 -m unittest -v test_sources.py
Saved CLI outputRecorded output · jsonDownload
{
  "execution_mode": "offline-source-verification",
  "model_called": false,
  "fixture_kind": "supplied original Python API summaries",
  "passage": {
    "chunk_id": "dc15f2a3d615086bc11ba6695d673d064b6385a6277ae93079e9c7901144950e",
    "source_path": "docs/path-write.md",
    "source_sha256": "a48b4b9f763cd6309ebe32af19c5a814adcf810c2259683da9f328ccee46948e",
    "start_line": 1,
    "end_line": 1,
    "text": "Path.write_text(data, encoding=None, errors=None, newline=None) writes text to a file and closes it. Set encoding=\"utf-8\" to select UTF-8. An existing file is overwritten. The method returns the number of characters written.",
    "source_id": "path-write",
    "version": "3.12",
    "url": "https://docs.python.org/3.12/library/pathlib.html#pathlib.Path.write_text"
  },
  "verified_text": "Path.write_text(data, encoding=None, errors=None, newline=None) writes text to a file and closes it. Set encoding=\"utf-8\" to select UTF-8. An existing file is overwritten. The method returns the number of characters written.",
  "tampered_text_rejection": "passage does not match the current source snapshot"
}

Inspect the loaded records and any rejected inputs in the saved result.

Inspect the complete saved result
Actual local ingestion resultRecorded output · jsonDownload
{
  "execution_mode": "offline-source-verification",
  "model_called": false,
  "fixture_kind": "supplied original Python API summaries",
  "passage": {
    "chunk_id": "dc15f2a3d615086bc11ba6695d673d064b6385a6277ae93079e9c7901144950e",
    "source_path": "docs/path-write.md",
    "source_sha256": "a48b4b9f763cd6309ebe32af19c5a814adcf810c2259683da9f328ccee46948e",
    "start_line": 1,
    "end_line": 1,
    "text": "Path.write_text(data, encoding=None, errors=None, newline=None) writes text to a file and closes it. Set encoding=\"utf-8\" to select UTF-8. An existing file is overwritten. The method returns the number of characters written.",
    "source_id": "path-write",
    "version": "3.12",
    "url": "https://docs.python.org/3.12/library/pathlib.html#pathlib.Path.write_text"
  },
  "verified_text": "Path.write_text(data, encoding=None, errors=None, newline=None) writes text to a file and closes it. Set encoding=\"utf-8\" to select UTF-8. An existing file is overwritten. The method returns the number of characters written.",
  "tampered_text_rejection": "passage does not match the current source snapshot"
}

Change one input and compare the failure

In a scratch copy, bind a passage and then edit the source file. Keep the old passage record. Call verify_passage again and compare its SourceError with the successful text returned before the edit. Next change only the stored text: it should also be refused.

The verifier caps each source read and has no persistence, concurrent-writer protection or remote source authentication. It is a small local contract example; large-corpus throughput remains unmeasured.

A passage reference matches the local file. What does that tell you?

Choose one answer, then check your reasoning.

Read the explanation

File integrity, source authority and passage relevance are separate checks.

What’s next

Continue with Handle Ingestion Failures.

References

Implementation APIs were checked on 23 October 2026 against official Python pathlib, hashlib and JSON documentation. The source summaries retain their version-specific official links and existing fact-status metadata. Python APIs use the standard library; no framework SDK is installed by this lab.

Written by Arjun Prabhulal.

Module 3

Document ingestion