Skip to content

Per-Pipe IO Contracts

Every valid validate report carries a pipe_io_contracts map: for each pipe, the typed contract of what it takes in and what it produces. It sits beside the input-form descriptor on the same report, and the two are keyed identically — by the namespaced pipe_ref (domain.code) — over exactly the same set of pipes, PipeSignature placeholders included.

Reach for pipe_io_contracts when you need the schema and the declared contract; reach for input_form when you are building a form and want field kinds, defaults, and choices without reading schemas.

Where it lives

  • mthds.protocol.pipe_io_contracts — the wire models (PipeInputContract, PipeOutputContract, PipeIOContract, IOMultiplicity, PresenceMarker), declared by the MTHDS standard's Python client and mirroring its pipe-io-contracts page. Pipelex does not declare a second copy of them.
  • pipelex/pipeline/pipe_io_contracts.py — the one public derivation, build_pipe_io_contracts(pipes), which imports and re-exports those models. Because it constructs the standard's own closed shapes, a projection that drifted from the standard fails at derivation rather than on the wire.
  • PipelexValidationReport.pipe_io_contracts (pipelex/pipeline/validation_report.py) — where the artifact travels.
  • pipelex/pipeline/validate_in_process.py — the in-process assembly derives it inside the validation library's window, right beside build_input_form. Iterating the same loaded pipes is what makes the two key sets equal by construction.

What an entry carries

Each pipe gets an inputs map and one output.

An input entry carries the concept_ref, the JSON Schema of its content (json_schema), and two facts about the slot as declared:

  • presence — the authored marker, verbatim: plain, optional (?, the caller may omit it and the pipe handles absence itself), or force (!, must be provided, and the author asserted so). plain and force are both required; the distinction is the assertion, which lint and graph surfaces read.
  • multiplicity — single, variable (Concept[], unbounded), or fixed (Concept[N]), with item_count carrying the exact N on the fixed arm and null otherwise. Concept[1] is single: no list framing.

An output entry carries concept_ref, the same multiplicity / item_count pair, and a two-valued optional — ! is rejected on outputs, so there is nothing three-valued to report. It carries no json_schema: the schema render runs per input only. A consumer that wants the output's payload shape gets it from build output --format schema, not from this contract.

Where the concept identity sits on the schema

The JSON Schema on an input names its concept: title is the concept_ref, description the concept's authored description. On a single slot those sit at the top level; on variable and fixed slots the schema is an array wrap that keeps them on items, and a fixed count adds minItems/maxItems on the wrap itself. A variable list carries no bounds.

{
  "type": "array",
  "items": { "type": "object", "title": "my_domain.Gadget", "description": "…", "properties": { "…": "…" } },
  "minItems": 2,
  "maxItems": 2
}

The one concept with no structure class: native.Anything

native.Anything is the only concept that declares no structure class, so its input schema cannot come from a pydantic render. (That is a different question from the three natives which declare no pinned structure — Dynamic, Anything, Composite — and therefore render as unknown in the input-form descriptor.) It publishes the identity annotations every rendered input schema carries and one constraint — {"title": "native.Anything", "description": "…", "not": {"type": ["array", "null"]}}. An Anything value is any JSON value but an array or null: a list is the multiplicity's to express, as Anything[], and null is never an input. Multiplicity wraps exactly as above: Anything[] is an array of that schema, so every item carries the same exclusion, and a fixed count adds the bounds.

The schema and the input shaper agree on what a value may be: a string, a number, a boolean or an object at an Anything slot is shaped into its natural content (text, number, yes/no, JSON object) under the native.Anything concept, and an array or null at a single slot is refused, as the schema says. The two differ in two places the schema does not state: it wraps a single bare value sent to an Anything[] input into a one-item list, and it refuses an Anything[] item keyed exactly concept and content, which reads as an envelope.

Which surfaces carry it

The protocol validate operation carries pipe_io_contracts on its report. The Agent CLI does not: pipelex-agent validate emits the verdict, pending_signatures, is_runnable, and its own advisory warnings array, and stops there — the typed contracts are not part of its envelope.

Seeing it

pipelex-dev trace-input-semantics captures the contracts as hop5_pipe_io_contracts.json beside hop5_input_form.json, so an authored fact can be checked on both projections at once — see Tracing Input Semantics. The emitted shapes are pinned by tests/integration/pipelex/pipeline/test_pipe_io_contracts.py.