# Pipeline

## Concept

A pipeline is a linear sequence of pure transformation steps: input flows
through `transform_1 → transform_2 → ... → transform_n` to produce output,
with each step depending only on the previous step's output and producing
its own output without side effects.

## Shape

Every pipeline implementation carries these invariants:

- **Linear data flow.** Steps form a list (or composable equivalent), not a
  graph. Each step has exactly one input source — the previous step — and
  exactly one output channel — the next step. No fan-out, no fan-in, no
  cross-step references.
- **Pure transformation steps.** Each step is a function `(input) -> output`
  with no side effects. The output is determined entirely by the input. (See
  `code-design.md` §Effects — pipeline steps are the "pure" half of the
  effect / pure split; effect-doing belongs in interceptors or
  effects-as-data, not in pipeline steps.)
- **Data-in / data-out at every step boundary.** The value flowing between
  steps is the entire contract. No threaded `Context` carrying out-of-band
  state, no shared mutable accumulator, no implicit channels. Steps that
  need additional context take it as a parameter at construction time
  (closure capture) — not at call time.
- **Composable as a value, not a closure.** The pipeline itself is a list
  of steps the program can introspect, slice, prepend to, or recompose —
  not an opaque `compose(f, compose(g, h))` closure chain. References
  `code-design.md` §Composability's Inspectability invariant.
- **Identity-step admissible.** An empty pipeline (zero steps) is a valid
  pipeline — it returns its input unchanged. This is the monoid identity;
  pipelines compose with the empty pipeline yielding themselves. See
  `code-design.md` §Composability invariant 2 (Identity).

## Forecloses

When a function is designed as a pipeline, the following designs are
explicitly *not* the right choice for the same problem:

- **Interceptor stack.** Pipelines have no enter / leave / error
  three-traversal machinery; if the problem genuinely needs compensation
  or error-aware unwinding, choose interceptor instead. Choosing pipeline
  forecloses that lifecycle.
- **Effects in the steps.** A "step" that writes a file, posts an HTTP
  request, or reads the clock is not a pipeline step — it is an effect.
  Pipeline-as-design says effects live elsewhere; mixing them in destroys
  the pure-transformation invariant.
- **Shared mutable accumulator.** Threading state through a shared object
  the steps mutate is not a pipeline — it is procedural code wearing a
  pipeline costume. Pipelines compose by returning new values; an
  accumulator-mutating loop has a different testability and parallelism
  story.
- **Branching dispatch.** A step that inspects its input and routes to one
  of several next steps via conditional dispatch is not a pipeline step —
  it is a router. Pipelines are linear by definition; conditional fan-out
  belongs in a different shape (often effects-as-data, where the branch
  is a value the runtime interprets).

## Judgment probes

These checks verify that a function actually implements the pipeline
pattern (vs merely being a sequence of statements). Future detector
work (per RFC #590 sibling issue #597) will operationalize these as
mechanical or LLM-judgment checks.

- **Step enumerability.** Can a reader list the steps of the pipeline by
  reading one function? If the "pipeline" is spread across N functions
  that call each other transitively, the linear shape is lost; the
  reader has to reconstruct it by graph traversal.
- **Step purity audit.** Pick each step and ask: would replacing this
  step with the identity function change anything outside the
  pipeline's return value? If yes, the step is not pure — it is an
  effect or has a hidden side channel.
- **Step argument count.** Each step should accept exactly one argument
  (the value flowing through) plus any closure-captured parameters bound
  at pipeline-construction time. A step that accepts the pipeline's
  initial input plus a separate "context" argument is not a pipeline
  step — it is participating in a different abstraction.
- **No cross-step state.** Search the steps for references to a mutable
  collection (`list`, `dict`, `set`) that lives outside the step's scope
  and is mutated by multiple steps. Any such reference disqualifies the
  pattern.
- **Identity-pipeline reachable.** Is there a way to construct an empty
  pipeline (zero steps) and apply it to an input, yielding that input
  unchanged? If no — if the pipeline machinery requires at least one
  step to function — the implementation is missing the identity element.
- **Replace-step locality.** Can a single step be replaced (in a test or
  a production variant) by changing the construction call, with no
  modification to the other steps? If replacement requires touching the
  whole pipeline, the steps are coupled and the linear shape is
  illusory.

## Render: Python

Python's idiomatic pipeline rendering is a `list[Callable[[T], T]]`
applied via `functools.reduce`. The list — not a `compose(f, compose(g,
h))` closure chain — is what makes the pipeline a value with the
Inspectability invariant from `code-design.md` §Composability. Steps
that need construction-time configuration bind it via closure (or
`functools.partial`); call-time signature stays single-argument.

```python
from collections.abc import Callable
from functools import partial, reduce
from typing import TypeVar

T = TypeVar("T")
Step = Callable[[T], T]

def apply_pipeline(steps: list[Step[T]], value: T) -> T:
    """Run a pipeline. An empty list is the identity (monoid)."""
    return reduce(lambda acc, step: step(acc), steps, value)

# Example: construction-time config bound by closure / partial.
def _trim(s: str) -> str:
    return s.strip()

def _lower(s: str) -> str:
    return s.lower()

def _prefix(p: str, s: str) -> str:
    return f"{p}{s}"

pipeline: list[Step[str]] = [_trim, _lower, partial(_prefix, "user:")]
result = apply_pipeline(pipeline, "  Alice  ")  # "user:alice"
```

The shape mirrors how `forge_manager_mcp/server/handlers/release.py`'s
`release_project` orchestrator builds its check list — each `_check_*`
helper is a pure `(env) -> dict` step, and the orchestrator concatenates
their results (a fold over checks, not a fan-out). Construction-time
config (project_dir, target version) is closure-captured by each
`_check_*` partial bound at orchestrator entry, not threaded as a second
call-time argument.

### Probes

Each probe is implementable as an `ast.walk` check over the candidate
function (the one tagged `@design pattern pipeline` per #595).

- **single-arg step calls.** Walk every `Call` node inside the function
  body. For each call whose target is a step (resolved from the local
  step list), assert `len(node.args) == 1` and `len(node.keywords) == 0`.
  More-than-one argument at call time disqualifies the step shape —
  closure capture or `partial` is the binding mechanism.
- **list-or-tuple step container.** The composition vehicle is an
  `ast.List` or `ast.Tuple` literal (or a name resolving to one) — not a
  nested `Call(Call(Call(...)))` chain. AST-detect by checking that no
  `Call` node has another `Call` node as its direct `func` for more than
  one level (single-arg chained calls are admissible; deep nesting that
  hides the step count is not).
- **identity-pipeline path reachable.** Either an explicit zero-element
  literal (`[]` / `()`) flows into `apply_pipeline`, or a test exists
  that calls the runner with an empty container. Detect by searching for
  `Call(func=Name("apply_pipeline"), args=[List(elts=[]), ...])` or
  equivalent in the test module.
- **no mutable accumulator across steps.** Walk for `AugAssign`,
  `list.append`, `dict.__setitem__`, or `set.add` calls on a name
  declared outside any single step's scope and referenced inside two or
  more steps. Any hit disqualifies the pattern (Forecloses §Shared
  mutable accumulator).
- **no branching dispatch in steps.** Walk each step's body for `If`
  nodes whose true-branch and false-branch both contain a `Return` /
  expression that resolves to a different downstream step. Conditional
  dispatch is router-shape, not pipeline-shape (Forecloses §Branching
  dispatch).
- **step return-type uniformity.** Each step's annotated return type
  (from `Step[T]`) equals its single parameter's annotated type. AST
  detection via `FunctionDef.returns` and `FunctionDef.args.args[0]
  .annotation` equality.

### Example

`mcp-server/src/forge_manager_mcp/release/version_asset.py` —
`generate_version_asset` orchestrates `_resolve_version → render_version
_asset → write_version_asset` as a near-pipeline (the resolution-step
output feeds the render-step input feeds the write-step input). The
intermediate try/except wrapping makes this a railway-flavoured pipeline
in practice (see `railway.md`); the pure-pipeline shape is what
`apply_pipeline` above models.

### Anti-patterns

- **`compose(f, compose(g, h))` closure chain.** A function returned
  from `functools.reduce(compose, steps)` where `compose` produces a
  closure looks like a pipeline but has no inspectable structure — a
  reader cannot list the steps without unwrapping `__closure__` cells.
  Violates Inspectability.
- **Shared list mutation across step bodies.** A pipeline of steps each
  doing `accumulator.append(x)` on a list closed over from outside the
  steps is a procedural loop wearing pipeline syntax. Violates the
  Forecloses §Shared mutable accumulator clause; the steps' outputs no
  longer carry the contract.
- **Step that calls `httpx.get` / `Path.read_text` directly.** Effect
  work inside a step body breaks the pure-transformation invariant
  (Forecloses §Effects in the steps) and Rule 10's `injectable_seams`
  gate. Effects-as-data or interceptor are the correct shapes when
  effect work is genuinely needed.

## Render: Go

Go's idiomatic pipeline rendering uses generic step functions (Go 1.18+)
collected into a slice the caller can introspect and fold over. The
slice — not a `compose(f, compose(g, h))` closure chain — is what makes
the pipeline a value-with-inspectability rather than an opaque
composition.

```go
package pipeline

// Step is a single pure transformation T -> T. Steps close over any
// construction-time dependencies; at call time they take exactly one
// argument (the value flowing through) and return exactly one value.
type Step[T any] func(T) T

// Pipeline is a slice of steps. The empty pipeline is the identity.
// Callers may introspect (len, indexing), slice, prepend, append, or
// recompose — the pipeline is a value, not a closure.
type Pipeline[T any] []Step[T]

// Apply runs the pipeline over an input. An empty pipeline returns
// its input unchanged (monoid identity).
func (p Pipeline[T]) Apply(in T) T {
	out := in
	for _, step := range p {
		out = step(out)
	}
	return out
}

// Example construction with closure-captured config:
//
//   trim := func(s string) string { return strings.TrimSpace(s) }
//	 lower := func(s string) string { return strings.ToLower(s) }
//	 prefix := func(p string) Step[string] {
//	     return func(s string) string { return p + s }
//	 }
//	 p := Pipeline[string]{trim, lower, prefix("user:")}
//	 result := p.Apply("  Alice  ")  // "user:alice"
```

### Probes

- **Step-type homogeneity (AST).** Walk the package's declared `Step`
  types via `go/ast` and verify each is `func(T) T` for some
  monomorphic `T`. A `func(T, Context) T` signature or a `func(T) (T,
  error)` signature is a different pattern (interceptor or railway
  respectively) and should be flagged.
- **Pipeline-as-slice check.** Locate the pipeline value's declared
  type. It must be a slice (or array) of step type, not a function
  composition (`func(T) T` produced by folding). A composition closure
  is opaque to introspection and fails the "composable as a value"
  invariant.
- **Step argument count.** For each step's function literal or named
  function declaration, walk `*ast.FuncType.Params.List` and assert
  `len(params) == 1`. A step taking two parameters (value + context) is
  participating in a different abstraction.
- **No cross-step state.** Walk each step body via `ast.Inspect` and
  flag references to package-level `var` declarations of map/slice/chan
  types where the step performs an assignment (`*ast.AssignStmt` with
  LHS resolving to a package-level identifier). Cross-step mutable
  state disqualifies the pattern.
- **No effect calls in step bodies.** Walk each step body for calls
  whose selector resolves to a known IO package (`os`, `net/http`,
  `io/ioutil`, `database/sql`, `time.Now`, `os.Getenv`). Effect calls
  inside a pipeline step break the pure-transformation invariant —
  those belong in effects-as-data or interceptor renderings.
- **Empty pipeline reachable.** Search for at least one test (`*_test.go`)
  that constructs a `Pipeline[T]{}` and calls `Apply` on it. Absence
  of an identity-pipeline test is a soft warning — the identity
  property may hold by accident but is not pinned by a test.
- **Replace-step locality (AST diff probe).** A renaming refactor that
  replaces step `i` with a different function should result in a diff
  touching exactly one slice-literal element. If the refactor cascades
  into other step bodies, coupling between steps exists.

### Example

`io.Pipe` + chained `io.Reader` wrappers in the Go stdlib
(`bufio.NewReader(gzip.NewReader(file))`) is the canonical streaming
pipeline; for value-level pipelines, `golang.org/x/text/transform`
exposes a `Chain(t ...Transformer) Transformer` that is exactly the
slice-of-steps idiom above (each `Transformer` is the
single-input-single-output unit).

### Anti-patterns

- **Step that returns `(T, error)`.** A step signature `func(T) (T,
  error)` is railway-shaped, not pipeline-shaped. If the design needs
  error short-circuiting, use the railway rendering and its `Result`
  vocabulary; mixing the two collapses the failure semantics.
- **`compose(f, compose(g, h))` closure chain.** Folding the steps
  into a single `func(T) T` via reduction is opaque — `len(pipeline)`
  is no longer meaningful, slicing is impossible, and the
  inspectability invariant is lost. Keep the slice.
- **Step body mutating a captured pointer.** A step closing over
  `*State` and mutating `state.Counter++` inside the step is a
  shared-mutable-accumulator anti-pattern dressed as a pipeline.
  Steps must return new values, not mutate captured pointers.
- **Step routing to one of several next steps.** A step body
  containing `if input.Kind == X { return stepA(input) } else { return
  stepB(input) }` is a router, not a linear step. Routing belongs in
  effects-as-data (the branch as a value the runtime interprets) or in
  an interceptor's enter handler — not in a pipeline step body.

## Render: Ada

In Ada, a pipeline is a generic package parameterised over the data
type, with each step exposed as a child generic package whose
`Apply` function is a pure transformation. The pipeline value is a
constrained array of access-to-function values (one per step)
introspectable at runtime; composition is array concatenation. The
identity pipeline is the empty array — admitted by the array's index
subtype `Positive range 0 .. <>`. Contracts (`Pre`, `Post`) on the
step access type enforce purity at the type level: each step's
function profile carries `Global => null` (Ada 2012 SPARK-style aspect)
so the compiler / GNATprove rejects any step that reads or writes
external state.

```ada
generic
   type Data is private;
package Pipelines is

   --  A step is a pure data-in / data-out function. The
   --  Global => null aspect (SPARK / Ada 2022) forbids reads or
   --  writes of external state at the type level; any step that
   --  touches a package-level variable fails compilation under
   --  GNATprove.
   type Step is access function (Input : Data) return Data
     with Global => null;

   --  Pipeline = constrained array of steps. Length 0 is admitted
   --  by the index subtype's lower bound; the empty pipeline is
   --  the monoid identity.
   type Pipeline is array (Positive range <>) of Step;

   Empty : constant Pipeline (1 .. 0) := (others => null);

   function Run (P : Pipeline; Input : Data) return Data
     with Post => (if P'Length = 0 then Run'Result = Input);

   function Compose (Left, Right : Pipeline) return Pipeline
     with Post => Compose'Result'Length = Left'Length + Right'Length;

end Pipelines;

package body Pipelines is

   function Run (P : Pipeline; Input : Data) return Data is
      Current : Data := Input;
   begin
      for S of P loop
         Current := S.all (Current);
      end loop;
      return Current;
   end Run;

   function Compose (Left, Right : Pipeline) return Pipeline is
   begin
      return Left & Right;
   end Compose;

end Pipelines;
```

### Probes

- **Step type carries `Global => null` (or equivalent).** The
  pipeline's step access type declaration must include a
  `Global => null` aspect or a SPARK `pragma Pure_Function`. A step
  access type with no purity aspect cannot enforce the pipeline
  invariant mechanically; flag it. (libadalang: walk
  `AccessToSubpDef` nodes, check `f_aspects` for `Global` with
  `null` actual.)
- **Pipeline is an array-typed value, not a record-of-procedures.**
  The pipeline type must be a constrained or unconstrained array of
  access-to-function values. A record with named components per
  step (`type Pipeline is record Step_1, Step_2, Step_3 : ... end
  record`) collapses the introspectability — array indexing is the
  load-bearing access shape.
- **Run loops, does not recurse.** The Run function body iterates
  over the array (for-loop or `for S of P loop`). A recursive Run
  (`Run (P (P'First + 1 .. P'Last), S.all (Input))`) is admissible
  semantically but harder to introspect; the iterative form is the
  idiomatic shape. (libadalang: locate the `Run` body, check for
  `LoopStmt` containing a call on `Step` access values.)
- **No package-level `Current : Data` (or equivalent mutable
  state).** The package body must not declare a package-level
  variable of type `Data` that any step reads or writes. Walk the
  `PackageBody` declarations for `ObjectDecl` of type `Data` and
  flag any whose initialiser is not a constant.
- **Empty pipeline declared and reachable.** A constant `Empty` of
  type `Pipeline (1 .. 0)` (or equivalent length-zero literal) must
  exist in the public part. Without it the identity element is
  unreachable from clients.
- **Run postcondition asserts identity behaviour.** The Run
  function's `Post` aspect must include the clause
  `(if P'Length = 0 then Run'Result = Input)` — without it the
  identity property is not contract-enforced.

### Example

N/A — synthesized example. Closest real-world analogue: AdaCore's
`Ada.Strings.Unbounded.Translate` chain (functional composition of
mapping functions), and the Ada Distilled examples for generic
pipelining over typed buffers.

### Anti-patterns

- **Procedure-chain via package-level state.** A package that
  exposes `Step_1`, `Step_2`, `Step_3` as procedures all reading
  from and writing to a package-level `Current : Data` variable —
  the chain forms by call order, not by composition. Looks like a
  pipeline, mutates shared state.
- **Tagged-type chain via `Next` access component.** A `Step`
  tagged type with a `Next : access Step'Class` component, where
  each step's body calls `Step.Next.Apply (Result)`. The recursion
  is opaque; the steps are not enumerable without traversing the
  linked list at runtime.
- **Step procedures with `in out Data` and side-effecting bodies.**
  Steps declared as `procedure Apply (D : in out Data)` whose
  bodies write to `Ada.Text_IO.Put_Line` or open files. The
  `in out` mode disguises the purity violation as state-threading
  and the IO call breaks the pure-step invariant.

## Render: TypeScript

TypeScript's idiomatic pipeline rendering is a
`ReadonlyArray<(x: T) => T>` step array folded with `reduce`. The
array — not an `f(g(h(x)))` nesting — is what carries the
Inspectability invariant: the steps are an introspectable value, not
closure cells. Construction-time configuration binds via arrow-function
closure; call-time signature stays single-argument.

```typescript
type Step<T> = (value: T) => T;

const applyPipeline = <T>(steps: ReadonlyArray<Step<T>>, value: T): T =>
  steps.reduce((acc, step) => step(acc), value);

// Construction-time config bound by closure.
const trim: Step<string> = (s) => s.trim();
const lower: Step<string> = (s) => s.toLowerCase();
const prefix = (p: string): Step<string> => (s) => `${p}${s}`;

const pipeline: ReadonlyArray<Step<string>> = [trim, lower, prefix("user:")];
const result = applyPipeline(pipeline, "  Alice  "); // "user:alice"
```

### Probes

Mechanical (wired in `code_design_drift_gap`'s `_TYPESCRIPT_PROBES` —
text/structural checks over the `.ts` source; v1 ships the two
highest-signal probes, the rest are judgment probes):

- **no_mutable_accumulator.** The function body must not mutate a
  shared accumulator across steps: no `+=` compound assignment and no
  `.push(` onto an array shared between steps. A pipeline whose steps
  push into a closed-over array is a procedural loop wearing pipeline
  syntax (Forecloses §Shared mutable accumulator).
- **step_purity.** No effect call inside the step bodies: `fetch(`,
  `axios.`, `fs.`, `Date.now(`, `localStorage.` disqualify the
  pure-transformation invariant (Forecloses §Effects in the steps).

Judgment (prose-only):

- **steps_are_array_literal.** The composition vehicle is an
  array-of-functions value (`[trim, lower, prefix("x")]`), not an
  `f(g(h(x)))` nesting — the step count must be readable without
  unwrapping closures.
- **step return-type uniformity.** Each `Step<T>` returns its own
  parameter type.

### Example

The SolidJS UI's derived-list transforms (`mcp-server/ui/src/`) fold
filter/sort/format steps over fetched swimlane payloads — each step a
pure `(rows) => rows` arrow, composed as an array value.

### Anti-patterns

- **`f(g(h(x)))` nesting.** No inspectable structure — violates
  Inspectability exactly like Python's `compose` chain.
- **`let acc` reassigned across steps / `.push(` into a shared
  array.** A mutable accumulator wearing pipeline syntax.
- **`await fetch(...)` inside a step.** Effect work belongs in
  effects-as-data or an interceptor chain, not a pipeline step.
