Typed Pipeline Review¶
A typed pipeline review is a boundary audit, not a count of annotations. Module 01
uses distinct domain values to make illegal stage order visible, RagPipe to preserve
unary composition types, and direct orchestration where collection shape would make a
generic chain less clear.
Trace the actual shapes¶
flowchart LR
raw["RawDoc"] -->|"clean_doc"| clean["CleanDoc"]
clean -->|"chunk_doc(doc, env)"| sliced["list[ChunkWithoutEmbedding]"]
sliced -->|"map embed_chunk"| embedded["list[Chunk]"]
embedded -->|"structural_dedup_chunks"| final["list[Chunk]"]
Review both the element type and the container shape:
| Boundary | Producer | Consumer requirement | Review result |
|---|---|---|---|
| raw to clean | clean_doc: RawDoc \| CleanDoc -> CleanDoc |
chunk_doc needs CleanDoc plus RagEnv |
element types fit; configuration remains explicit |
| clean to chunks | chunk_doc returns list[ChunkWithoutEmbedding] |
embed_chunk needs one ChunkWithoutEmbedding |
direct composition is invalid; mapping is required |
| embedded to canonical | mapped embedding returns list[Chunk] |
dedup accepts list[Chunk] |
container and element types fit |
The one-to-many transition is the important pressure. A wrapper that records only
A -> B cannot invent flattening semantics. docs_to_embedded therefore uses named
list comprehensions rather than pretending every stage is a unary value transform of
the same shape.
Check what the generic wrapper actually guarantees¶
RagPipe[A, B].then accepts Callable[[B], C] and returns RagPipe[A, C]. For a
genuinely unary chain, this rejects a wrong adjacent value:
from collections.abc import Callable
from funcpipe_rag import CleanDoc, RagEnv, RawDoc, chunk_doc, clean_doc
from funcpipe_rag.rag_pipe import RagPipe
def chunk_count(env: RagEnv) -> Callable[[CleanDoc], int]:
def count(doc: CleanDoc) -> int:
return len(chunk_doc(doc, env))
return count
count_chunks = RagPipe(clean_doc).then(chunk_count(RagEnv(chunk_size=8)))
result: int = count_chunks(
RawDoc("paper", "Types", "typed pipelines", "cs.PL")
)
The invalid adjacency is shorter:
clean_doc produces CleanDoc; embed_chunk requires
ChunkWithoutEmbedding. A static checker should reject construction before the
pipeline is called. By contrast, Module 01's flow uses
Callable[[Any], Any], so this same mismatch can escape static checking:
That difference is a real benefit of RagPipe, but it does not justify using the
wrapper where direct orchestration is clearer.
Separate static, runtime, and behavioral evidence¶
No one proof route establishes the whole contract:
| Evidence | Establishes | Does not establish |
|---|---|---|
| strict mypy | the seven tracked source files satisfy their declared static relationships | valid runtime configuration, purity, or correct output |
| frozen dataclass validation | RagEnv.chunk_size is an exact positive integer and values reject reassignment |
correct stage order or chunk coverage |
| foundation tests | normalization, chunk coverage, embedding locality, canonical behavior, and input preservation | absence of every type mismatch in unexecuted code |
| source review | effects and Any escapes are visible to a reviewer |
laws over a broad generated input space |
This matters for the cleaning fixed-point law:
The runtime expression requires clean_doc to accept its own CleanDoc output.
Typing the parameter only as RawDoc would make the executable law and the public
annotation disagree even if Python happened to run it successfully.
Run the two independent routes¶
From the repository root:
make PROGRAM=python-programming/python-functional-programming \
capstone-foundation-types
make PROGRAM=python-programming/python-functional-programming \
capstone-foundation-proof
The type target uses strict mypy, points MYPYPATH at the Module 01 snapshot, and
writes its cache under artifacts/. Snapshot isolation is essential: resolving
imports from the live Module 10 package could produce a green result for the wrong
source state.
The behavior target runs the six-test learning proof against the same snapshot. A review should require both results rather than treating passing runtime tests as a static check or treating mypy as proof of domain behavior.
Review the annotation cost¶
Keep the typed wrapper when:
- adjacent intermediate types differ and a mismatch is plausible;
- a unary chain is reused as one callable;
- the inferred public result remains understandable;
- strict checking is part of the executable review path.
Prefer a direct function when:
- a stage expands one value into many;
- mapping or flattening policy must remain visible;
- configuration would need hidden capture solely to fit the wrapper;
Anywould erase the guarantee the abstraction claims to add.
Types also stop at effect behavior. Callable[[str], int] can describe both a pure
length function and one that writes to a global audit list. Purity still needs source
review and behavioral evidence.
Compact review route¶
Before accepting a typed pipeline change:
- Write each stage as
input -> output, including containers. - Mark every one-to-many, flattening, and configuration boundary.
- Search public helper signatures for
Any. - Identify one realistic invalid adjacency the checker rejects.
- Run
capstone-foundation-types. - Run
capstone-foundation-proof. - Explain what remains a source-review claim.
Move to Isolating Side Effects once you can explain why
RagPipe(clean_doc).then(embed_chunk) is statically invalid, why flow cannot prove
that, and why neither fact establishes purity.