Effect Boundaries¶
Page Maps¶
graph LR
family["Python Programming"]
program["Python Functional Programming"]
section["Data First Apis Expression Style"]
page["Effect Boundaries"]
capstone["Capstone evidence"]
family --> program --> section --> page
page -.applies in.-> capstone
flowchart LR
orient["Orient on the page map"] --> read["Read the main claim and examples"]
read --> inspect["Inspect the related code, proof, or capstone surface"]
inspect --> verify["Run or review the verification path"]
verify --> apply["Apply the idea back to the module and capstone"]
Module 01 already separates a pure RAG transformation from a CSV-to-JSONL shell. It lets ordinary filesystem exceptions escape. Module 02 keeps that structural separation and adds one new boundary promise: expected read, parse, and write failures are returned as values.
The design goal is not “remove all effects.” A useful application must read and write. The goal is to make effect ownership visible so the transformation can still be tested and reasoned about with ordinary values.
The boundary in one sentence¶
Keep path handling and filesystem access in thin adapters, pass replaceable capabilities explicitly, and call the document-based core only after input has become validated domain data.
In the completed Module 02 state, that rule has two related forms:
injected path API
path -> Reader.read_docs -> Result[list[RawDoc]]
-> full_rag_api_docs -> Ok[(chunks, observations)]
concrete filesystem shell
input path -> FSReader -> full_rag_api -> write_chunks_jsonl -> Result[Observations]
The first form isolates the reader so tests can substitute it. The second proves that the real CSV and JSONL adapters obey the same successful-value contract.
Read the actual source in ownership order¶
All paths below are under
capstone/module-reference-states/module-02/src/funcpipe_rag/.
| Owner | Source | Responsibility |
|---|---|---|
| domain values | rag_types.py |
define documents, chunks, and RagEnv invariants |
| pure application | api/core.py |
transform RawDoc values and calculate observations |
| capability contract | api/config.py |
define Reader and group core/boundary dependencies |
| outcome value | result.py |
define the minimal Ok[T] | Err boundary result |
| concrete adapters | shells/rag_api_shell.py |
parse CSV, serialize JSONL, and translate expected I/O failures |
| CLI configuration | shells/rag_main.py |
parse arguments and validate policy before execution |
This ownership line matters. full_rag_api_docs accepts documents, not paths. It
does not open files, catch filesystem exceptions, or decide where output belongs.
A capability is narrower than an adapter¶
The core configuration module declares only what the path API needs:
FSReader is one implementation. A test reader can return a fixed Ok or Err
without touching the filesystem. Both satisfy the same capability, so
full_rag_api_path does not need a branch for “test mode.”
failure = Err("source unavailable")
class FailingReader:
def read_docs(self, path: str):
assert path == "missing.jsonl"
return failure
When this reader is placed in RagBoundaryDeps, the returned value is exactly
failure. No cleaner, chunker, or embedder is needed to explain that result.
This is the Module 02 meaning of dependency injection: pass a small value that implements a required operation. It does not require a framework or global registry.
Short-circuiting is part of the contract¶
The path API has one decision:
docs_result = deps.reader.read_docs(path)
if isinstance(docs_result, Err):
return docs_result
chunks, observations = full_rag_api_docs(
docs_result.value,
config,
deps.core,
)
return Ok((chunks, observations))
An Err stops the composition. An Ok unwraps validated documents and crosses
into the pure API. result_map and result_and_then encode the same rule for
small compositions: their function argument is never called for Err.
The focused API proof makes the skipped call observable in
test_reader_failure_short_circuits_the_pure_rag_core and
test_result_combinators_do_not_run_after_boundary_failure.
The real filesystem shell adds a writer¶
shells/rag_api_shell.py owns the complete effect sequence:
FSReader.read_docsopens and parses CSV intoRawDocvalues.full_rag_apitransforms those values and returns chunks plus observations.write_chunks_jsonlserializes the chunks.runreturnsOk(observations)after a successful write.
Failure timing is observable:
| Condition | Core runs? | Output state | Outcome |
|---|---|---|---|
| missing input | no | output is not created | load Err |
| malformed CSV row | no | output is not created | load Err |
| header-only CSV | yes, with no documents | empty file | Ok with zero counts |
| valid CSV and output | yes | JSONL equals pure API chunks | Ok with observations |
| unavailable output parent | yes | output is not created | write Err |
The write failure occurs after core work because output feasibility is discovered when the writer opens the target. The module makes that order explicit; it does not claim transactional or atomic output.
Expected failures and broken invariants are different¶
Module 02 uses Err(str) for expected edge failures. It does not convert every
exception into an ordinary result.
| Situation | Treatment in this state | Reason |
|---|---|---|
| file missing, unreadable, or malformed | Err("Load failed: ...") |
caller can reasonably handle it |
| output cannot be opened or written | Err("Write failed: ...") |
caller can choose another destination |
untyped chunk size is True, zero, or negative |
configuration Err |
reject boundary data before construction |
| unknown cleaning rule from CLI | configuration Err |
reject policy before dependency lookup |
| internally produced impossible chunk offsets | exception | signals a violated program invariant |
| interruption or memory exhaustion | not normalized | not an ordinary domain outcome |
RagEnv is still allowed to defend its own invariant. The boundary parser must
check untyped values first so routine user error does not reach that constructor as
an exception.
Module 04 introduces richer failure modelling. Do not read a typed error taxonomy,
retry strategy, or recovery policy into this minimal Result.
What the two proof styles establish¶
The injected-reader test proves control flow: reader failure is returned unchanged
and later work is skipped. It cannot prove that open, csv.DictReader, or
json.dump are wired correctly.
The real-filesystem tests prove adapter behavior using temporary paths:
- valid input matches
full_rag_api_docsafter JSON serialization; - the source file remains unchanged;
- empty, malformed, missing, and unwritable routes have distinct outcomes;
- no output exists after a load failure.
Run both proof owners from the repository root:
make PROGRAM=python-programming/python-functional-programming \
capstone-data-api-proof
make PROGRAM=python-programming/python-functional-programming \
capstone-data-shell-proof
The first route executes
test_module_02_data_first_apis.py; the second executes
test_module_02_filesystem_boundary.py. A passing fake-boundary test is not a
substitute for the second route.
Review the boundary without guessing¶
For any proposed change, trace these questions in order:
- Does the function accept paths or already-validated domain values?
- Which exact capability or adapter owns the effect?
- Can an expected failure be returned before core work starts?
- If core work has started, which effects may already have happened?
- Which focused test observes the success value, failure value, and output state?
- What stronger property is deliberately deferred?
A boundary is sealed when those answers are visible in signatures, source ownership, and executable evidence—not merely asserted in prose.
Limits of the Module 02 boundary¶
This state does not guarantee atomic replacement, cleanup of a partially written file, bounded-memory CSV loading, retry policy, typed error categories, async cancellation, or resource acquisition protocols. Those omissions are real design boundaries, not hidden accomplishments.
Continue with Configuration as Data to see how the CLI and other untyped callers validate policy before selecting dependencies.