{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"lib/","title":"hexa","text":""},{"location":"lib/#hexa","title":"hexa","text":"
Hexa: Type-declared dependency injection for Python.
"},{"location":"lib/#hexa-classes","title":"Classes","text":""},{"location":"lib/#hexa.MethodSpec","title":"MethodSpecdataclass","text":"MethodSpec(\n name: str,\n args: list[str] = list(),\n kwargs: dict[str, str] = dict(),\n)\n Specification of a single abstract method.
"},{"location":"lib/#hexa.PortNode","title":"PortNodedataclass","text":"PortNode(\n name: str,\n port_cls: str,\n kind: str = \"leaf\",\n attributes: dict[str, ConfigValue] = dict(),\n children: list[PortNode] = list(),\n methods: dict[str, MethodSpec] = dict(),\n)\n A node in the hexa port tree.
"},{"location":"lib/#hexa.PortNode--parameters","title":"Parameters","text":"name : str Slot name, e.g. \"parser\", \"amount_balance\". kind : str One of \"leaf\", \"port\", or \"parent\". - \"leaf\": no children (e.g. NumberPort) - \"port\": has children (e.g. TransactionParserPort) - \"parent\": the root of the tree (e.g. ExtractionPipeline) port_cls : str ABC class name that gives this port its shape, e.g. \"TransactionParserPort\". Every port has a concrete typed shape. attributes : dict[str, ConfigValue] Annotated attributes: name -> type_expr for scalars, or a nested dict of sub-attributes (a struct) for grouped/structured values. children : list[PortNode] Nested port slots. methods : dict[str, MethodSpec] Abstract methods keyed by method name.
diff(other: PortNode, path: str = '') -> list[str]\n Return a list of human-readable difference strings.
An empty list means the trees are equal.
"},{"location":"lib/#hexa.PortNode.find","title":"find","text":"find(name: str) -> PortNode | None\n Find a direct child by slot name.
"},{"location":"lib/#hexa.PortNode.walk","title":"walk","text":"walk() -> list[PortNode]\n Return all nodes in depth-first order (self first).
"},{"location":"lib/#hexa-functions","title":"Functions","text":""},{"location":"lib/#hexa.build","title":"build","text":"build(\n cls: type[T],\n *,\n instances: dict[str, Any] | None = None,\n config: dict[str, Any] | None = None\n) -> T\n Recursively instantiate the port tree from annotations.
"},{"location":"lib/#hexa.build--parameters","title":"Parameters","text":"cls : type The root class to build (e.g. AxisExtractionPipeline). instances : dict, optional {slot_name: object} injected verbatim into every node whose annotations contain that name (runtime values \u2014 repos, handlers, clients \u2014 that must not be re-constructed). Applied to the whole tree by name. config : dict, optional {field_name: value} propagated by name to every node's annotated config fields (non-port attributes), replacing the default None.
instance A fully wired instance of cls and all its nested ports.
Verify that two hexa representations agree.
"},{"location":"lib/check_matches/#hexa.check_matches-classes","title":"Classes","text":""},{"location":"lib/check_matches/#hexa.check_matches-functions","title":"Functions","text":""},{"location":"lib/check_matches/#hexa.check_matches.check_matches","title":"check_matches","text":"check_matches(\n path_a: str | Path, path_b: str | Path\n) -> bool | list[str]\n Verify two representations agree.
"},{"location":"lib/check_matches/#hexa.check_matches.check_matches--parameters","title":"Parameters","text":"path_a : str | Path Path to a .yaml or .py file. path_b : str | Path Path to a .yaml or .py file.
"},{"location":"lib/check_matches/#hexa.check_matches.check_matches--returns","title":"Returns","text":"bool | list[str] True if they match exactly, otherwise a list of difference strings.
"},{"location":"lib/cli/","title":"Cli","text":""},{"location":"lib/cli/#hexa.cli","title":"hexa.cli","text":"Command-line interface for hexa utilities.
"},{"location":"lib/cli/#hexa.cli-functions","title":"Functions","text":""},{"location":"lib/container/","title":"Container","text":""},{"location":"lib/container/#hexa.container","title":"hexa.container","text":"Container to build a wired hexa pipeline from annotations.
"},{"location":"lib/container/#hexa.container-functions","title":"Functions","text":""},{"location":"lib/container/#hexa.container.build","title":"build","text":"build(\n cls: type[T],\n *,\n instances: dict[str, Any] | None = None,\n config: dict[str, Any] | None = None\n) -> T\n Recursively instantiate the port tree from annotations.
"},{"location":"lib/container/#hexa.container.build--parameters","title":"Parameters","text":"cls : type The root class to build (e.g. AxisExtractionPipeline). instances : dict, optional {slot_name: object} injected verbatim into every node whose annotations contain that name (runtime values \u2014 repos, handlers, clients \u2014 that must not be re-constructed). Applied to the whole tree by name. config : dict, optional {field_name: value} propagated by name to every node's annotated config fields (non-port attributes), replacing the default None.
instance A fully wired instance of cls and all its nested ports.
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.
"},{"location":"lib/design/#file-map-samplesextraction_pipeline","title":"File map (samples/extraction_pipeline/)","text":"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 \u2014 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."},{"location":"lib/design/#the-port-tree-abc-contract","title":"The port tree (ABC contract)","text":"ExtractionPipeline\n\u251c\u2500\u2500 source / pipeline / trust_fallback [attributes; source is a nested struct]\n\u251c\u2500\u2500 ingest \u2192 IngestPort (source \u2192 content)\n\u251c\u2500\u2500 raw_lines \u2192 RawLinesPort (content \u2192 raw_lines)\n\u251c\u2500\u2500 txn_blocks \u2192 TxnBlocksPort (raw_lines \u2192 txn_blocks)\n\u2502 \u251c\u2500\u2500 bucket_finder \u2192 BucketFinderPort\n\u2502 \u2514\u2500\u2500 splitter \u2192 SplitterPort\n\u251c\u2500\u2500 txn_dicts \u2192 TxnDictsPort (txn_blocks \u2192 txn_dicts)\n\u2502 \u2514\u2500\u2500 parser \u2192 TransactionParserPort\n\u2502 \u251c\u2500\u2500 number \u2192 NumberPort\n\u2502 \u251c\u2500\u2500 date \u2192 DatePort\n\u2502 \u251c\u2500\u2500 desc \u2192 DescPort\n\u2502 \u2514\u2500\u2500 amount_balance \u2192 AmountBalancePort\n\u2502 \u251c\u2500\u2500 number \u2192 NumberPort\n\u2502 \u2514\u2500\u2500 ambiguity \u2192 AmbiguityPort\n\u2514\u2500\u2500 raw_expense \u2192 RawExpensePort (txn_dicts \u2192 list[RawExpense], terminal)\n"},{"location":"lib/design/#generic-default-impl","title":"Generic default (Impl*)","text":"ImplExtractionPipeline pins every stage slot to a concrete Impl* port:
ImplExtractionPipeline\n\u251c\u2500\u2500 ingest: ImplIngestPort\n\u251c\u2500\u2500 raw_lines: ImplRawLinesPort\n\u251c\u2500\u2500 txn_blocks: ImplTxnBlocksPort\n\u2502 \u251c\u2500\u2500 bucket_finder: ImplBucketFinderPort\n\u2502 \u2514\u2500\u2500 splitter: ImplSplitterPort\n\u251c\u2500\u2500 txn_dicts: ImplTxnDictsPort\n\u2502 \u2514\u2500\u2500 parser: ImplTransactionParserPort\n\u2502 \u251c\u2500\u2500 number: ImplNumberPort\n\u2502 \u251c\u2500\u2500 date: ImplDatePort\n\u2502 \u251c\u2500\u2500 desc: ImplDescPort\n\u2502 \u2514\u2500\u2500 amount_balance: ImplAmountBalancePort\n\u2502 \u251c\u2500\u2500 number: ImplNumberPort\n\u2502 \u2514\u2500\u2500 ambiguity: ImplAmbiguityPort\n\u2514\u2500\u2500 raw_expense: ImplRawExpensePort\n"},{"location":"lib/design/#bank-variation-only-what-differs","title":"Bank variation (only what differs)","text":"The root class re-pins only txn_dicts. Everything else inherits Impl*.
banks/axis/pdf.py)","text":"class AxisExtractionPipeline(ImplExtractionPipeline):\n txn_dicts: AxisTxnDictsPort\n AxisTxnDictsPort(ImplTxnDictsPort)\n \u2514\u2500 parser: AxisTransactionParserPort(ImplTransactionParserPort)\n \u251c\u2500 number: AxisNumberPort # NUMBER_RE, all clean numbers\n \u251c\u2500 desc: AxisDescPort # STARTTERS = [upi/, imps/, neft-, neft/, ach/, ach-, 2a/]\n \u2514\u2500 amount_balance: AxisAmountBalancePort(ImplAmountBalancePort)\n \u2514\u2500 number: AxisNumberPort # re-pin back to axis number port\n Notes: - date is not redeclared on AxisTransactionParserPort \u2014 it inherits ImplDatePort (correct already, adds no behavior). - amount_balance.ambiguity is not redeclared \u2014 inherits ImplAmbiguityPort.
banks/icici/pdf.py)","text":"class IciciExtractionPipeline(ImplExtractionPipeline):\n txn_dicts: IciciTxnDictsPort\n IciciTxnDictsPort(ImplTxnDictsPort)\n \u2514\u2500 parser: IciciTransactionParserPort(ImplTransactionParserPort)\n \u251c\u2500 number: IciciNumberPort # NUMBER_RE, returns decimals[-2:] (last 2)\n \u251c\u2500 desc: IciciDescPort # STARTTERS + fix_ocr (UPl\u2192UPI, ..., ICIC]\u2192ICICI)\n \u2514\u2500 amount_balance: IciciAmountBalancePort(ImplAmountBalancePort)\n \u251c\u2500 number: IciciNumberPort # re-pin to icici number port\n \u251c\u2500 adjust_balance: # neg-markers [(-), (-}, {-}, {-) , ()] \u2192 -abs(balance)\n \u2514\u2500 missing_number_candidates: # Icici-only one-number hook\n"},{"location":"lib/design/#redundancy-rule-applied-in-both-banks","title":"Redundancy rule (applied in both banks)","text":"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: ImplDatePort was previously redeclared redundantly and has been removed \u2014 it added mental load on the extender for zero behavior change. (Fidelity audit: it is still concrete ImplDatePort in ImplTransactionParserPort, so behavior is unchanged.)
build(cls) in hexa/container.py)","text":"The annotations are metadata; a small reflection builder turns a root class into a wired instance:
build(AxisExtractionPipeline) \u2192 wired AxisExtractionPipeline\n Algorithm (build(cls)):
cls.__annotations__, merged across the MRO so inherited slot annotations are visible (a bank subclass only adds/overrides a slot; the rest come from Impl*).Impl*/bank class (not an ABC, not a builtin), instantiate it.setattr(parent, slot, child) to wire each slot onto the parent instance.Selection is fully static: the annotated type is the chosen implementation. The container holds no per-bank branch logic \u2014 Axis vs Icici is decided purely by which root class you call build on.
A leaf-stage port exposes one entry method; stages chain data through the shared run(source) signature on the root:
run(source)\n ingest.materialize(source) \u2192 content\n raw_lines.extract(content) \u2192 raw_lines\n txn_blocks.extract(raw_lines) \u2192 {opening_balance: [blocks]}\n txn_dicts.extract(txn_blocks) \u2192 [txn dicts]\n raw_expense.build(txn_dicts) \u2192 [RawExpense] (terminal)\n 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.
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 \u2014 they are never required. A user is free to:
The utilities only ever translate between two interchangeable representations of the same contract \u2014 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).
All utilities serialize through one neutral model, a PortNode tree. It is NOT tied to the extraction sample \u2014 any port tree fits.
@dataclass\nclass PortNode:\n name: str # slot name, e.g. \"parser\", \"amount_balance\"\n kind: str # \"leaf\" | \"port\" | \"parent\"\n port_cls: str # ABC class name, e.g. \"TransactionParserPort\"\n attributes: dict[str, ConfigValue] # annotated attributes: name -> type expr,\n # or a nested struct dict for grouped values\n children: list[\"PortNode\"]\n methods: dict[str, MethodSpec] # abstract methods\n"},{"location":"lib/design/#1-parse_yamlpath-portnode","title":"1. parse_yaml(path) -> PortNode","text":"Read a YAML spec into the PortNode tree (YAML \u2192 model).
extraction_pipeline.yaml): a root class whose inline attributes (scalars and nested structs), port slots, and reserved methods: recurse into child ports.ValueError with file/line on structural errors.generate_abc(path_yaml, out_path=None) -> str","text":"Generate _abc.py source from a YAML spec (YAML \u2192 ABC).
parse_yaml(...) \u2192 write one class <PortCls>(ABC) per PortNode with attribute annotations (nested structs become synthesized @dataclasses), port-slot annotations, and @abstractmethod stubs, matching the style of the hand-written extraction_pipeline_abc.py.out_path optional; always returns the generated source.parse_abc(module_or_path) -> PortNode","text":"Reflect over an ABC module into the PortNode tree (ABC \u2192 model).
__annotations__ / @abstractmethod from generic ABC classes (no sample assumptions) to reconstruct the same PortNode tree.generate_yaml(module_or_path, out_path=None) -> str","text":"Generate a .yaml spec from an ABC module (ABC \u2192 YAML).
parse_abc(...) \u2192 write the reverse of generate_abc.check_matches(path_a, path_b) -> bool","text":"Verify two representations agree \u2014 YAML vs ABC, either direction.
PortNode models (parse_yaml + parse_abc) and compares: classes exist and are ABCs, attributes match, port slots match, @abstractmethod signatures match (missing keyword-only params treated conservatively).True only if the whole tree matches; otherwise False (or a diff).# YAML-first\nedit spec.yaml\n \u2192 generate_abc(spec.yaml, abc.py) # regenerate ABCs\n \u2192 hand-write/update Impl* + banks\n \u2192 check_matches(spec.yaml, abc.py) == True # guard\n\n# ABC-first\nedit abc.py\n \u2192 generate_yaml(abc.py, spec.yaml) # emit YAML spec\n \u2192 check_matches(spec.yaml, abc.py) == True\n\n# hand-written both (no utilities at all)\nedit abc.py + spec.yaml independently # optional check_matches as a guard\n Impl*/bank files are always hand-extended on top and are never generated.
Generate _abc.py source from a hexa PortNode tree.
"},{"location":"lib/generate_abc/#hexa.generate_abc-classes","title":"Classes","text":""},{"location":"lib/generate_abc/#hexa.generate_abc-functions","title":"Functions","text":""},{"location":"lib/generate_abc/#hexa.generate_abc.generate_abc","title":"generate_abc","text":"generate_abc(\n node: PortNode, out_path: str | Path | None = None\n) -> str\n Generate ABC Python source from a :class:PortNode tree.
node : PortNode The root of the parsed YAML (or generated) tree. out_path : str | Path | None Optional file path to write the generated source to.
"},{"location":"lib/generate_abc/#hexa.generate_abc.generate_abc--returns","title":"Returns","text":"str The generated Python source code.
"},{"location":"lib/generate_yaml/","title":"Generate Yaml","text":""},{"location":"lib/generate_yaml/#hexa.generate_yaml","title":"hexa.generate_yaml","text":"Generate YAML spec (nested mapping form) from a hexa PortNode tree.
"},{"location":"lib/generate_yaml/#hexa.generate_yaml-classes","title":"Classes","text":""},{"location":"lib/generate_yaml/#hexa.generate_yaml-functions","title":"Functions","text":""},{"location":"lib/generate_yaml/#hexa.generate_yaml.generate_yaml","title":"generate_yaml","text":"generate_yaml(\n node: PortNode, out_path: str | Path | None = None\n) -> str\n Generate YAML source from a :class:PortNode tree.
node : PortNode The root of the tree. out_path : str | Path | None Optional file path to write the generated source to.
"},{"location":"lib/generate_yaml/#hexa.generate_yaml.generate_yaml--returns","title":"Returns","text":"str The generated YAML source.
"},{"location":"lib/model/","title":"Model","text":""},{"location":"lib/model/#hexa.model","title":"hexa.model","text":"Shared neutral model for hexa port trees.
All utilities (parse_yaml, generate_abc, parse_abc, generate_yaml, check_matches) serialize through this single model. It is NOT tied to any sample \u2014 any port tree fits.
dataclass","text":"MethodSpec(\n name: str,\n args: list[str] = list(),\n kwargs: dict[str, str] = dict(),\n)\n Specification of a single abstract method.
"},{"location":"lib/model/#hexa.model.PortNode","title":"PortNodedataclass","text":"PortNode(\n name: str,\n port_cls: str,\n kind: str = \"leaf\",\n attributes: dict[str, ConfigValue] = dict(),\n children: list[PortNode] = list(),\n methods: dict[str, MethodSpec] = dict(),\n)\n A node in the hexa port tree.
"},{"location":"lib/model/#hexa.model.PortNode--parameters","title":"Parameters","text":"name : str Slot name, e.g. \"parser\", \"amount_balance\". kind : str One of \"leaf\", \"port\", or \"parent\". - \"leaf\": no children (e.g. NumberPort) - \"port\": has children (e.g. TransactionParserPort) - \"parent\": the root of the tree (e.g. ExtractionPipeline) port_cls : str ABC class name that gives this port its shape, e.g. \"TransactionParserPort\". Every port has a concrete typed shape. attributes : dict[str, ConfigValue] Annotated attributes: name -> type_expr for scalars, or a nested dict of sub-attributes (a struct) for grouped/structured values. children : list[PortNode] Nested port slots. methods : dict[str, MethodSpec] Abstract methods keyed by method name.
diff(other: PortNode, path: str = '') -> list[str]\n Return a list of human-readable difference strings.
An empty list means the trees are equal.
"},{"location":"lib/model/#hexa.model.PortNode.find","title":"find","text":"find(name: str) -> PortNode | None\n Find a direct child by slot name.
"},{"location":"lib/model/#hexa.model.PortNode.walk","title":"walk","text":"walk() -> list[PortNode]\n Return all nodes in depth-first order (self first).
"},{"location":"lib/parse_abc/","title":"Parse Abc","text":""},{"location":"lib/parse_abc/#hexa.parse_abc","title":"hexa.parse_abc","text":"Parse an ABC Python module into a hexa PortNode tree.
"},{"location":"lib/parse_abc/#hexa.parse_abc-classes","title":"Classes","text":""},{"location":"lib/parse_abc/#hexa.parse_abc-functions","title":"Functions","text":""},{"location":"lib/parse_abc/#hexa.parse_abc.parse_abc","title":"parse_abc","text":"parse_abc(module_or_path: str | Path | Any) -> PortNode\n Parse an ABC Python module into a :class:PortNode tree.
ABC classes become port nodes. A nested-struct attribute is expressed as a @dataclass annotation on an ABC port (e.g. source: ExtractionSource where ExtractionSource is a @dataclass); the dataclass's fields are expanded into a nested attributes entry.
module_or_path : str | Path | Any A file path to a .py file, a module import string, or a loaded module.
PortNode The root node of the tree.
"},{"location":"lib/parse_yaml/","title":"Parse Yaml","text":""},{"location":"lib/parse_yaml/#hexa.parse_yaml","title":"hexa.parse_yaml","text":"Parse a hexa YAML spec into a :class:PortNode tree.
The YAML grammar uses a nested mapping format built from three member kinds:
name: type scalar (e.g. source_type: str)name: mapping of sub-attributes (e.g. source:)name: mapping whose single key is a class whose value is a body, or a class key with a ... body. A port carries a class shape.Example::
RootClassName:\n attr: type # scalar attribute\n source: # nested struct (all scalar sub-attributes)\n source_type: str\n bank: str\n methods: # reserved block of functions\n run:\n args: [a, b]\n ingest: # port (single non-scalar class key)\n IngestPort:\n source_type: str\n methods: {Method: ...}\n Disambiguation of a mapping value A mapping with exactly one key whose value is a non-scalar body (a mapping, ..., or a list) is a port. Any other mapping (multiple keys, or all scalar-typed values) is a nested struct of attributes.
See samples/minimal/sample.yaml and samples/extraction_pipeline/extraction_pipeline.yaml for examples.
parse_yaml(path: str | Path) -> PortNode\n Parse a hexa YAML spec file into a :class:PortNode tree.
path : str | Path Path to the .yaml file.
PortNode The root node of the parsed tree.
"},{"location":"lib/parse_yaml/#hexa.parse_yaml.parse_yaml--raises","title":"Raises","text":"ValueError On structural errors in the YAML.
"},{"location":"lib/philosophy/","title":"hexa \u2014 Philosophy","text":"This document explains the why behind the type-declared dependency pattern used for the extraction pipeline sample \u2014 specifically the Axis/Icici bank variation. It is, together with design.md, self-sufficient: a fresh agent should be able to pick up the codebase from here and reason about (and extend) the pattern without needing the original session context.
"},{"location":"lib/philosophy/#one-idea","title":"One idea","text":"A concrete class's annotated slot type is the dependency decision.
Bank variation is expressed entirely as class annotations \u2014 never as imperative wiring, never as __init__ parameters, never as a composition-root that assembles objects by hand. The tree of ports a pipeline needs is declared once, and a bank differs from the generic pipeline only by which concrete types are pinned onto which slots.
The pattern is built from three fixed layers. Understanding which layer something belongs to is the whole mental model.
"},{"location":"lib/philosophy/#layer-1-abc-contracts-samplesextraction_pipelineextraction_pipeline_abcpy","title":"Layer 1 \u2014 ABC contracts (samples/extraction_pipeline/extraction_pipeline_abc.py)","text":"Authoritative declaration of need (\"WHAT a stage requires\").
ABC.TransactionParserPort.min_numbers: int = 3), optionally grouped as a nested struct via a @dataclass (e.g. source: ExtractionSource).TransactionParserPort.number: NumberPort, amount_balance: AmountBalancePort).@abstractmethod bodies \u2014 the method signatures each concrete implementation must provide.The ABC never runs, never holds an instance, and never says which implementation to use. It only says what the shape is.
"},{"location":"lib/philosophy/#layer-2-generic-concurrent-implementations-samplesextraction_pipelineextraction_pipeline_implpy","title":"Layer 2 \u2014 generic concurrent implementations (samples/extraction_pipeline/extraction_pipeline_impl.py)","text":"The common default behavior (\"Imple \u2014 the shared adaptor\").
class ImplTransactionParserPort(TransactionParserPort) subclasses the ABC, implements every @abstractmethod, and re-pins its port slots to concrete Impl* types: number: ImplNumberPort, desc: ImplDescPort, amount_balance: ImplAmountBalancePort.ImplAmountBalancePort in turn pins number: ImplNumberPort and ambiguity: ImplAmbiguityPort.ImplExtractionPipeline, pins its five stage slots to Impl* ports: ingest, raw_lines, txn_blocks, txn_dicts, raw_expense.Impl* is the default that shared, bank-agnostic behavior lives in. Any slot a bank does not override falls back to Impl* at runtime.
samples/extraction_pipeline/banks/{axis,icici}/pdf.py, samples/extraction_pipeline/extraction_pipeline_banks.py)","text":"The variation (\"what differs per bank\").
Impl* classes, never the ABC \u2014 they extend, they never reinvent the contract.Axis*/Icici* class overrides only the members that differ from the generic Impl*; everything shared is inherited.ImplExtractionPipeline that re-pins only the slots that differ:class AxisExtractionPipeline(ImplExtractionPipeline):\n txn_dicts: AxisTxnDictsPort\n"},{"location":"lib/philosophy/#the-re-pinning-cascade","title":"The re-pinning cascade","text":"Bank-specific wiring is hierarchical and cascading. Each level narrows exactly one slot, and its narrowed type drags the next level's narrowing along with it:
AxisExtractionPipeline\n \u2514\u2500 txn_dicts: AxisTxnDictsPort (only override on the root)\n \u2514\u2500 parser: AxisTransactionParserPort\n \u251c\u2500 number: AxisNumberPort (get_numbers \u2192 all clean nums)\n \u251c\u2500 desc: AxisDescPort (STARTTERS)\n \u2514\u2500 amount_balance: AxisAmountBalancePort\n \u2514\u2500 number: AxisNumberPort (re-pin: same bank's number port)\n Siblings that are already correct in Impl* are inherited, not redeclared. E.g. the parser's date slot comes from ImplTransactionParserPort; the amount-balance port's ambiguity comes from ImplAmountBalancePort. Re-declaring them adds mental load on the extender with zero behavior change.
Rule \u2014 only redeclare the slot you are actually changing. If a slot's behavior is already correct from Impl*, leave it out.
ingest, raw_lines, txn_blocks, raw_expense, parser date, and ambiguity are bank-agnostic. Leaving them at the Impl* default is deliberate, not an omission.Axis*/Icici* only extend Impl*. The ABC contract in samples/extraction_pipeline/extraction_pipeline_abc.py is defined once and shared by every bank.build_axis_pipeline() wiring objects by hand.Impl* classes have no __init__; wiring is done by assigning concrete port instances onto annotated slots..yaml spec and the _abc.py module are two representations of the same contract. hexa is agnostic of the extraction pipeline sample and never imposes a workflow: a user can hand-write the ABC file, start from YAML (ABC generated), start from an ABC file (YAML emitted), or use both as a spec. The optional utilities in design.md \u00a7 Utilities translate between them and can verify they agree \u2014 but the bank Impl*/specialization files are always hand-written (their slots are a manual choice) and never generated.The annotations are the decision; hexa.build(cls) (see design.md \u00a7 Container) turns a root class into a wired instance by reading __annotations__ + MRO, recursively instantiating each pinned concrete type, and setattring the child onto the parent slot. This document is scoped to the notation \u2014 the rest of the tree can be expressed purely as class annotations and filled at runtime by the container.