# ADR 0117: Contract streams own validation and wire termination

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0117-contract-streams-own-validation-and-wire-termination.md

---

# ADR 0117 — Contract streams own validation and wire termination

## Context

`rawResponse`, `streamingRoute`, `parseNDJSON` and `parseSSE` intentionally solve
separate low-level jobs. Used alone they cannot prove that a producer yields the
declared shape, that a client validates it, or that normal EOF is different from
a truncated operation. An unconstrained line can also grow before JSON parsing.

## Decision

An HTTP endpoint may declare `stream: { item, format, maxFrameBytes,
lifetimeMs, heartbeatMs, idleTimeoutSeconds, terminal }`. Its handler returns an
`AsyncIterable` of schema-derived items and its typed client returns an owned
`AsyncIterableIterator` of the same output type.

The wire is a bounded internal envelope: `data`, safe `error`, and explicit
`end`. The server validates before encoding; the client validates both envelope
and item. EOF without `end`, or `end` without a declared terminal item, fails
explicitly. Iterator `return`, request abort, producer failure and operation
lifetime converge on one cancellation signal.

The descriptor is HTTP-only. It does not create a tool, cursor, replay log,
topic, durable subscription or business completion model. `rawResponse` and raw
`streamingRoute` remain available where the application owns the protocol.

The low-level NDJSON/SSE parsers gain a one-mebibyte default line ceiling and
fatal UTF-8 decoding. Parse errors fail closed unless the caller deliberately
supplies `onParseError`.

## Consequences

- Finite logs and quiet observations share one contract/client pipeline without
  claiming durable delivery.
- The default encoded contract frame limit is 256 KiB and may be tightened per
  endpoint.
- Post-header failures use safe codes because an HTTP status is no longer
  available.
- Existing tolerant parser consumers must opt back into skipping malformed
  records with `onParseError`.
