Repository navigation
Synopsis surfaces: LLM-derived summaries as indexed surfaces (document-summary routing fuel) #48
Description
Activity
Design decision (confirming before coding).
Module. New
ir/synopsis.py(an operator module besideexpand.py/formulate.py/graph.py):with_synopsis(strategy, *, synthesize=None, synthesizer_id=None, synopsis_kind="synopsis") -> IndexingStrategy— wraps any strategy, adds onesynopsissurface per artifact.make_llm_synthesizer(*, summarize=None, prompt=SYNOPSIS_PROMPT, model=None, synthesizer_id=None, **prompt_function_kwargs) -> Synthesizer— the lazy-oadefault (mirrorsmake_llm_formulator/make_llm_selector;oaimported only inside the call, soimport irstays offline).Synthesizer = Callable[[Artifact], str].
Surface placement. The synopsis is prepended (plan position 0) so it is the first summary surface → the collapsed-tree router (the #48 intent: route from a synopsis match). On
with_synopsis(Package())the synopsis (not the tersedescription) routes. An empty synopsis (synth returns "") is dropped — the artifact keeps its other surfaces.Staleness — reuse the existing ledger mechanism, no new bookkeeping. The wrapper exposes its identity as scalar attrs (
synthesizer_id,synopsis_kind) and holds the inner strategy. I makeindex._strategy_idrecurse into nested strategy attributes (a small, generic change: any attr with adecomposemethod folds in its own_strategy_id). Sostrategy_idencodes (inner strategy + params) + synthesizer identity → a model/prompt change OR an inner-param change re-synthesizes exactly the affected artifacts through the normal incremental path. Defaultsynthesizer_id = oa:{model}:{sha(prompt)[:12]}; an injected double should passsynthesizer_id=(else falls back to its__qualname__).Routing is surface-level, not links-view edges. collapsed_tree (#47) descends synopsis→chunks within an artifact via
records_for_artifact(sameartifact_id), so synopsis routing needs no edges. I'm therefore not writing synopsis→chunk PARENT/CHILD edges into the #46linksview: that view is artifact-keyed with artifact-id targets (cross-artifact REF/PARENT), whereas synopsis→chunk is surface→surface within one artifact — a different grain. This refines the issue body's wording. (Cross-artifact synopsis edges, if ever wanted, are just anotheredge_extractor.)Known interaction (documenting, not fixing here). Eager edge ingest (
build(edge_extractor=...)) callsdecomposefor every artifact each build, so combiningwith_synopsiswithedge_extractor=re-runs synthesis every rebuild (synthesis lives indecompose). The common case — synopsis routing with noedge_extractor— stays fully incremental (the AC). I'll document the caveat; decoupling edge extraction from fulldecomposeis a separate build() refactor I can file if wanted.Proceeding on this.
- added a commit that references this issue
on Jun 13, 2026 Shipped in ir 0.1.22 (squash-merge 746d859, PR #55).
ir.with_synopsis(strategy, *, synthesize=None)adds one LLM-derivedsynopsissurface per artifact (prepended → the collapsed-tree router);make_llm_synthesizeris the lazy-oadefault (import irstays offline). Staleness reuses the ledger —index._strategy_idnow recurses into nested strategies, folding inner-strategy params + synthesizer identity intostrategy_id, so a model/prompt/inner change re-synthesizes only the affected artifacts.Adversarial review (12 agents, every finding verified) caught two should-fixes — both contradicting docstrings shipped in the PR — fixed before merge:
- text_key alignment — the default synthesizer summarized
text_of(raw)with notext_key, sowith_synopsis(Chunked(text_key='body'))summarized the wrong field. Now threads the inner strategy'stext_key. - non-identifiable synthesizer — an unnamed lambda / local closure aliased to a shared
<lambda>/<locals>qualname → swapping it was silently stale. Now warns and uses a sentinel id (named functions + explicit/stamped ids still track).
Plus 4 test-hardening nits (Package router precedence, mutation-resistant offline guarantee, default-id content-stability, non-str guard, file-backed round-trip). 365 tests, lint/format clean.
This completes the cap-1 graph arc (#46 links → #47 traverse → #48 synopsis). Next per ADR #43 sequencing: budget governor + run-log (raglab), then PurposeStore.
- text_key alignment — the default synthesizer summarized
Problem
The motivating case for linked retrieval (ADR #43): run an LLM over each document to produce a synopsis, index the synopses, and route from a synopsis match down to the document's chunks. ir's data model reserved the slot —
"synopsis"is a documented surface kind and the strategy docstrings name "AI synopsis / problem-class surfaces" as the extension point — but no strategy produces them. This is report 12's document-summary-index pattern (build-time LLM cost, ≈free at query time) and the natural fuel for the collapsed-tree policy (#47).Scope
with_synopsis(strategy, *, synthesize=None)): wraps any IndexingStrategy, adds onesynopsissurface per artifact.synthesize: Callable[[Artifact], str]is injectable; built lazily onoaonly when omitted (themake_llm_*idiom —import irstays offline; tests inject doubles).surfaces={"synopsis"}search works immediately; PARENT/CHILD edges to the artifact's chunks land in the links view (links: typed-edge view on CorpusStore + GraphStore protocol (semantic link graph) #46) at build time.embedder_id/strategy_idare stamped, so a prompt change triggers re-synthesis, not silent staleness.Acceptance criteria
build(source, strategy=with_synopsis(Chunked()))with an injected fake synthesizer indexes a synopsis surface per artifact, hermetically.Size: M. Depends on #46 (edges) and feeds #47 (routing policy). Capability 1 (ir). Refs ADR #43, report 12.