hexa — Design (extraction pipeline, Axis/Icici)
Note: The extraction pipeline discussed below is a sample/example illustrating the hexa composition pattern.
This document is the concrete counterpart to philosophy.md: where the files are, what the tree looks like for both banks, and how a future container instantiates it. Self-sufficient for a fresh agent.
File map (samples/extraction_pipeline/)
| File | Layer | Role |
|---|---|---|
extraction_pipeline_abc.py |
ABC contracts | All port ABCs + ExtractionPipeline root. Declares attributes, the ExtractionSource dataclass, port slots, @abstractmethods. |
extraction_pipeline_impl.py |
Generic Impl* |
Concrete default implementations of every port; ImplExtractionPipeline root; clean_num helper. |
extraction_pipeline_ambiguity.py |
Shared logic | Ported ResolutionContext, AmbiguityHandler, AmountBalanceNotFound, validate_signs — used by ImplAmountBalancePort. |
extraction_pipeline_banks.py |
Composition root | AxisExtractionPipeline(ImplExtractionPipeline) + IciciExtractionPipeline (each re-pins txn_dicts). |
banks/axis/pdf.py |
Bank specialization | AxisNumberPort, AxisDescPort, AxisAmountBalancePort, AxisTransactionParserPort, AxisTxnDictsPort. |
banks/icici/pdf.py |
Bank specialization | Icici* mirror of the above + Icici-only adjust_balance/missing_number_candidates. |
extraction_pipeline.yaml |
Contract spec | Declarative spec of the port tree; interchangeable with _abc.py via the optional utilities. |
The port tree (ABC contract)
Generic default (Impl*)
ImplExtractionPipeline pins every stage slot to a concrete Impl* port:
Bank variation (only what differs)
The root class re-pins only txn_dicts. Everything else inherits Impl*.
Axis (banks/axis/pdf.py)
Notes:
- date is not redeclared on AxisTransactionParserPort — it inherits
ImplDatePort (correct already, adds no behavior).
- amount_balance.ambiguity is not redeclared — inherits ImplAmbiguityPort.
Icici (banks/icici/pdf.py)
Redundancy rule (applied in both banks)
Only redeclare a slot you are actually changing from Impl*. If a slot's
behavior is already correct from the parent Impl*, omit it. This keeps the
diff between a bank and Impl* minimal and unambiguous.
date: ImplDatePortwas previously redeclared redundantly and has been removed — it added mental load on the extender for zero behavior change. (Fidelity audit: it is still concreteImplDatePortinImplTransactionParserPort, so behavior is unchanged.)
Container (build(cls) in hexa/container.py)
The annotations are metadata; a small reflection builder turns a root class into a wired instance:
Algorithm (build(cls)):
- Walk
cls.__annotations__, merged across the MRO so inherited slot annotations are visible (a bank subclass only adds/overrides a slot; the rest come fromImpl*). - For each port slot whose annotated type is a concrete
Impl*/bank class (not an ABC, not a builtin), instantiate it. - Recurse into that child — repeat until leaves (classes that declare no further port slots).
setattr(parent, slot, child)to wire each slot onto the parent instance.- Apply config defaults from the class-level annotated attributes.
Selection is fully static: the annotated type is the chosen implementation.
The container holds no per-bank branch logic — Axis vs Icici is decided purely by
which root class you call build on.
Control flow at runtime (target)
A leaf-stage port exposes one entry method; stages chain data through the shared
run(source) signature on the root:
The stage ports map naturally onto dagpipe Nodes (ingest, raw_lines,
txn_blocks, txn_dicts, raw_expense), each node's resolve delegating to its
wired port subtree, connected as a dagpipe Graph and run via Engine. That merge
is a separate, future step.
Utilities (optional, hexa-agnostic)
The hexa utilities are implemented in hexa/ (the package root) and they are agnostic of the extraction pipeline sample. It ships a set of optional code-gen / validation utilities so users can choose their own workflow — they are never required. A user is free to:
- hand-write an ABC file and never touch YAML, or
- start from YAML and generate the ABC file, or
- start from an ABC file and emit YAML for it, or
- use the ABC/YAML only as a spec (neither generated from the other).
The utilities only ever translate between two interchangeable representations of
the same contract — the .yaml spec and the _abc.py module. They never
generate or manage the Impl*/bank files (those carry behavior and slot choice,
which is a human decision).
Shared model
All utilities serialize through one neutral model, a PortNode tree. It is NOT
tied to the extraction sample — any port tree fits.
1. parse_yaml(path) -> PortNode
Read a YAML spec into the PortNode tree (YAML → model).
- Parses completely (structural validation only, no type-checking).
- Supports the sample grammar (see
extraction_pipeline.yaml): a root class whose inline attributes (scalars and nested structs), port slots, and reservedmethods:recurse into child ports. - Raises
ValueErrorwith file/line on structural errors.
2. generate_abc(path_yaml, out_path=None) -> str
Generate _abc.py source from a YAML spec (YAML → ABC).
parse_yaml(...)→ write oneclass <PortCls>(ABC)perPortNodewith attribute annotations (nested structs become synthesized@dataclasses), port-slot annotations, and@abstractmethodstubs, matching the style of the hand-writtenextraction_pipeline_abc.py.- Deterministic (YAML-order) output so it is idempotent / diff-friendly.
out_pathoptional; always returns the generated source.
3. parse_abc(module_or_path) -> PortNode
Reflect over an ABC module into the PortNode tree (ABC → model).
- Reads
__annotations__/@abstractmethodfrom generic ABC classes (no sample assumptions) to reconstruct the samePortNodetree.
4. generate_yaml(module_or_path, out_path=None) -> str
Generate a .yaml spec from an ABC module (ABC → YAML).
parse_abc(...)→ write the reverse ofgenerate_abc.- Lets a user author the ABC file first and emit YAML for it.
5. check_matches(path_a, path_b) -> bool
Verify two representations agree — YAML vs ABC, either direction.
- Builds both
PortNodemodels (parse_yaml+parse_abc) and compares: classes exist and areABCs, attributes match, port slots match,@abstractmethodsignatures match (missing keyword-only params treated conservatively). - Returns
Trueonly if the whole tree matches; otherwiseFalse(or a diff).
Workflows (user's choice)
Impl*/bank files are always hand-extended on top and are never generated.