ir¶
ir — an information-retrieval substrate for agentic systems.
One uniform “find the relevant things in this corpus” contract that scales from an ad-hoc search over an ephemeral list to a maintained search engine. Retrieval is the core; selection/expansion/reranking/generation are layered on top.
Quick start:
import ir
# Define a corpus source (abstract strategy + parameters, smart defaults):
source = ir.CorpusSource.from_md_reports() # project docs/ reports
corpus = ir.build(source) # index (incremental)
hits = ir.search(corpus, "how do I deploy the app") # ranked SearchHits
# Light, dependency-free embedding for fast tests:
corpus = ir.build(source, embedder="light")
A corpus source is defined by a scope (what is in the corpus), a
change_signal (what counts as stale), an indexing_strategy (how a raw
item becomes filter fields + embeddable surfaces), and an embedder. The
default embedder is a decent local model (all-MiniLM-L6-v2); "light"
selects a numpy-only hashing embedder. Data persists under XDG dirs through a
dol repository layer.
ir is a retrieval substrate, not a search agent — it has no agent of its
own. ir.discover() is a deterministic single-shot retrieve → select →
disclose pipeline: the query is embedded verbatim (no LLM query rewriting) and
committed by a score rule (no LLM reviewing results), with no planner and no
back-edge (evaluate → reformulate → search again). The agent layer — planner,
LLM query formulator, and the evaluator→reformulate loop — lives in raglab
(https://github.com/thorwhalen/raglab), which is built on top of ir:
raglab imports ir; ir never imports raglab. To drive an agentic
search from Claude Code instead of a coded agent, see the ir-search skill and
the reviewing search subagent shipped under .claude/.
Functions
|
The reranker |
|
The key an abstention floor is stored and loaded under. |
|
Search a named |
|
Return a corpus-bound |
|
Wrap strategy to add one LLM-derived |
|
A |
|
Ready-made gate: synthesize only for packages with an empty / short description. |
|
An LLM-backed |
|
Build or incrementally update source into a |
|
Reopen a previously built corpus by name. |
|
Diff the reports docs tree on disk against the built |
|
Search a |
|
Bind ONE corpus to the uniform |
|
A lazy |
|
A |
|
Merge per-source ranked hit lists into one ranking — by rank, not score. |
|
A |
|
All stored records of artifact_id, ordered by |
|
Edges latent in the standard filter fields: |
|
Canonicalize a neighbor target to a |
|
Walk store from query under policy, returning the top-k hits. |
|
The pure-vector summary-routing / collapsed-tree |
|
A |
|
Expand hit into a |
±*k* same-kind neighbors around the seed (NEXT/PREV expansion). |
|
The whole artifact (small-to-big): every stored surface, plan order. |
|
|
Stamp source on every hit that doesn't already carry one. |
|
An LLM-backed |
|
Commit to a subset of ranked |
|
Reveal the payload of each selected hit at |
|
Find and commit to the capabilities for |
|
Register (or overwrite) a named corpus definition. |
|
All registered corpus definitions, keyed by name. |
|
Build (or update) a registered/preset corpus by name; returns a |
|
Run due background work for one corpus (name) or every registered one (all). |
|
Do the due background work for one corpus (idempotent). |
|
The effective policy for a registry |
|
The smart-default |
|
The effective |
Classes
|
A logical corpus item before decomposition into surfaces. |
|
One embeddable unit derived from an artifact. |
|
A stored, embedded surface — one row of the index. |
|
A scored record returned by retrieval (higher score = closer). |
|
An |
|
Decompose one artifact into filter fields + embeddable surfaces. |
|
One surface = the entire text. |
|
Split the artifact's text into overlapping chunk surfaces. |
|
Capability strategy: embed |
|
Package strategy: |
|
Index a Claude Code session turn-pair: user prompt + assistant summary. |
|
A corpus definition: scope + change signal + strategy + embedder. |
|
Repository bundling the meta/vectors/ledger/config views of one corpus. |
|
A built, queryable corpus: a store plus its embedder. |
|
Disk-vs-index coverage for a filesystem-backed corpus. |
|
Structural contract a traversal operator binds to — node + neighbors. |
|
A |
|
The pluggable strategy of a walk — graph semantics, not safety. |
|
The operator-owned state of one |
|
An expanded hit: the seed's identity + the stitched neighborhood text. |
|
A selector's commitment: the chosen subset of a ranked candidate list. |
|
The progressively-disclosed payload for one selected artifact. |
|
The result of |
|
What |
|
The background-work policy for one corpus (reindex + synopsis). |
|
When to (incrementally) rebuild a corpus. |
|
Whether/when to attach (expensive, LLM-generated) synopses. |
- class ir.Artifact(id, raw, metadata=<factory>)[source]¶
Bases:
objectA logical corpus item before decomposition into surfaces.
- class ir.Chunked(*, chunk_size=1200, overlap=200, text_key=None, kind='chunk', max_tokens=None, token_model=None)[source]¶
Bases:
objectSplit the artifact’s text into overlapping chunk surfaces.
By default chunks are packed to ~``chunk_size`` characters. Set
max_tokensto chunk by tokens instead — each chunk is bounded tomax_tokenstokens oftoken_model(default: the default embedding model), so no chunk overflows the embedder’s sequence limit and is silently truncated. Token mode degrades to char mode with a warning if the tokenizer is unavailable. The charchunk_size/overlapstill set the overlap ratio used in token mode.
- class ir.ClaudeTurn(*, include_full=False)[source]¶
Bases:
objectIndex a Claude Code session turn-pair: user prompt + assistant summary.
Two surfaces by default —
user_prompt(what the human asked) andassistant_summary(the assistant’s final end-of-turn text, the highest-signal “here’s what I did”; the deliberation before it is mostly noise) — so a query can target either side viasurfaces={"user_prompt"}/{"assistant_summary"}. Withinclude_full=Truea thirdassistant_fullsurface (all the turn’s assistant natural-language text) trades noise for recall — off by default. Session / project / time / model / tool-use become hard-filter fields.The raw artifact is a turn-pair record (see
priv.claude_transcripts.turn_pair_records()): a mapping withuser_prompt/assistant_summary/assistant_fullplus metadata.
- class ir.Corpus(name, store, embedder, embedder_id)[source]¶
Bases:
objectA built, queryable corpus: a store plus its embedder.
- search(query, **kwargs)[source]¶
Search this corpus for query.
**kwargs(k/mode/filter/surfaces/per_artifact/ …) are forwarded toir.retrieve.search().
- class ir.CorpusGraph(store_or_corpus)[source]¶
Bases:
objectA
GraphStoreover one corpus — artifact nodes,linksedges.node_idis anartifact_id.graph[aid]is the artifact’s stored records (its scorable surfaces, in plan order);graph.neighbors(aid, edge_type=...)reads the corpus store’slinksview. Single-corpus, so it resolves intra-corpus targets; cross-corpus[source, artifact_id]targets are returned byneighbors()verbatim but__getitem__only dereferences ids in this corpus (federated traversal is a follow-up).- fan_in(node_id, *, edge_type='REF')[source]¶
How many artifacts point at node_id by edge_type (its in-degree).
With
REFdependency edges this is the number of dependents, a PageRank-style centrality prior: a widely depended-on library is central to the corpus.- Return type:
- max_fan_in(*, edge_type='REF')[source]¶
The largest
fan_in()over all targets (0without edges).- Return type:
- neighbors(node_id, *, edge_type=None)[source]¶
Outgoing neighbor ids of node_id, optionally of one edge_type.
Returns target ids in stored form (a bare
artifact_id, whose source is this graph’ssource; or a[source, artifact_id]list for a cross-corpus edge), de-duplicated with first-seen order preserved. An artifact with no edges — or a store without a links view — yields[]. Pass a canonical(source, artifact_id)tocanonical_node_id()for a traversal’s visited-set.node_id is an intra-corpus
artifact_id(astr); a cross-corpus target fetched from another graph is out of contract here (it has no edges in this corpus).- Return type:
- reverse_neighbors(node_id, *, edge_type='REF')[source]¶
The artifact ids whose edge_type edges point at node_id.
The reverse of
neighbors(): with the defaultREFedges ofdefault_edge_extractor()(package -> its deps), this lists a library’s dependents. node_id may name an artifact of this corpus or a target outside it (a third-party dependency). ForREFedges, string ids match throughnormalize_dist_name()(case, and-/_/., are ignored), so"scikit_learn"finds the dependents of"scikit-learn"; other edge types match ids exactly. A cross-corpus target is a(source, artifact_id)pair, matched exactly. Order is first-seen over the links view, de-duplicated.
- source¶
The corpus name, when known — the
sourcehalf of this graph’s node identities (Nonefor a bare store).
- class ir.CorpusSource(name, scope, indexing_strategy=<factory>, change_signal=<function content_hash_signal>, embedder='default', metadata_of=None)[source]¶
Bases:
objectA corpus definition: scope + change signal + strategy + embedder.
- classmethod from_claude_sessions(*, name='sessions', since=90, projects=None, include_full=False, include_session_title=True, max_sessions=None, root=None, fetcher=None, strategy=None, **kwargs)[source]¶
The user’s Claude Code session transcripts as a corpus (turn pairs).
Each artifact is one user→assistant turn pair; the default
ClaudeTurnstrategy indexes the user prompt and the assistant’s end-of-turn summary as separate surfaces (target either withsurfaces={"user_prompt"}/{"assistant_summary"}).include_fulladds the full assistant text surface (off by default — the summary is the signal).include_session_title(default on) also indexes one record per session whose surface is the session’s persisted custom/AI title — a cheap “what was this session about” surface. Scope defaults to the lastsincedays (a full-history build is heavy); narrow withprojects(a cwd substring or list) andmax_sessions.fetcheroverrides the record source (each a mapping withuser_prompt/assistant_summary/ … ) — a callable (inject a test double to avoid theprivdependency) or a"module:attr"reference (resolve_fetcher()), the form a registry entry can carry, so another package can own the transcripts this corpus indexes. Otherwise records come frompriv.claude_transcripts.turn_pair_records().- Return type:
- classmethod from_files(root, *, name=None, pattern='.*\\\\.md$', exclude=None, strategy=None, **kwargs)[source]¶
A directory tree of text files as a corpus (lazy
dolscope).- Return type:
- classmethod from_mapping(mapping, *, name, strategy=None, **kwargs)[source]¶
Any mapping
{id -> raw}(dict,dolstore) as a corpus.- Return type:
- classmethod from_mcp_tools(tools, *, name='tools', strategy=None, **kwargs)[source]¶
A catalogue of MCP tools as a corpus, for tool search.
Each tool is indexed on
name + descriptiononly, through theSkillstrategy: its parameter schema never enters the embedded text (ir_02 §4, “the body carries no retrieval responsibility”). The schema still drives invalidation, throughmcp_tool_signal(), so a changed schema re-embeds the tool. Its top-level argument names ride along as theparam_namesmetadata of every hit, so a caller can see what a tool takes without loading it.toolsis an iterable of tool records, a{name: tool}mapping (the key names a record that has noname; a record’s own name wins), or a fetcher (a zero-argument callable or a"module:attr"reference, seeresolve_fetcher()). A record is duck-typed (seemcp_tool_record()), soirtakes no MCP dependency; an adapter for a specific client lives with the consumer. Tool names are the artifact ids and must be unique: namespace them ("server.tool") before merging several servers’ catalogues.- Return type:
>>> src = CorpusSource.from_mcp_tools( ... [{"name": "ping", "description": "Check the server is up"}] ... ) >>> plan = src.indexing_strategy.decompose("ping", src.scope["ping"]) >>> print(plan.surfaces[0].text) ping Check the server is up
- classmethod from_md_reports(*, name='reports', projects_root=None, strategy=None, recursive=True, exclude_dirs=None, **kwargs)[source]¶
Markdown reports under projects’
docs/andmisc/docs/trees.Walks each
*/*/docsand*/*/misc/docsfolder, and those of pocket repos (i/_zodals/<repo>/docs, seeiter_pocket_repos()), recursively (recursive=True, default) so reports nested one or more levels deep —docs/research/…,docs/decisions/…,docs/adr/…— are indexed, not just files sitting directly in the folder. Passrecursive=Falsefor the old shallow (top-level-only) behavior.Excludes ALL-CAPS filenames (README/CLAUDE/MEMORY/SKILL…) and any file under a vendored/hidden directory (
exclude_dirs, defaultDFLT_EXCLUDE_DIRS; a dotted directory is always skipped). Each record is a project-tagged document; ids are paths relative to the projects root.- Return type:
- classmethod from_packages(*, name='packages', manifest=None, readme_chars=20000, strategy=None, **kwargs)[source]¶
The local package ecosystem, scanned from the
.pthmanifest.- Return type:
- classmethod from_records(*, name, fetcher, strategy=None, metadata_keys=None, id_key='id', text_key='text', **kwargs)[source]¶
Records produced by a named callable as a corpus.
The generic, persistable counterpart of
from_mapping(): wherefrom_mappingtakes an in-process object (so a registry entry cannot name it), this takes afetcher— a callable or a"module:attr"import reference (seeresolve_fetcher()) — so a corpus owned by another package round-trips through~/.config/ir/corpora.jsonand rebuilds in a fresh process.Each record is a mapping; its
id_keyis the artifact id (falling back to<name>_<i>so no record is silently dropped).metadata_keysnames the record fields lifted into the strategy’s filter fields — the JSON-friendly stand-in for ametadata_ofcallable, which no registry entry could carry. The default strategy chunkstext_key.- Return type:
>>> src = CorpusSource.from_records( ... name="notes", ... fetcher=lambda: [{"id": "a", "text": "hi", "project": "p"}], ... metadata_keys=["project"], ... ) >>> src.scope["a"]["text"], src.metadata_of("a", src.scope["a"]) ('hi', {'project': 'p'})
- classmethod from_skills(*, name='skills', filter=None, fetcher=None, strategy=None, **kwargs)[source]¶
The agent-skills corpus, via
priv.skills_index.fetcheroverrides the source of skill records (each a mapping withname/description/parent) — a callable (inject a test double to avoid theprivdependency) or a"module:attr"reference (resolve_fetcher()), which is the form a registry entry can carry.- Return type:
- class ir.CorpusStore(meta, vectors, ledger, config, calibration=None, links=None, *, packed_dir=None)[source]¶
Bases:
objectRepository bundling the meta/vectors/ledger/config views of one corpus.
- delete_links(artifact_id)[source]¶
Remove an artifact’s edges; a missing entry is tolerated.
- Return type:
- delete_record(record_id)[source]¶
Remove a record’s metadata + vector; a missing id is tolerated.
The meta goes first, so the id stops being listed before its vector disappears. Each removal tolerates the file being gone already (another process may be deleting the same record).
- Return type:
- get_calibration(mode)[source]¶
The stored calibration record for ranking
mode(Noneif absent).A deep copy, so a caller cannot mutate the nested
gridback into the stored record (in-memory stores share their objects by reference).
- get_config()[source]¶
The persisted corpus build settings (empty dict if never written).
- Return type:
- get_links(artifact_id)[source]¶
The outgoing edges of artifact_id —
{edge_type: [target, ...]}.Empty dict when the artifact has no stored edges (or no links view). A copy, so a caller cannot mutate the persisted adjacency in place.
- Return type:
- get_maintenance_state()[source]¶
Background-work bookkeeping (e.g.
last_maintained);{}if unset.Kept under a separate
config-view key from the build settings: it is regenerable scheduler state (whenir maintainlast ran), not part of the corpus’s build identity, so it must never clobber it.- Return type:
- get_record(record_id)[source]¶
Reassemble the
Recordfor record_id (KeyErrorif absent).- Return type:
- ledger_items()[source]¶
Iterate
(key, entry)ledger pairs (the ledger may be mutated while iterating).
- classmethod local(name)[source]¶
File-backed store under
~/.local/share/ir/corpora/<name>.- Return type:
- matrix()[source]¶
Return
(record_ids, normalized_matrix, metas)for brute force.Rows are L2-normalized so cosine similarity is a dot product. Empty corpora return a
(0, 0)matrix.Caching is two-tier: an in-process cache (invalidated on the next write) backed, for file-rooted stores, by an on-disk packed cache — one normalized-matrix
.npyplus its ids/metas, written once and reloaded with a single memory-mapped read. The packed cache turns a cold reopen from a per-record vector-file storm (thousands of tiny reads) into three file reads; it is cleared by a writer’s first record write, and every record write replaces a write stamp that a packed set must match to be published or loaded, so a set built before any later write – by this process or another – is never served (i2mint/ir#86).Another process may be writing or deleting records while this one rebuilds. A record that vanishes or is only half-written when read is left out of the result (it is “not yet written”), with a warning; such a partial result is cached in-process but never published as the packed set, so a fresh process reads the records again.
- metas()[source]¶
Return
(record_ids, metas)without loading any vectors.The vector-free counterpart of
matrix(), for ranking modes that score on text alone (mode="lexical"): they need candidate metadata (text + filter fields) but never the embedding matrix, so they must not pay its I/O. Reuses the in-process or packed cache when present; else reads only themetaview (notvectors), skipping a record that vanishes or is half-written mid-read (asmatrix()does).
- put_record(record)[source]¶
Persist record’s metadata + vector, invalidating the search matrix.
The vector is written before the meta: record ids are listed from the meta view, so a reader in another process that lists an id always finds its vector (
delete_recordremoves in the reverse order).- Return type:
- set_calibration(mode, record)[source]¶
Persist a calibration
recordfor rankingmode(one per mode).modekeys a file in the calibration store, so it must be a non-empty string with no path separator (the real modes —dense/lexical/hybrid— already satisfy this).- Return type:
- set_config(settings)[source]¶
Persist the corpus build settings (name / embedder spec + id).
- Return type:
- set_ledger_entry(key, entry)[source]¶
Write the ledger entry (version / embedder id / record ids) for key.
- Return type:
- class ir.CoverageReport(name, on_disk, indexed, excluded, missing=<factory>)[source]¶
Bases:
objectDisk-vs-index coverage for a filesystem-backed corpus.
- class ir.Disclosure(artifact_id, level, name, score, summary, body=None, pointer=None, metadata=<factory>, source=None, passage=None)[source]¶
Bases:
objectThe progressively-disclosed payload for one selected artifact.
- artifact_id¶
the artifact this payload belongs to.
- level¶
how much was loaded —
"metadata"(no I/O),"body"(the pointer’s full text), or"bundled"(body + extras).
- name¶
a display name (the
namefilter field, else the id).
- score¶
the selecting hit’s score.
- summary¶
the matched surface text — always present, always cheap.
- body¶
the full payload (SKILL.md / file text);
Nonebelow"body"level or when the pointer could not be read.
- pointer¶
the source pointer (
skill_path/path) — the “package pointer” an agent follows to act;Noneif the hit has none.
- metadata¶
the hit’s filter metadata, plus a
disclosurenote when a pointer was present but unreadable (stale/moved/deleted), and anexpansionnote when expansion was requested but not possible for this hit.
- source¶
the corpus/source name the selecting hit came from (
Nonewhen unattributed) — the attribution a federated caller needs to tell two same-id artifacts from different corpora apart.
- passage¶
the expanded neighborhood text (
disclose(..., expand=...)) — the mid-granularity payload betweensummary(the matched surface) andbody(the pointer’s full text);Nonewhen expansion was not requested or not possible. Assembled from the corpus’s stored records (seeir.expand), unlikebody, which dereferences the pointer to an external resource.
- class ir.DiscoveryResult(query, mode, strategy, disclose_level, results, abstained, reason, n_retrieved, signals=<factory>)[source]¶
Bases:
objectThe result of
discover()— retrieve → select → (optional) disclose.The qh-exposable payload:
to_dict()is fully JSON-serializable (lists of dicts, floats, strings, bools — no numpy, no objects), so a FastAPI facade can return it directly.
- class ir.GraphStore(*args, **kwargs)[source]¶
Bases:
ProtocolStructural contract a traversal operator binds to — node + neighbors.
Deliberately minimal (two methods) so it is satisfied by ir’s
CorpusGraphand by any external graph:__getitem__resolves a node id to its scorable payload,neighborslists adjacent node ids (optionally of one edge type). Granularity-agnostic on purpose — an artifact graph and a surface-level tree are both ``GraphStore``s.runtime_checkablemakesisinstance(x, GraphStore)a structural check on attribute names only (not signatures) — enough to tell a conforming adapter from an arbitrary object, but it cannot validate thatneighborstakes the right arguments; treat it as a smoke check.
- class ir.IndexPlan(filter_fields=<factory>, surfaces=<factory>)[source]¶
Bases:
objectAn
IndexingStrategy’s output for one artifact.
- class ir.IndexingStrategy(*args, **kwargs)[source]¶
Bases:
ProtocolDecompose one artifact into filter fields + embeddable surfaces.
- class ir.MaintenancePolicy(reindex=<factory>, synopsis=<factory>)[source]¶
Bases:
objectThe background-work policy for one corpus (reindex + synopsis).
- class ir.MaintenanceResult(name, ran, reason, reindex=False, synopsis=False, records=None, error=None)[source]¶
Bases:
objectWhat
maintain_corpus()did (or would do) for one corpus.
- class ir.Package(*, chunk_size=1500, overlap=200, embed_deps=False, deps_template=None, max_tokens=None, token_model=None)[source]¶
Bases:
objectPackage strategy:
name + descriptionsurface plus README chunks.Filter fields capture ownership (ours vs third-party), name, deps. AI synopsis / problem-class surfaces are a documented extension point.
With
embed_deps=True,decomposeadditionally emits oneSurface(kind="deps", granularity="field")whose text is a prefix-form serialization of the bare dependency names (deps_template, default_default_deps_text()) — so a query for a domain matches a package by the libraries it depends on (e.g.sentence-transformers-> embeddings,networkx-> graphs), and the BM25 leg picks up exact dep-token matches. The deps bag is kept separate from prose (its own surface) so a rare library name is not diluted, and deps remain a filter field regardless.embed_depsdefaultsFalse(today’s behavior); it folds into the strategy id, so toggling it re-decomposes incrementally. The deps surface is appended last, leaving thedescription(position 0) andreadme_chunkindices unchanged.Surface indexing: the
descriptionsurface (kept whenever name or description is non-empty) occupies plan position 0, soreadme_chunkj is stored withRecord.surface_index == j + 1while its surface metadata sayschunk_index == j—surface_indexis plan-global,chunk_indexper-kind (seeir.base.Record.make_id()). Never derive sibling record ids fromchunk_index; use the ledger (ir.retrieve.records_for_artifact()).n_chunksis stamped on readme chunks at decompose time, but corpora built before the stamp keep records without it until the artifact re-indexes (content / embedder / strategy change) — read it withmetadata.get("n_chunks").- synopsis_fields = ('name', 'description', 'readme')¶
The record fields a synopsis is written from (see
ir.synopsis.with_synopsis()): prose only. The record’spath(a local absolute path) anddepsnever go to an LLM.
- class ir.Passage(artifact_id, surface_kind, score, text, record_ids=(), source=None, surface_index=None)[source]¶
Bases:
objectAn expanded hit: the seed’s identity + the stitched neighborhood text.
artifact_id/surface_kind/score/source/surface_indexare the seed hit’s — expansion never disturbs hit identity(source, artifact_id)or scores.textis the assembled neighborhood (overlap-deduped, plan order) andrecord_idsthe ordered stored segments it was stitched from (empty when expansion degraded to the seed’s own text).
- class ir.Record(id, artifact_id, surface_kind, surface_index, text, vector, metadata=<factory>)[source]¶
Bases:
objectA stored, embedded surface — one row of the index.
- static make_id(artifact_id, surface_kind, surface_index)[source]¶
Deterministic storage id for a surface of an artifact.
surface_indexis the surface’s plan-global position — its enumeration index across all surfaces of the artifact’sIndexPlan, regardless of kind — as assigned byir.index.build(). On multi-kind strategies it therefore differs from per-kind counters likemetadata["chunk_index"](e.g.Package: thedescriptionsurface takes position 0, shiftingreadme_chunkj tosurface_indexj+1— and the offset is plan-dependent, since empty surfaces are dropped).Ids of already-built corpora are a stability contract: never re-derive a sibling’s id from a per-kind index — address siblings through the ledger via
ir.retrieve.records_for_artifact().- Return type:
- class ir.ReindexPolicy(on='source-change', every_hours=None)[source]¶
Bases:
objectWhen to (incrementally) rebuild a corpus.
- class ir.SearchHit(artifact_id, surface_kind, score, text, metadata=<factory>, source=None, surface_index=None)[source]¶
Bases:
objectA scored record returned by retrieval (higher score = closer).
Maps onto ir_09’s
Result:textis the snippet,scorethe rank score,metadatathe meta, andpointerthe key into a resource store (ir_09 §5).to_dict()is the serialization-clean form for a cross-process / subagent boundary (no numpy scalars leak).sourceis the corpus/source name the hit came from (Nonewhen unattributed — e.g. an ad-hoc corpus without a name). It is a first-class field, not a metadata key, becausemetadatais the strategy-owned hard-filter namespace and provenance is structural: artifact identity is only unique within a source, so any cross-source operation keys on(source, artifact_id)(seebest_per_artifact()).surface_indexis the storedRecord.surface_indexof the hit’s surface — its plan-global position among the artifact’s surfaces — so a hit can name which surface of its artifact it is (the prerequisite for sibling addressing and context expansion).Nonewhen unknown (e.g. a hand-built hit). It is not the per-kindmetadata["chunk_index"]; seeRecord.make_id()for why the two differ on multi-kind strategies.
- class ir.Selection(selected, candidates, abstained, reason, signals=<factory>)[source]¶
Bases:
objectA selector’s commitment: the chosen subset of a ranked candidate list.
- selected¶
the committed hits, best-first (empty iff
abstained).
- candidates¶
the full ranked input, kept for provenance / audit.
- abstained¶
True iff the selector committed to nothing by policy.
- reason¶
which rule ended the commit (e.g.
"rel_threshold","score_gap","max_k","abstain:below_floor").
- signals¶
concrete, defined numbers behind the decision (
top_score,n_candidates,n_selected,min_ratio) — the auditable replacement for an opaque “confidence” float.
- property sufficient: bool¶
A model-free sufficiency hint for an agent’s Evaluator (ir_09 §3).
Truewhen this selection committed to at least one item (i.e. did not abstain). It is a signal, not a directive: the re-query /refinementdecision and the loop belong to the agent layer (the back-edge, ir_09 §4) —irderives this from its own outcome and never acts on it.
- class ir.Skill[source]¶
Bases:
objectCapability strategy: embed
name + descriptiononly.The body (SKILL.md) is loaded post-selection and is not indexed; name and parent are filter fields.
- class ir.Surface(artifact_id, kind, text, granularity='document', metadata=<factory>)[source]¶
Bases:
objectOne embeddable unit derived from an artifact.
kindnames the surface type (e.g."description","synopsis","problem_class","chunk") so a query can match the right part of an artifact.granularityis a coarse hint ("document"/"chunk"/"field").metadatais surface-local (e.g. chunk offsets).
- class ir.SynopsisPolicy(enabled=False, scope='recent', window_days=30, downtime_hours=None)[source]¶
Bases:
objectWhether/when to attach (expensive, LLM-generated) synopses.
downtime_hoursis a[start, end)pair of local-clock hours (wrapping past midnight is allowed, e.g.(22, 6));Nonemeans “any time”.scope="recent"limits synthesis to artifacts whose timestamp is withinwindow_days(corpora that expose a time signal);scope="all"synthesizes every artifact (bounded by incrementality).
- class ir.WalkPolicy(*args, **kwargs)[source]¶
Bases:
ProtocolThe pluggable strategy of a walk — graph semantics, not safety.
seedproduces the initial frontier;scoreranks a node against the query;selectchooses which scored frontier nodes to commit/expand this step (beam/greedy — default: all, best-first);expandyields a node’s neighbors;node_idis the hashable visited-set key;stopis the injected sufficiency check;to_hitmaterializes a committed node as aSearchHit— orNonefor a router-only node (a summary that routes but is not itself a result).
- class ir.WalkState(query, max_depth, budget, visited=<factory>, results=<factory>, cache=<factory>)[source]¶
Bases:
objectThe operator-owned state of one
traverse()call — the safety home.visited(node ids already committed),budget, andmax_depthare the structural safety primitives the operator enforces;resultsare the emitted hits;cacheis scratch space a policy may use (e.g. to embed the query once). A policy reads this but the operator enforces the bounds — a policy cannot opt out of termination.
- class ir.WholeText(*, text_key=None, kind='document')[source]¶
Bases:
objectOne surface = the entire text. Sensible default for a naive corpus.
- ir.as_retriever(corpus_or_name, **search_defaults)[source]¶
Bind ONE corpus to the uniform
Retrievercontract.Returns
retrieve(query, **overrides) -> list[SearchHit]that callssearch()withsearch_defaults(a per-call kwarg overrides a bound default). A corpus name is resolved once viair.open_corpus(); pass an openCorpusto skip that. The returned callable carries the bound corpus on.corpusfor introspection.>>> retr = as_retriever(corpus, mode="hybrid", k=20) >>> hits = retr("how do I deploy the app") >>> hits = retr("deploy", filter={"owner": "me"})
- ir.build(source, *, store=None, embedder=None, full=True, batch_size=256, edge_extractor=None)[source]¶
Build or incrementally update source into a
Corpus.- Parameters:
store (
CorpusStore|None)embedder (
Any)full (
bool)batch_size (
int)edge_extractor (
Optional[Callable]) – ((artifact_id, filter_fields) -> {edge_type: [target]}) that populates the corpus’s semanticlinksgraph (seeir.graph; passir.default_edge_extractor()for the latent deps/parent edges). Ingest is eager — edges are (re)written for every in-scope artifact, a decompose-only pass with no embedding, so the graph never goes partially stale — while embedding stays fully incremental. Edges are derived state, not part of build identity. A rebuild without an extractor leaves existing edges untouched (they are only refreshed by re-running with one, and only cleared per artifact by thefullprune below) — so droppingedge_extractordoes not wipe a graph.
- Return type:
- ir.build_corpus(name, **kwargs)[source]¶
Build (or update) a registered/preset corpus by name; returns a
Corpus.**kwargsare forwarded toir.build()— notablystore,embedder(e.g."light"for the numpy-only hashing embedder),full(prune artifacts no longer in the source), andbatch_size.
- ir.calibration_key(mode, tag=None)[source]¶
The key an abstention floor is stored and loaded under.
modealone for a first-stage ranking (today’s behaviour, so existing floors keep working);"<mode>+<tag>"for a reranked one.- Return type:
>>> calibration_key("hybrid") 'hybrid' >>> calibration_key("hybrid", "rerank-m+artifact") 'hybrid+rerank-m+artifact'
- ir.canonical_node_id(target, *, source)[source]¶
Canonicalize a neighbor target to a
(source, artifact_id)node id.The repo’s node identity is
(source, artifact_id)— the key a traversal visited-set must use so the same id in two corpora stays two nodes.CorpusGraph.neighbors()returns targets in stored form: a bareartifact_id(implicitly in source, the graph it came from) or a[source, artifact_id]cross-corpus pair. This resolves either to the canonical tuple.>>> canonical_node_id("dol", source="packages") ('packages', 'dol') >>> canonical_node_id(["skills", "deploy"], source="packages") ('skills', 'deploy')
- ir.collapsed_tree_policy(*, summary_kinds=('description', 'synopsis', 'capability', 'document'), leaf_kinds=('chunk', 'readme_chunk'), seed_k=10)[source]¶
The pure-vector summary-routing / collapsed-tree
WalkPolicy.Seeds on the top seed_k matches among
summary_kindssurfaces and descends to each routed artifact’sleaf_kindssurfaces (the emitted results), scored by cosine to the query. No LLM in the loop. A summary surface is a router (suppressed from results) only when its artifact has leaf surfaces; on a single-surface corpus (WholeTextdocument, Skillcapability) the summaries are leaf-less and emitted directly, so the walk degrades to flat-over-summaries instead of returning nothing.The defaults keep
document/capabilityinsummary_kinds*on purpose* — that is what lets a WholeText / Skill corpus seed at all; the structural router check (above) is what keeps those seeds from being silently swallowed.- Return type:
>>> hits = traverse(q, corpus, policy=collapsed_tree_policy())
- ir.default_edge_extractor(artifact_id, filter_fields)[source]¶
Edges latent in the standard filter fields:
deps→ REF,parent→ PARENT.deps(Package) →REFedges to each dependency’s bare name (version specifiers / extras / markers stripped). Self-edges and blanks are dropped.parent(Skill) → a singlePARENTedge.
A package whose
depsname other packages in the same corpus gets intra-corpus REF edges; third-party deps become REF edges to ids not in the corpus (harmless —CorpusGraph.neighbors()lists them, and a traversal simply finds no node to expand).Self-edges are dropped case-insensitively (
_dep_namelower-cases, so a package"AA"depending on"aa"is recognized as a self-reference).
- ir.default_policy_for_kind(kind)[source]¶
The smart-default
MaintenancePolicyfor a corpuskind.- Return type:
- ir.dependency_evidence_boost(relevant_libs, *, weight, saturate=1, deps_field='deps')[source]¶
A
dep_boostforsearch(): reward dependencies on relevant_libs.Reads the candidate’s
depsfilter field (asPackagestores it), reduces each spec to its bare, PEP 503-normalized name, and returnsweight * min(n, saturate) / saturatewherencounts the dependencies found in relevant_libs. With the defaultsaturate=1a single relevant dependency earns the fullweight. A structural signal, cheap and model-free, that a prose reranker cannot be fooled out of: an async-task package whose README says “DAG” does not depend on graph libraries.relevant_libs is the caller’s call: which libraries matter for a goal is a planning decision that belongs to the agent layer, never to
ir.weightis added to the score on the search’s scale (an RRF fused score is near0.03, a cosine near0.5, a sigmoid reranker in(0, 1)), so tune it per configuration.>>> from ir.base import SearchHit >>> boost = dependency_evidence_boost({"networkx"}, weight=0.1) >>> boost(SearchHit("unbox", "description", 0.4, "", {"deps": ["networkx>=3"]})) 0.1 >>> boost(SearchHit("au", "description", 0.5, "", {"deps": ["anyio"]})) 0.0
- ir.disclose(selection, *, level='body', loader=None, store=None, expand=None, corpus=None)[source]¶
Reveal the payload of each selected hit at
level— append-only, pure.- Parameters:
level (
str) –"metadata"(no I/O — summary + pointer only),"body"(load the pointer’s full text), or"bundled"(body + extras; today the same as"body", reserved for bundled scripts/references).loader (
Optional[Callable[[Mapping[str,Any]],str|None]]) – override the body resolver —metadata -> str | None. The default reads theskill_path/pathpointer from disk and tolerates a missing target (returnsNone, never raises).store (
Mapping[str,Any] |None) – aResourceStore(pointer -> payloadMapping) to dereference instead of disk — ir_09 §5 pointer-passing over adolstore / URL map / blob storage. Mutually exclusive withloader.expand (
Optional[Callable[[SearchHit,Sequence[Record]],Sequence[Record]]]) – aNeighborhoodPolicyto also stitch each hit’s neighborhood from the corpus’s stored records intoDisclosure.passage(seeir.expand). Orthogonal tolevel, which governs pointer payloads: e.g.level="metadata", expand=sentence_window_policy()reads no pointer at all but still returns mid-granularity passages. Requirescorpus=.corpus (
Any) – whereexpandfinds each hit’s stored siblings — aCorpus/CorpusStore/ name, or, for cross-source selections, a{source_name: corpus}Mappingresolved per hit viahit.source. Only meaningful withexpand=.
- Return type:
- Returns:
one
Disclosureper selected hit, best-first. This is a pure read: theSelectionand its hits are never mutated, so a caller can disclose append-only without disturbing a cached ranked prefix.
- ir.discover(corpus, query, *, k=10, mode='hybrid', strategy='conservative', disclose_level='metadata', filter=None, surfaces=None, max_k=3, rel=0.9, gap_ratio=0.5, min_score=None, merge='rrf', merge_weights=None, merge_rrf_k=None, loader=None, store=None, expand=None, **search_kw)[source]¶
Find and commit to the capabilities for
query— the one search tool.Retrieves
kcandidates, commits to a distractor-robust subset, and (optionally) discloses each committed item’s payload. This is the single agent-callable surface the capability-discovery research argues for: one tool that returns few, high-precision answers rather than a long candidate list the model must then filter under context rot.- Parameters:
corpus (
Any) – a builtCorpus, or a registered corpus name (resolved withir.open_corpus()). Pass a name for the qh / HTTP surface — it is the JSON-friendly form. Pass a list/tuple of names (or Corpus objects) for single-shot federated discovery across several corpora: each is searched, per-source abstention floors gate before any merging, and the survivors are rank-fused (seemerge). The caller names the sources explicitly; ir never chooses the set (source planning is the agent layer’s job, ir_09 §3).query (
str) – the user intent.k (
int) – candidate depth retrieved before selection. Federated:kcandidates are retrieved per source, and the fused ranking is also truncated tokbefore selection.mode (
str) – ranking mode —"hybrid"(default;ir’s strongest overall),"dense", or"lexical".strategy (
Union[str,Callable[[Sequence[SearchHit]],list[SearchHit]]]) – selection strategy (seeselect()).disclose_level (
str) –"metadata"(default; cheap, no body I/O),"body", or"bundled".filter (
Mapping[str,Any] |None) – retrieval constraints (forwarded toir.retrieve.search()).surfaces (
Iterable[str] |None) – retrieval constraints (forwarded toir.retrieve.search()).max_k (
int) – selection parameters (seeselect()).min_score="auto"loads the floor calibrated for this(corpus, mode)byir.eval.calibrate_min_score()and persisted on the corpus — the opt-in that turns on absolute abstention; it falls back to no floor (with a warning) when no calibration is stored or it is stale (a different embedder). Federated: floors are per-(corpus, mode, embedder), so a single number cannot apply across corpora — pass"auto"(each source’s own calibrated floor), a{name: floor_or_"auto"}mapping, orNone; a bare float raises. Floors gate each source on its own raw scores before fusion; the fused ranking is never floored (rank-fused scores are ordinal — ir_07/ir_08).rel (
float) – selection parameters (seeselect()).min_score="auto"loads the floor calibrated for this(corpus, mode)byir.eval.calibrate_min_score()and persisted on the corpus — the opt-in that turns on absolute abstention; it falls back to no floor (with a warning) when no calibration is stored or it is stale (a different embedder). Federated: floors are per-(corpus, mode, embedder), so a single number cannot apply across corpora — pass"auto"(each source’s own calibrated floor), a{name: floor_or_"auto"}mapping, orNone; a bare float raises. Floors gate each source on its own raw scores before fusion; the fused ranking is never floored (rank-fused scores are ordinal — ir_07/ir_08).gap_ratio (
float) – selection parameters (seeselect()).min_score="auto"loads the floor calibrated for this(corpus, mode)byir.eval.calibrate_min_score()and persisted on the corpus — the opt-in that turns on absolute abstention; it falls back to no floor (with a warning) when no calibration is stored or it is stale (a different embedder). Federated: floors are per-(corpus, mode, embedder), so a single number cannot apply across corpora — pass"auto"(each source’s own calibrated floor), a{name: floor_or_"auto"}mapping, orNone; a bare float raises. Floors gate each source on its own raw scores before fusion; the fused ranking is never floored (rank-fused scores are ordinal — ir_07/ir_08).min_score (
float|str|Mapping[str,float|str|None] |None) – selection parameters (seeselect()).min_score="auto"loads the floor calibrated for this(corpus, mode)byir.eval.calibrate_min_score()and persisted on the corpus — the opt-in that turns on absolute abstention; it falls back to no floor (with a warning) when no calibration is stored or it is stale (a different embedder). Federated: floors are per-(corpus, mode, embedder), so a single number cannot apply across corpora — pass"auto"(each source’s own calibrated floor), a{name: floor_or_"auto"}mapping, orNone; a bare float raises. Floors gate each source on its own raw scores before fusion; the fused ranking is never floored (rank-fused scores are ordinal — ir_07/ir_08).merge (
Union[str,Callable]) – federated only — how the per-source rankings combine:"rrf"(default; rank-based, scale-free — seeir.retrieve.fuse_hits()),"score"(raw-score merge, valid only when all corpora share an embedder — verified, raises on mismatch), or a callable{name: hits} -> hits.merge_weights (
Mapping[str,float] |None) – federated only — per-source trust weights formerge="rrf"(default 1.0 each).merge_rrf_k (
int|None) – federated only — the cross-source RRF rank constant (default:DFLT_RRF_K; distinct from the within-corpus hybridrrf_kinsearch_kw).loader (
Optional[Callable[[Mapping[str,Any]],str|None]]) – optional body resolver for disclosure (seedisclose()).expand (
Optional[Callable[[SearchHit,Sequence[Record]],Sequence[Record]]]) – aNeighborhoodPolicy— also stitch each committed hit’s neighborhood from its corpus’s stored records intoDisclosure.passage(retrieval-time context expansion, seeir.expand). Works at anydisclose_level; the federated form resolves each hit’s corpus via itssource.**search_kw (
Any) – any otherir.retrieve.search()keyword (rrf_k,rerank,bm25, …).
- Return type:
- Returns:
a
DiscoveryResult(.to_dict()for JSON / qh). Federated results addsignals["per_source"](per-corpusn_retrieved/top_score/floor/abstained) and each disclosure carries itssource.
- ir.expand(hit, corpus, *, policy=None)[source]¶
Expand hit into a
Passageof its neighborhood in corpus.Fetches the hit’s sibling records through the ledger (
ir.retrieve.records_for_artifact()), asks policy which to keep, and stitches them in plan order with overlap-aware dedupe. The default policy is a ±:data:DFLT_WINDOWsentence window; passparent_policy()for the whole artifact, or anyNeighborhoodPolicy.- Parameters:
hit (
SearchHit) – the seedSearchHit(its identity and score pass through to thePassageuntouched).corpus (
Any) – aCorpus,CorpusStore, or corpus name — whateverrecords_for_artifact()accepts. Must be the corpus the hit came from.policy (
Optional[Callable[[SearchHit,Sequence[Record]],Sequence[Record]]]) – which siblings make up the neighborhood (default: sentence window). A policy that selects nothing degrades the passage to the hit’s own text (record_ids=()) rather than returning nothing.
- Raises:
KeyError – the corpus has no ledger entry for the hit’s artifact (
ir.retrieve.NoLedgerEntry), or the ledger is stale — an entry listing records missing from the store.SeedNotFound – the default window policy cannot find the seed among its artifact’s stored records (stale hit / wrong corpus).
ValueError – the policy returned records that are not siblings of the hit’s artifact (operator-enforced safety), or the seed hit lacks
surface_index(hand-built hit) under the default window policy — useparent_policy(), which needs no seed position.
- Return type:
- ir.fuse_hits(hits_by_source, *, rrf_k=60, weights=None, identity=None, k=None)[source]¶
Merge per-source ranked hit lists into one ranking — by rank, not score.
The cross-source counterpart of the within-corpus hybrid fusion: scores from different (corpus, mode, embedder) tuples live on incommensurable scales (ir_07: “a different model re-scales everything”), so **raw scores never cross the source boundary** — within each source they order and dedup that source’s hits (one scale, sound), and across sources only ranks interact, via weighted Reciprocal Rank Fusion: each hit contributes
weights[source] / (rrf_k + rank).- Parameters:
hits_by_source (
Mapping[str|None,Sequence[SearchHit]]) –{source_name: ranked hits}. Hits without asourceare stamped with their mapping key (existing tags win, so one corpus bound under two keys still counts as one source). ANonekey is the untagged pseudo-source: its hits fuse as one rank group and stay unattributed (source=None). Within each list, duplicate artifacts — and, whenidentityis given, identity-duplicates — collapse to their best raw score before ranking, so a multi-query / multi-round pool can never double-count one artifact’s RRF mass.rrf_k (
int) – the RRF rank constant (standard default 60).weights (
Mapping[str,float] |None) – optional per-source trust dial (default 1.0 each) — a source’s contribution scales linearly, no score comparability needed. Keys naming sources absent fromhits_by_sourceare ignored (a per-round pool may legitimately lack a configured source); callers with a closed source set should validate keys upfront, as federatedir.discover()does.identity (
Union[Callable[[SearchHit],Any],str,None]) – how cross-source duplicates merge — seeIdentity. DefaultNone: never; each(source, artifact_id)stays a distinct result.k (
int|None) – truncate the fused ranking to this many hits.
- Return type:
- Returns:
the fused hits, best-first. Each carries the fused score in
scoreand keeps its pre-fusion magnitude asmetadata["source_score"](+"source_rank"), so downstream consumers (abstention gates, LLM judges) never lose the per-source signal. When anidentitymerge combined several sources’ hits,metadata["fused_sources"]lists them and the representative hit is the one with the best rank. Single-source input passes through with raw scores (RRF of one list is that list’s order — same convention as the hybrid fusion’s single-channel fallback), so the fused-score rescaling only happens when there is genuinely something to fuse. The post-fusionscoreis ordinal: valid for ordering and relative cuts, meaningless against absolute floors — apply calibratedmin_scorefloors per source, before fusing (see ir_07/ir_08 andir.discover’s federated form).
- ir.maintain(name=None, *, all=False, now=None, dry_run=False)[source]¶
Run due background work for one corpus (name) or every registered one (all).
With neither, defaults to all registered corpora. Returns one
MaintenanceResultper corpus considered.A named corpus raises; a sweep records. Asking for one corpus by name is a direct request, and swallowing its error would turn a typo’d name into a result that reads as success to any caller written before
errorexisted. A sweep is the unattended case, where the opposite is true: one corpus failing must not leave every corpus after it stale with nothing to say so.- Return type:
- ir.maintain_corpus(name, *, now=None, dry_run=False, full=True)[source]¶
Do the due background work for one corpus (idempotent).
Reads the corpus’s resolved policy and its
last_maintainedtime, decides whether a (synopsis-aware) reindex is due and permitted now, and — unlessdry_run— runs the incremental build and records the run.- Return type:
- ir.make_llm_formulator(*, rewriter=None, prompt='Rewrite the search query into {n} short, diverse alternative search queries that would retrieve the same target documents: fix typos, expand jargon, and add synonyms, but keep each a terse search phrase. One query per line, no numbering.\\n\\nQuery: {query}', n=3, fallback=None, **prompt_function_kwargs)[source]¶
An LLM-backed
Formulator(rewrite / expand / multi-query).rewriteris an injectablequery -> str | [str, ...]callable (a test double, or your own router); when omitted it is built lazily onaix(aix.prompt_func), so importing this module stays offline.nis the multi-query fan-out width. Any error or empty reply falls back tofallback(default:identity_formulator()).
- ir.make_llm_synthesizer(*, summarize=None, prompt='Write a concise synopsis (2-4 sentences) of the document below: what it is about and what questions it answers, so that a search over synopses can route to it. Output only the synopsis, no preamble.\\n\\nDocument:\\n{text}', model=None, synthesizer_id=None, text_key=None, text_fields=None, **prompt_function_kwargs)[source]¶
An LLM-backed
Synthesizer(Artifact→ synopsis).summarizeis an injectabletext -> strcallable (a test double, or your own summarizer); when omitted it is built lazily onaix(aix.prompt_func) on the first synthesis and reused — so importing this module, and even constructing the synthesizer, stays offline. The artifact’s text is extracted withir.strategy.text_of()usingtext_key— whichwith_synopsis()threads from the inner strategy, so the synopsis summarizes the same field the strategy indexes.text_fields(mutually exclusive withtext_key) names the fields of a mapping artifact whose text is sent to the LLM, and only those: without it, a mapping with notext/text_keyhas every string field joined and sent (seeir.strategy.text_of()) — including, for aPackagerecord, its localpath.with_synopsisthreads the inner strategy’ssynopsis_fields(Packagedeclares("name", "description", "readme")), so the Package default never leaks the path. The fields fold into the defaultsynthesizer_id, so changing them re-synthesizes. An empty text, or any synthesis error, yields""(the surface is then skipped, never a fabricated summary).The returned callable carries a
synthesizer_idattribute (default"aix:{model}:{sha(prompt)[:12]}") thatwith_synopsis()reads into the corpus’sstrategy_idfor staleness — a prompt or model change re-synthesizes.
- ir.make_search(corpus, *, name=None, description=None, k=8, mode='hybrid', rerank=None, rerank_text='surface')[source]¶
Return a corpus-bound
search(query, k=...) -> dicttool.The returned function exposes only
query(andk) — the corpus is fixed — so a connector built over it surfaces exactly one corpus and nothing else. Its__name__/__doc__are set so an MCP/agent host shows a clean tool name and description. Use this when wiring a single-corpus connector; usesearch()when the caller should choose the corpus.
- ir.make_thin_description_predicate(*, min_chars=40, field='description')[source]¶
A
Predicatetrue when the artifact’sfieldis empty or thin.Reads
artifact.raw[field](aPackagerecord’sdescription); a raw that is not a mapping is a thin source when its text is shorter thanmin_chars. Whitespace is ignored. The returned callable carries awhen_idstamp so the threshold folds intostrategy_id.>>> thin = make_thin_description_predicate(min_chars=10) >>> thin(Artifact(id="a", raw={"name": "a", "description": ""})) True >>> thin(Artifact(id="b", raw={"name": "b", "description": "Tools to parse dates"})) False
- ir.open_corpus(name, *, embedder=None)[source]¶
Reopen a previously built corpus by name.
The embedding model is lazily resolved (see
_LazyEmbedder): the returned corpus knows itsembedder_idfrom stored config immediately, but only loads the model when a dense/hybrid query actually embeds. Soir ls,ir info, and lexical-only search open a corpus without the model-load cost. Passembedder=to override the stored spec.Without an override the corpus is **pinned to the embedder it was built with** (its stored
embedder_id), not re-resolved from the spec: a spec like"default"means different models on machines with and withoutsentence-transformers, which would make the query vectors incompatible with the index.- Return type:
- ir.parent_policy()[source]¶
The whole artifact (small-to-big): every stored surface, plan order.
The mid-granularity analogue of
disclose(level="body")— but assembled from the indexed segments rather than dereferencing the pointer, so it works for corpora whose artifacts have no on-disk body.
- ir.policy_for(name)[source]¶
The effective
ir.policy.MaintenancePolicyfor corpus name.Resolves the registered entry’s
maintenanceover its kind’s smart default over the global default (seeir.policy.resolve_policy()). An unregistered name resolves to the global default policy.
- ir.records_for_artifact(store_or_corpus, artifact_id, *, surface_kind=None)[source]¶
All stored records of artifact_id, ordered by
surface_index.The sibling-addressing primitive beneath retrieval-time context expansion: a
SearchHitnames its artifact (and, viasurface_index, which surface of it matched); this returns every surface of that artifact, in plan order, so an expansion policy can stitch neighbors / parents around the hit.Resolution is ledger-backed only: the artifact’s ledger entry lists its
record_ids. Record ids are never re-derived from a per-kind index likemetadata["chunk_index"]— on multi-kind strategies that index differs from the plan-globalsurface_indexbaked into the id (seeir.base.Record.make_id()), so derivation would fetch wrong or missing siblings.- Parameters:
store_or_corpus – a
CorpusStore, anything carrying one as.store(e.g. aCorpus), or a corpus name — resolved straight to its local store: sibling lookup never embeds, so unlikeir.open_corpus()no embedder is loaded.artifact_id (
str) – the artifact whose surfaces to fetch.surface_kind (
str|None) – restrict to one surface kind (e.g."readme_chunk"); a known artifact with no surfaces of that kind yields[].
- Raises:
NoLedgerEntry – the ledger has no entry for artifact_id (an unknown artifact, or a corpus built without
ir.index.build()’s ledger bookkeeping). AKeyErrorsubclass.KeyError – an entry exists but lists a record missing from the store (a stale ledger: interrupted build or out-of-band
delete_record) — data corruption, named in the message.
- Return type:
- ir.register(name, kind, *, embedder='default', strategy=None, maintenance=None, storage=None, **params)[source]¶
Register (or overwrite) a named corpus definition.
Beyond the v1
kind/embedder/params, an entry may now carry (all optional, with smart per-kind defaults applied at resolution time — seeir.policy):strategy— anIndexingStrategy(or a{"name", "params"}spec) persisted so the corpus’s segmentation is stable across rebuilds.Nonekeeps the preset’s default strategy.maintenance— the background-work policy dict (reindex/synopsis; validated here, seeir.policy.MaintenancePolicy).storage— the persistence backend (default{"backend": "local"}).
Entries written by older
ir(none of these keys) keep working unchanged.- Return type:
- ir.reports_coverage(name='reports', *, projects_root=None, indexed_ids=None, exclude_dirs=frozenset({'.git', '.hg', '.ipynb_checkpoints', '.obsidian', '.svn', '.tox', '.venv', '__pycache__', 'build', 'dist', 'node_modules', 'site-packages', 'venv'}))[source]¶
Diff the reports docs tree on disk against the built
namecorpus.Walks every
*/*/docsand*/*/misc/docsfolder recursively (raw, no filtering), classifies each*.mdwith the shared inclusion SSOT, and reports how many includable reports are indexed vs. silently missing. Acoverage_ratiobelow 1.0 means on-disk reports are absent from the index — rebuild withir build <name>.indexed_ids(the built corpus’s artifact ids) is read from the registerednamecorpus by default; pass it explicitly (withprojects_root) to diff an arbitrary tree against an arbitrary index without opening a corpus.- Return type:
- ir.resolve_policy(entry)[source]¶
The effective policy for a registry
entry: entry over kind over global.A v1 entry (no
maintenancekey) resolves to its kind’s smart default, so existing corpora gain a sensible policy without a migration.- Return type:
- ir.resolve_reranker(spec)[source]¶
The reranker
specnames, orNonewhen there is none to apply.specisNone(no rerank), anyef.Rerankercallable (returned as is), or the string"cross-encoder"/"cross-encoder:<model>"– ef’s sentence-transformers cross-encoder, built lazily and cached per model name (loading one is the expensive part). If its optional dependency is absent this warns once and returnsNone, so the caller degrades to first-stage ranking; resolve before deriving a calibration key so the degraded run uses the plain key.>>> resolve_reranker(len) is len True >>> resolve_reranker("nope") Traceback (most recent call last): ... ValueError: unknown rerank spec 'nope'; expected a callable, 'cross-encoder' or 'cross-encoder:<model>'.
- ir.retriever_for(name, **search_defaults)[source]¶
A
Retrieverbound to the registered corpus name.Opens the corpus (it must have been built) and wraps it with
ir.as_retriever();search_defaults(e.g.mode="hybrid") bind to every call.
- ir.retrievers(**search_defaults)[source]¶
A lazy
Mapping[name, Retriever]view over the registry (ir_09 §8).The query-time projection of the build-recipe registry: each value is a ready-to-call
Retriever. This is the source-registry facade an orchestration layer (raglab) consumes — it never opens a corpus until the key is accessed, and always reflects the currentregistered()set.search_defaultsapply to every source.
- ir.reverse_dependency_policy(*, seeds, edge_type='REF', fan_in_weight=0.0, proximity_weight=0.0)[source]¶
A
WalkPolicythat walks dependency edges backwards from seeds.seeds are library ids the caller deems relevant to the query (ir never chooses them: picking the domain libraries for a goal is the planner’s job, in the agent layer). The walk commits each seed that is an artifact of the corpus, then its dependents (the artifacts whose
edge_typeedges point at it), then theirs, up totraverse()’smax_depth. A seed outside the corpus (a third-party library) routes without being a result. So a package that depends on a domain library but never says so in prose is still found.Each result scores its best surface’s cosine to the query, plus
fan_in_weighttimes its normalized fan-in (its share of the most depended-on target, a centrality prior), plusproximity_weight / (1 + hops)(closer to a seed scores higher). Both weights default to0, so the score is plain cosine. Hits carrymetadata["walk_depth"],metadata["fan_in"]and, for dependents,metadata["seed"].The store passed to
traverse()is aCorpusbuilt with an edge extractor (e.g.ir.default_edge_extractor(), whoseREFedges run from a package to its dependencies). Fuse the walk’s hits with flat search throughir.fuse_hits().- Return type:
>>> policy = reverse_dependency_policy(seeds=["networkx"]) >>> hits = traverse(query, corpus, policy=policy)
- ir.search(corpus, query, **kwargs)[source]¶
Search a
Corpus(or a corpus name, reopened lazily).Thin facade over
ir.retrieve.search();**kwargsare forwarded to it — the useful ones arek(how many hits),mode("dense"/"lexical"/"hybrid"),filter(avdMongo-style metadata filter),surfaces(restrict to surface kinds), andper_artifact(collapse to the best surface per artifact). Seeir.retrieve.search()for the full signature and defaults.
- ir.search_tool(query, *, corpus, k=8, mode='hybrid', filter=None, rerank=None, rerank_text='surface')¶
Search a named
ircorpus and return a JSON-serializable result dict.Thin agent-callable wrapper over
ir.discover()— returns its.to_dict()(committed results, scores, disclosures), fit to hand straight back from an MCP tool or HTTP endpoint. The corpus is a parameter, so this single function serves any corpus.- Parameters:
query (
str) – the natural-language query.corpus (
Any) – a registered corpus name (str), a list of names (federated search), or a builtir.Corpus.k (
int) – maximum number of results.mode (
str) –"dense"|"lexical"|"hybrid".filter (
dict|None) – optionalvdMongo-style metadata filter (hard pre-filter).rerank (
Any) – optional second-stage reranker (e.g."cross-encoder"); off by default.rerank_textpicks the text it reads.
- Return type:
- ir.select(hits, *, strategy='conservative', max_k=3, rel=0.9, gap_ratio=0.5, min_score=None)[source]¶
Commit to a subset of ranked
hits— the selection stage.- Parameters:
hits (
Sequence[SearchHit]) – rankedSearchHits (best first), as returned byir.retrieve.search().strategy (
Union[str,Callable[[Sequence[SearchHit]],list[SearchHit]]]) –"conservative"(the default distractor-robust commit), one of"top_k"/"abs_threshold"/"rel_threshold"/"score_gap", or anySelectorcallable (hits -> subset) — e.g. one built bymake_llm_selector().max_k (
int) – never commit to more than this many (caps distractor exposure).rel (
float) – relative-to-top keep threshold for"conservative"/ the ratio for"rel_threshold".gap_ratio (
float) – score-gap elbow ratio — used by the"score_gap"strategy only ("conservative"deliberately usesrelalone, not an elbow; see this module’s docstring).min_score (
float|None) – optional absolute floor; with"conservative"the selector abstains when even the top hit falls below it (also usable as the"abs_threshold"floor).
- Return type:
- Returns:
a
Selection.abstainedis True iffselectedis empty.
- ir.sentence_window_policy(k=1)[source]¶
±*k* same-kind neighbors around the seed (NEXT/PREV expansion).
The window runs over the artifact’s surfaces of the seed’s kind only, in plan order — a
readme_chunkwindow never swallows thedescriptionsurface.k=0selects just the seed’s own record.
- ir.tag_source(hits, source)[source]¶
Stamp source on every hit that doesn’t already carry one.
Existing tags win: a hit already attributed to a corpus keeps that attribution (so re-tagging under a different registry key cannot double-count one corpus as two sources). A
Nonesource is the untagged pseudo-source — hits pass through unattributed.
- ir.traverse(query, store, *, policy, max_depth=2, node_budget=64, k=10)[source]¶
Walk store from query under policy, returning the top-k hits.
The loop — score the frontier → select → commit → expand — is the operator’s; the safety primitives are non-negotiable and enforced here: a node id is committed at most once (the visited-set), expansion stops at
max_depth, and no more thannode_budgetnodes are ever committed. A policy whoseexpandcycles forever and whosestopnever fires still terminates.- Parameters:
query (
str) – the user intent.store (
Any) – passed to policy verbatim — aCorpusforcollapsed_tree_policy(), aCorpusGraphfor an artifact-link policy. The operator never inspects it.policy (
WalkPolicy) – theWalkPolicy(e.g.collapsed_tree_policy()).max_depth (
int) – maximum expansion depth from a seed (safety).node_budget (
int) – maximum nodes committed (safety).k (
int) – number of hits to return.
- Return type:
- Returns:
the committed hits, best-first, top-k — each a
SearchHitwithmetadata["walk_depth"]/["seed"].
- ir.with_synopsis(strategy, *, synthesize=None, synthesizer_id=None, synopsis_kind='synopsis', when='always', when_id=None)[source]¶
Wrap strategy to add one LLM-derived
synopsissurface per artifact.- Parameters:
strategy (
IndexingStrategy) – the innerIndexingStrategy(Chunked,Package, …). Its surfaces are kept; the synopsis is prepended.synthesize (
Optional[Callable[[Artifact],str]]) – an injectableArtifact -> str(test double / custom summarizer). Omitted →make_llm_synthesizer()(lazyaix).synthesizer_id (
str|None) – explicit identity stamp for staleness (recommended when injecting an unnamed callable / lambda). Omitted → the synthesizer’s ownsynthesizer_id/__qualname__.synopsis_kind (
str) – the surface kind (default"synopsis", a summary kind).when (
Union[str,Callable[[Artifact],bool]]) –"always"(default: every artifact) or a predicateArtifact -> bool; the synthesizer is called, and a synopsis surface added, only where it returns true (e.g.thin_description_predicate). A predicate that raises propagates.when_id (
str|None) – explicit identity stamp for an unnamed predicate (staleness); omitted → the predicate’swhen_id/__qualname__.
- Return type:
- Returns:
an
IndexingStrategyusable anywhere a strategy is —ir.CorpusSource.from_mapping(docs, name=..., strategy=with_synopsis(...)).
>>> strat = with_synopsis(Chunked(), synthesize=lambda a: "a summary")
Modules
Core data model for |
|
Command-line surface for |
|
Filesystem locations and process-wide defaults for |
|
Corpus-coverage diagnostic — what's on disk vs. what's actually indexed. |
|
Embedder resolution for |
|
Capability-discovery evaluation — measuring how well a corpus is found. |
|
LLM-backed generation of evaluation cases for |
|
Query formulation — the Formulator seam (ir_09 §3). |
|
The semantic link graph — typed edges between artifacts (report 12). |
|
The indexing pipeline and incremental maintenance. |
|
Idempotent background-work runner — |
|
Per-corpus policy — how a corpus is segmented/stored and what background work it gets. |
|
Named-corpus registry — persistent, reusable corpus definitions. |
|
Dependency check for |
|
The precision stage: a cross-encoder rerank over the first-stage candidates. |
|
Retrieval — hard metadata filtering + dense / lexical / hybrid ranking. |
|
Install and operate the OS job that runs |
|
Defining a corpus source — an abstract strategy plus parameters. |
|
Persistence for |
|
Indexing strategies — the "what do we index?" seam. |
|
Synopsis surfaces — LLM-derived summaries as an indexed surface (report 12). |
|
Agent-callable tool surface over |