Skip to content

Target architecture

Why the workspace boundaries sit where they do.

The product has two logical stages in one deployable system:

  1. plan() observes an input cheaply and returns a serializable, explainable route bound to a versioned capability snapshot.
  2. encode() executes either that supplied plan or a newly created plan and returns a block tree, embeddings, usage, and an execution trace.

Planning never invokes the expensive processing paths. Execution never silently changes a caller-supplied plan: it may use only the fallbacks already declared in that plan.

indx-interfaces leaf contracts, extension protocols, one shared helper
indx-source bounded loading, digest, media-type detection
├── indx-router preflight, signatures, registry view, planning
├── indx-executor capability invocation, fallback, blocks, embeddings
└── indx public facade over router and executor
├── indx-app-server FastAPI transport
└── indx-app-cli in-process CLI and server launcher

The arrows are dependency direction toward the packages above them:

  • indx-interfaces has no workspace dependency. It is contracts and protocols with one fenced exception: a helper that is standard library only, holds no state, does no I/O and reads no INDX_* variable may live here, because it is a rule the packages below already share rather than an implementation (ADR-0034).
  • indx-source depends only on indx-interfaces. It exists as a package rather than a router module because both stages need the same bytes: the router observes them, and the executor validates a supplied plan’s source_digest against them before handing the content to a capability. Loading it twice in two places is how the two stages would come to disagree about what the caller sent.
  • indx-router and indx-executor depend on indx-interfaces and indx-source, and never on one another.
  • indx composes both and re-exports only the supported public surface.
  • Application packages depend on indx; indx never imports applications.

The protocol guide explains the cross-package ports that connect these layers and the responsibilities on each side of those boundaries.

source + constraints
cheap preflight ──► capability snapshot ──► RoutePlan
supplied or generated │
executor + fallbacks
blocks + embedding spaces + trace

Routes have a document default, page overrides, and optional region overrides. Every selected capability exists in the recorded snapshot. A plan is bound to the source content digest, policy version, and snapshot ID so it can be replayed or rejected explicitly when incompatible.

The extension contract is a typed CapabilityProvider. Installed distributions will advertise providers in the indx.capabilities Python entry-point group. Discovery uses importlib.metadata.entry_points(); the registry rejects duplicate IDs and produces a deterministically ordered, content-addressed snapshot. Heavy implementations must remain lazy until execution.

Discovery loads provider distributions at run time. That is not a workspace dependency: no first-party package imports a provider, and the boundary test forbids naming one, so “router and executor never depend on one another” still holds when a provider module is imported during discovery.

Hook frameworks are deferred. The first requirement is keyed discovery and a reproducible snapshot, not ordered 1:N hook invocation.

See the detailed guides for CapabilityRegistry and CapabilityProvider.

  • Native text extraction on CPU.
  • Generic OCR on CPU for scanned pages.
  • Generic VLM on GPU where OCR cannot meet quality.
  • One optional invoice signature and invoice parser.
  • One optional process-chart signature and parser, page-scoped, over PowerPoint shapes.
  • Manual review or explicit failure as the terminal fallback.

Region optimization, broad modality coverage, RAG export, persistence, and separate router/executor deployments follow only after the first slice has measured baselines.

Benchmark data and labels live under the active repository’s benchmarks/ contract. There is no dependency on indx-everything-bench. Every admitted document has more than one page and carries scope-level requirement and acceptable-route labels under pinned constraints and capability snapshots.