ir.synopsis

Synopsis surfaces — LLM-derived summaries as an indexed surface (report 12).

The document-summary-index / collapsed-tree fuel (ADR #43): run a summarizer over each artifact at build time to produce a short synopsis, index it as a "synopsis" surface, and let the collapsed-tree policy (ir.traverse()) route a synopsis match down to that artifact’s chunks. Build-time cost, ≈free at query time — and incremental, so only new / changed artifacts are re-synthesized.

with_synopsis() wraps any IndexingStrategy and adds one synopsis surface per artifact:

strat  = ir.with_synopsis(ir.Chunked(), synthesize=my_summarizer)
corpus = ir.build(ir.CorpusSource.from_mapping(docs, name="d", strategy=strat))
hits   = ir.traverse(q, corpus, policy=ir.collapsed_tree_policy())  # routes via synopsis

The synopsis is prepended (plan position 0) so it is the first summary surface — hence the collapsed-tree router (on with_synopsis(Package()) the synopsis, not the terse description, routes). An empty synopsis is dropped, so a synth that returns "" simply leaves the artifact with its other surfaces.

synthesize: Callable[[Artifact], str] is injectable (a test double, or your own summarizer); omitted, it is built lazily on aix (the multi-provider LLM facade) via make_llm_synthesizer() (the make_llm_* idiom — import ir stays offline, aix is imported only on the first synthesis).

Staleness. The wrapper exposes its identity as scalar attributes (synthesizer_id, synopsis_kind) and holds the inner strategy, so ir.index._strategy_id() (which recurses into nested strategies) folds both the inner strategy’s parameters and the synthesizer identity into the corpus’s strategy_id. A prompt / model change — or an inner-strategy change — therefore re-synthesizes exactly the affected artifacts, the same way an embedder_id change does; no silent staleness. (An injected unnamed synthesizer — a lambda or a local closure — has no stable identity to fold in, so with_synopsis warns and disables its staleness tracking unless you pass synthesizer_id=.)

Routing needs no edges. collapsed-tree descends synopsis→chunks within an artifact via ir.retrieve.records_for_artifact() (shared artifact_id), so synopsis routing works with no entries in the ir.graph links view — that view models cross-artifact edges (REF / PARENT between artifacts), a different grain than surface→surface within one artifact.

Conditional synopsis. when= (default "always") restricts synthesis to the artifacts a predicate selects, so LLM cost is paid only where a synopsis helps — e.g. with_synopsis(ir.Package(), when=thin_description_predicate) synthesizes only for packages whose [project].description is empty or short (their README is the only topical signal), and skips the rest *without calling the synthesizer*. The predicate’s identity folds into strategy_id (when_id) like the synthesizer’s, so changing it re-processes the corpus (strategy_id is corpus-wide) and re-synthesizes every artifact the new gate selects.

**Caveat — edge_extractor.** Eager edge ingest (build(edge_extractor=...)) calls decompose for every artifact each build, and synthesis lives in decompose; so combining with_synopsis() with an edge_extractor re-runs synthesis on every rebuild. The common path — synopsis routing with no edge_extractor — stays fully incremental.

Module Attributes

Synthesizer

an Artifact → its synopsis text ("" to skip — no synopsis surface is added for that artifact).

Predicate

an Artifact → whether to synthesize for it.

ALWAYS

when value meaning "synthesize for every artifact" (the default).

DFLT_THIN_DESCRIPTION_CHARS

Descriptions shorter than this many characters count as thin by default.

SYNOPSIS_KIND

The surface kind a synopsis is indexed under — a member of ir.traverse.DFLT_SUMMARY_KINDS, so collapsed-tree routes from it.

SYNOPSIS_PROMPT

Default prompt for make_llm_synthesizer() (a routing-oriented summary).

TOPICAL_SYNOPSIS_PROMPT

A topical / classification-oriented alternative to SYNOPSIS_PROMPT, suited to thin-description packages (opt-in: make_llm_synthesizer(prompt=...)).

thin_description_predicate(artifact)

Ready-made gate: synthesize only for packages with an empty / short description.

Functions

make_llm_synthesizer(*[, summarize, prompt, ...])

An LLM-backed Synthesizer (Artifact → synopsis).

make_thin_description_predicate(*[, ...])

A Predicate true when the artifact's field is empty or thin.

thin_description_predicate(artifact)

Ready-made gate: synthesize only for packages with an empty / short description.

with_synopsis(strategy, *[, synthesize, ...])

Wrap strategy to add one LLM-derived synopsis surface per artifact.

ir.synopsis.ALWAYS = 'always'

when value meaning “synthesize for every artifact” (the default).

ir.synopsis.DFLT_THIN_DESCRIPTION_CHARS = 40

Descriptions shorter than this many characters count as thin by default.

ir.synopsis.Predicate

an Artifact → whether to synthesize for it.

Type:

A synthesis gate

alias of Callable[[Artifact], bool]

ir.synopsis.SYNOPSIS_KIND = 'synopsis'

The surface kind a synopsis is indexed under — a member of ir.traverse.DFLT_SUMMARY_KINDS, so collapsed-tree routes from it.

ir.synopsis.SYNOPSIS_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}'

Default prompt for make_llm_synthesizer() (a routing-oriented summary).

ir.synopsis.Synthesizer

an Artifact → its synopsis text ("" to skip — no synopsis surface is added for that artifact).

Type:

A synthesizer

alias of Callable[[Artifact], str]

ir.synopsis.TOPICAL_SYNOPSIS_PROMPT = 'Write a concise synopsis (2-3 sentences) of the software package described below: which domain or problem class it belongs to (e.g. audio, graphs, http clients, data validation) and what it is used for, so that a search for that domain finds it. Output only the synopsis, no preamble.\n\nPackage:\n{text}'

A topical / classification-oriented alternative to SYNOPSIS_PROMPT, suited to thin-description packages (opt-in: make_llm_synthesizer(prompt=...)).

ir.synopsis.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).

summarize is an injectable text -> str callable (a test double, or your own summarizer); when omitted it is built lazily on aix (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 with ir.strategy.text_of() using text_key — which with_synopsis() threads from the inner strategy, so the synopsis summarizes the same field the strategy indexes.

text_fields (mutually exclusive with text_key) names the fields of a mapping artifact whose text is sent to the LLM, and only those: without it, a mapping with no text / text_key has every string field joined and sent (see ir.strategy.text_of()) — including, for a Package record, its local path. with_synopsis threads the inner strategy’s synopsis_fields (Package declares ("name", "description", "readme")), so the Package default never leaks the path. The fields fold into the default synthesizer_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_id attribute (default "aix:{model}:{sha(prompt)[:12]}") that with_synopsis() reads into the corpus’s strategy_id for staleness — a prompt or model change re-synthesizes.

Return type:

Callable[[Artifact], str]

ir.synopsis.make_thin_description_predicate(*, min_chars=40, field='description')[source]

A Predicate true when the artifact’s field is empty or thin.

Reads artifact.raw[field] (a Package record’s description); a raw that is not a mapping is a thin source when its text is shorter than min_chars. Whitespace is ignored. The returned callable carries a when_id stamp so the threshold folds into strategy_id.

Return type:

Callable[[Artifact], bool]

>>> 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.synopsis.thin_description_predicate(artifact)

Ready-made gate: synthesize only for packages with an empty / short description.

Return type:

bool

ir.synopsis.with_synopsis(strategy, *, synthesize=None, synthesizer_id=None, synopsis_kind='synopsis', when='always', when_id=None)[source]

Wrap strategy to add one LLM-derived synopsis surface per artifact.

Parameters:
  • strategy (IndexingStrategy) – the inner IndexingStrategy (Chunked, Package, …). Its surfaces are kept; the synopsis is prepended.

  • synthesize (Optional[Callable[[Artifact], str]]) – an injectable Artifact -> str (test double / custom summarizer). Omitted → make_llm_synthesizer() (lazy aix).

  • synthesizer_id (str | None) – explicit identity stamp for staleness (recommended when injecting an unnamed callable / lambda). Omitted → the synthesizer’s own synthesizer_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 predicate Artifact -> 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’s when_id / __qualname__.

Return type:

IndexingStrategy

Returns:

an IndexingStrategy usable anywhere a strategy is — ir.CorpusSource.from_mapping(docs, name=..., strategy=with_synopsis(...)).

>>> strat = with_synopsis(Chunked(), synthesize=lambda a: "a summary")