# ADR-0003: Strongly typed stage interfaces with a two-zone data model

**Status:** Accepted
**Date:** 2026-07-13

## Context

Pipeline stages must exchange source data and extracted artifacts. Universal
source data has one engine-wide meaning, while extracted artifacts have
domain-specific shapes. Passing opaque bytes or `any` between all stages would
make invalid wiring and merge behavior runtime concerns. Requiring JSON at
every handoff would preserve interoperability but discard useful Go type safety
while all modules are in-process.

The framework must also support multiple configured artifact domains, durable
checkpoints, diagnostics, and output encoders without making those consumers
depend on every domain's Go types.

## Decision

Notarius uses two typed data zones followed by one serialized boundary.

### Source zone

Input and chunk stages use conservative, engine-owned document, segment, chunk,
and source-reference types. Their exact Go names are implementation details.
Every segment carries engine-owned source provenance identifying the source
location from which it was produced. Chunks preserve the ordered provenance of
their segments.

Source-format-specific fields remain in input modules or explicitly namespaced
metadata; they do not become framework contracts.

### Domain artifact zone

Each artifact lane has one domain-owned Go artifact type `T`. Its extract,
merge, normalize, and domain-aware validation implementations use generic,
strongly typed contracts over the same `T`. Raw JSON, opaque bytes, and `any`
are not stage-handoff contracts within a lane.

Each registered domain artifact type supplies a codec for `T`. The codec owns:

- stable schema identity and an explicit schema version;
- JSON serialization and deserialization;
- the media type and schema metadata required at serialized boundaries; and
- rejection of data that cannot be represented by the declared artifact
  schema.

An artifact type's JSON representation is a maintained domain contract.
Changing it incompatibly requires a new schema version.

Extract, merge, and normalize may change the contents of `T`, but they do not
change the lane's canonical Go artifact type or artifact schema identity. An
extractor maps any provider- or prompt-specific response type into `T` before
returning. A future lane that requires different artifact types at different
stages requires a new architectural decision.

### Serialized boundary

After normalization, each typed artifact is converted into an engine-owned
serialized artifact containing bytes, media type, and schema metadata. Output
aggregation and output encoders consume this type-erased form. Intermediate
checkpoint and debug encodings do not become stage-handoff contracts.

LLM transport, checkpoints, and opt-in debug recording are also explicit
serialization boundaries. They may encode or decode a typed artifact through
its domain codec, but they do not change the in-memory type used between
extract, merge, normalize, and typed validators. Checkpoint reuse requires a
compatible schema identity and version.

An LLM structured-response schema is a module transport contract and may differ
from the domain artifact schema. The calling module owns the response type and
maps it into the canonical `T`; the artifact codec remains authoritative for
artifact checkpoints and output serialization.

The framework may use private type-erased adapters to store heterogeneous lane
registrations and execute configured domains. Such an adapter must assemble a
type-consistent lane before execution and must not expose `any` or raw payloads
as module-facing handoffs inside the domain artifact zone.

### Construction and dependencies

Every module operation accepts `context.Context`. Modules receive stable runtime
collaborators through an injected dependency set at construction time. In
particular, LLM-using modules receive the application-provided structured LLM
client and do not construct provider clients or bypass shared scheduling.

The application boundary enforces one configurable global upper bound on
in-flight LLM calls across all stages, lanes, retries, and validators.

Configuration options are parsed and validated while a module is constructed,
before that module executes. Per-run data such as source material, references,
session identity, and lane identity remains operation input rather than a
construction dependency.

## Alternatives considered

- Pass raw bytes between stages. This maximizes decoupling but moves wiring,
  parsing, and merge errors to runtime and prevents domain types from being the
  canonical in-process contract.
- Require JSON plus schemas at every stage boundary. This is appropriate for an
  out-of-process boundary, but adds serialization and parsing inside the current
  in-process pipeline. The stable codec contract preserves this upgrade path if
  remote plugins are introduced.
- Use a uniform `Process(any) (any, error)` contract. This simplifies a fully
  dynamic engine but turns incompatible module composition into type assertions
  and runtime failures. The fixed topology does not require that tradeoff.

## Consequences

Domain pipelines gain compile-time handoff safety and explicit merge semantics.
Serialization, schema compatibility, checkpoint decoding, and output erasure
have named owners. Dynamic registration requires a small erased adapter around
each typed lane, and generic stage implementations must be instantiated for a
specific artifact type or behavior rather than manipulating arbitrary JSON.

The engine-owned source model becomes a long-lived contract and must evolve
conservatively. Domain authors must maintain a codec and versioned schema in
addition to their Go artifact type.
