Give a retrieval controller two distinct operations: search for candidate passages, then read a selected passage. A search rank helps choose what to inspect. The read result supplies the actual text and provenance needed to inspect a claim.
When you need this. Use this boundary when a caller should choose among candidates before receiving source bodies, and every read must refer to a passage returned by a matching search.
The lab uses the existing versioned developer-document summaries and SQLite lexical index. It runs those local tools directly. No model chooses a tool, generates a response or receives these passages.
Separate candidate selection from source reading
A passage is one versioned paragraph in the supplied snapshot. The snapshot contains original teaching summaries with primary-document links; it is not a fresh crawl. The loader checks its pinned file hash and the inherited snapshot validation rules before constructing the index.
search accepts a bounded alphanumeric query term, a requested documentation version and a result limit. This deliberately small interface is not a natural-language query parser. The backend ranks lexical matches, then exact-version eligibility is applied before the caller’s result limit. Fetching every candidate is practical only because this fixture caps corpus size.
Search returns handles: structured metadata containing the corpus identity, search-request identity, passage identity, version, revision, span, text hash, planned text bytes and rank. It returns neither source text nor source URL. A handle names a candidate; it does not assert that its content answers the question.
Read only a handle issued by this tool instance
read resolves a selected handle against the exact search result retained by the current instance. The result includes the authoritative paragraph, URL and a result hash. The line span and hashes connect the returned text to the pinned snapshot.
Changed handle. Editing a handle’s version, passage identity, score or other bound field causes refusal. A handle from an earlier corpus or an unissued search cannot select a different passage. These consistency checks are local application rules, not permissions, publisher authentication or protection from code that can modify the process.
Changed result. validate_read compares the complete result with the authoritative payload for that handle. A plausible URL or matching string length cannot substitute for that comparison. The controller must call this check before it accepts a read observation.
Inspect empty and ineligible searches
No lexical match returns an empty candidate list. The same happens when matches exist but none has the requested version. The caller can distinguish those searches from a read refusal by retaining the query, version and actual result.
| Operation | Observed result |
|---|---|
| Matched search | 1 issued handles |
| Resolved reads | path-write |
| No lexical match | 0 candidates |
| Requested version absent | 0 candidates |
| Edited handle | invalid_read_handle |
An empty result means this search returned no eligible passages; relevant documents may exist elsewhere. A validated read result matches the supplied snapshot but does not guarantee that a future answer will be correct.
Execute both tool operations
Download the complete search/read lab. It includes the pinned corpus, reused lexical index and version checks, tool implementation, failure tests and execution record. Python needs SQLite with FTS5 enabled; no provider SDK or downloaded model is required.
"""Bounded documentation search handles and receipt-bound passage reads."""
import copy
import hashlib
import json
from pathlib import Path
import re
from bm25 import LexicalIndex
from version_documents import validate_snapshot,canonical_hash
SNAPSHOT_SHA256='56bbeff06b0349186c3e5999d2b4fe634c6355f3bc267d6e07c47bf2d5e32b19'
class ToolError(ValueError):pass
def load_snapshot(root=None):
path=(root or Path(__file__).parent/'fixtures')/'snapshot.json'
raw=path.read_bytes()
if hashlib.sha256(raw).hexdigest()!=SNAPSHOT_SHA256:raise ToolError('snapshot_file_hash_mismatch')
snapshot=json.loads(raw);validate_snapshot(snapshot);return snapshot
class DocumentationTools:
def __init__(self,snapshot):
validate_snapshot(snapshot)
self.snapshot=copy.deepcopy(snapshot);self.corpus_id=snapshot['snapshot_id'];self.rows={};self.receipts={};self.closed=False
for source in self.snapshot['documents']:
for chunk in source['chunks']:
self.rows[chunk['chunk_id']]={'passage_id':chunk['chunk_id'],'source_id':source['source_id'],'version':source['version'],'document_revision':source['document_revision'],'source_sha256':source['sha256'],'start_line':chunk['start_line'],'end_line':chunk['end_line'],'text_sha256':chunk['text_sha256'],'byte_count':len(chunk['text'].encode('utf-8')),'text':chunk['text'],'url':source['url']}
if not self.rows or len(self.rows)>100:raise ToolError('unsupported_corpus_size')
self.index=LexicalIndex(self.snapshot)
def close(self):
if not self.closed:self.index.close();self.closed=True
def available(self):
if self.closed:raise ToolError('tools_closed')
def search(self,query,k=2,version='3.12'):
self.available()
if type(k) is not int or not 1<=k<=10:raise ToolError('invalid_k')
if not isinstance(query,str) or re.fullmatch(r'[A-Za-z0-9]{1,64}',query) is None:raise ToolError('invalid_query')
if not isinstance(version,str) or re.fullmatch(r'[0-9]+\.[0-9]+',version) is None:raise ToolError('invalid_version')
query=query.lower()
request={'corpus_id':self.corpus_id,'query':query,'version':version,'k':k}
request_id=canonical_hash(request)
# Corpus is capped at 100. Fetch all native ranks, then version-filter
# before applying the caller's k, without losing eligible candidates.
ranked=self.index.search(query,limit=100)['results']
eligible=[row for row in ranked if row['version']==version][:k]
handles=[]
for rank,row in enumerate(eligible,1):
source=self.rows[row['chunk_id']]
handle={'corpus_id':self.corpus_id,'search_request_id':request_id,**{key:value for key,value in source.items() if key not in ('text','url')},'native_score':row['native_score'],'rank':rank}
handle['read_token']=canonical_hash(handle);handles.append(handle)
result={'corpus_id':self.corpus_id,'search_request_id':request_id,'query':query,'version':version,'k':k,'results':handles}
self.receipts[request_id]=copy.deepcopy(result)
return copy.deepcopy(result)
def validate_handle(self,handle):
self.available()
if not isinstance(handle,dict):raise ToolError('invalid_read_handle')
if handle.get('corpus_id')!=self.corpus_id:raise ToolError('stale_corpus')
passage=handle.get('passage_id')
if not isinstance(passage,str) or passage not in self.rows:raise ToolError('unknown_passage')
request=handle.get('search_request_id')
if not isinstance(request,str) or request not in self.receipts:raise ToolError('unissued_search_receipt')
expected=next((row for row in self.receipts[request]['results'] if row['passage_id']==passage),None)
try:equal=expected is not None and canonical_hash(handle)==canonical_hash(expected)
except (ValueError,TypeError,OverflowError):equal=False
if not equal:raise ToolError('invalid_read_handle')
return copy.deepcopy(expected)
def read_cost(self,handle):return self.validate_handle(handle)['byte_count']
def _read_payload(self,handle):
handle=self.validate_handle(handle);source=self.rows[handle['passage_id']]
body={**handle,'text':source['text'],'url':source['url'],'read_request_id':canonical_hash({'search_request_id':handle['search_request_id'],'read_token':handle['read_token']})}
return {**body,'result_sha256':canonical_hash(body)}
def read(self,handle):return self._read_payload(handle)
def validate_read(self,result,handle):
expected=self._read_payload(handle)
try:equal=canonical_hash(result)==canonical_hash(expected)
except (ValueError,TypeError,OverflowError):equal=False
if not equal:raise ToolError('invalid_read_result')
return copy.deepcopy(expected)python run.py --output scratch-result.json
python -m unittest discover -v{
"changed_handle_refusal": "invalid_read_handle",
"empty_eligible_count": 0,
"execution_mode": "offline-native-document-search-and-read",
"no_match_count": 0,
"read_source_ids": [
"path-write"
],
"search_results": 1
}The deployed unit here is a local CLI. To use these methods across a service boundary, add request decoding, authorization and transport checks separately. The in-memory search-result table is scoped to this object; durable replay and sharing results across processes were not implemented. Corpus-size caps and result limits are guards, not throughput measurements.
View the complete local execution JSON
{
"execution_mode": "offline-native-document-search-and-read",
"model_called": false,
"corpus_id": "0615d22a3315e97c0a88c6c566c546877304f5a28a97e83f68dd36881d4bbd0e",
"search": {
"corpus_id": "0615d22a3315e97c0a88c6c566c546877304f5a28a97e83f68dd36881d4bbd0e",
"search_request_id": "aeffc68efdee46b8d23c597f651143424c575c0c1a4699ab08a48734d011f127",
"query": "overwritten",
"version": "3.12",
"k": 2,
"results": [
{
"corpus_id": "0615d22a3315e97c0a88c6c566c546877304f5a28a97e83f68dd36881d4bbd0e",
"search_request_id": "aeffc68efdee46b8d23c597f651143424c575c0c1a4699ab08a48734d011f127",
"passage_id": "c66643a4609c6ee157458bf4353f7446b9c6a200a3ef453846d6a18d5b5069a7",
"source_id": "path-write",
"version": "3.12",
"document_revision": "28c6349eaf80b629322edb310bb871dc45a04af9fad3b752bc75d12f0d7314e7",
"source_sha256": "a48b4b9f763cd6309ebe32af19c5a814adcf810c2259683da9f328ccee46948e",
"start_line": 1,
"end_line": 1,
"text_sha256": "a48b4b9f763cd6309ebe32af19c5a814adcf810c2259683da9f328ccee46948e",
"byte_count": 224,
"native_score": -0.7992166451067193,
"rank": 1,
"read_token": "08b866d8cf00533f3484aac4d78ac512bd4df8767c9ea11fbd2e817eeeb1dd75"
}
]
},
"reads": [
{
"corpus_id": "0615d22a3315e97c0a88c6c566c546877304f5a28a97e83f68dd36881d4bbd0e",
"search_request_id": "aeffc68efdee46b8d23c597f651143424c575c0c1a4699ab08a48734d011f127",
"passage_id": "c66643a4609c6ee157458bf4353f7446b9c6a200a3ef453846d6a18d5b5069a7",
"source_id": "path-write",
"version": "3.12",
"document_revision": "28c6349eaf80b629322edb310bb871dc45a04af9fad3b752bc75d12f0d7314e7",
"source_sha256": "a48b4b9f763cd6309ebe32af19c5a814adcf810c2259683da9f328ccee46948e",
"start_line": 1,
"end_line": 1,
"text_sha256": "a48b4b9f763cd6309ebe32af19c5a814adcf810c2259683da9f328ccee46948e",
"byte_count": 224,
"native_score": -0.7992166451067193,
"rank": 1,
"read_token": "08b866d8cf00533f3484aac4d78ac512bd4df8767c9ea11fbd2e817eeeb1dd75",
"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.",
"url": "https://docs.python.org/3.12/library/pathlib.html#pathlib.Path.write_text",
"read_request_id": "a9ec642e4ed3206ac85e5dd03d4de9ff57de3167e134079eafe4a31de23cec67",
"result_sha256": "662c96d3b972224ac1549f9a6c3ad495b3ed885b6db477d52aeb7527ec1dbbb7"
}
],
"no_match": {
"corpus_id": "0615d22a3315e97c0a88c6c566c546877304f5a28a97e83f68dd36881d4bbd0e",
"search_request_id": "f76beff608c3023ea069dea052656317ac6e5fbf4654fa39daeaaeed927c000f",
"query": "xyznotindexed",
"version": "3.12",
"k": 2,
"results": []
},
"empty_eligible_version": {
"corpus_id": "0615d22a3315e97c0a88c6c566c546877304f5a28a97e83f68dd36881d4bbd0e",
"search_request_id": "98f1cba586c00f8d9a3de99a7735dd9e5e453c850a0cff0ef67d2e2589eac672",
"query": "overwritten",
"version": "3.13",
"k": 2,
"results": []
},
"changed_handle_refusal": "invalid_read_handle",
"interpretation": "Tool contract checks and native retrieval only; no model selection, answer generation or permission system."
}The printed summary contains one issued result, the path-write read and the invalid-handle refusal; both empty searches return zero candidates. These are local tool-contract results.
Follow a handle back to its paragraph
Run the program and choose a returned handle. Compare its passage identity, version, span and text hash with the read result. Change its version and run the failure test. Explain why search metadata can select a passage but cannot stand in for reading its text.
Read the explanation
Handle consistency does not provide authorization or prove that a source supports every answer claim.
What’s next
Continue with Refine Queries and Search Again.
References
SQLite FTS5, checked on 23 October 2026, documents the inherited native lexical ranking. The Python copy documentation explains the defensive copying used for returned dictionaries. Search-result validation and version eligibility are application policies defined by the supplied code. The earlier RAG baseline supplies the original developer-document task context.