recon_gen.common.l2.primitives
LAYER 2 institutional-model primitives, typed 1:1 against SPEC.md.
This module is the single source of truth for what an L2 instance contains in memory. The YAML loader (M.1.2) deserializes into these types; the validator (M.1.3) enforces the SPEC’s load-time rules on top; the SQL emitter (M.1.4) walks them; downstream apps (M.2-M.6) consume them.
Notation matches SPEC: every dataclass mirrors a SPEC primitive’s tuple shape exactly, with PascalCase types + snake_case field names. Frozen + slotted to prevent surprise mutation and typo’d attribute access.
Per F2 (M.0.13 iteration gate): Rail is a discriminated union of
TwoLegRail / SingleLegRail — pyright catches “leg_role on a
two-leg rail” at the construction site instead of at validation time.
The aggregating-rail flags (aggregating / bundles_activity /
cadence) live as optional fields on either shape, since the SPEC
allows aggregating rails to be one-leg or two-leg.
Per F4: Money values are Decimal; the YAML loader (M.1.2) is
responsible for the Decimal(str(value)) coercion that dodges YAML
float precision.
Z.C (2026-05-15) — the legacy L2Instance.instance field has been
dropped. The DB-table prefix (formerly enforced via SPEC F5’s
^[a-z][a-z0-9_]*$/30-char cap on the instance: YAML key) now
lives on the cfg as cfg.db_table_prefix; the same regex/cap is
enforced by common/config.py’s loader at cfg-load time. The
QS-resource-ID prefix lives as cfg.deployment_name (replaces the
former cfg.resource_prefix + cfg.l2_instance_prefix pair).
Per F1 + SPEC’s load-time validation list: every Role referenced by a
Rail or AccountTemplate MUST resolve to either a declared Account
or an AccountTemplate. This module declares the field types; the
validator (M.1.3) walks the resolution graph.
Classes
|
A 1-of-1 account that exists exactly once in the institution. |
|
A class of accounts that exists in many instances at runtime. |
|
A firing rule: one parent + one list of candidate children. |
|
One entry in |
|
AF (E8): a soft per-period firing-COUNT bound on a Rail / Template. |
|
A curated AML / compliance scenario actor for handbook walkthroughs. |
|
A loaded + parsed L2 institutional model. |
|
A daily cap on per-direction flow per (parent role, rail, direction). |
|
A Rail that produces one Transaction leg per firing. |
|
A multi-leg shared Transfer that bundles many Rail firings. |
|
A Rail that produces two Transaction legs (debit + credit) per firing. |
- class recon_gen.common.l2.primitives.Account(id, scope, name=None, role=None, parent_role=None, expected_eod_balance=None, description=None)[source]
Bases:
objectA 1-of-1 account that exists exactly once in the institution.
Per SPEC: singletons that Rails reference by Role; the Role is technically optional but in practice required for any Account a Rail touches (per F1, enforced by the validator at load time).
descriptionis free-form prose (markdown OK) read by handbook + training render templates per the SPEC’s “Description fields” rule. Optional at the type level but SHOULD be filled.- Parameters:
id (Identifier)
scope (Literal['internal', 'external'])
name (Name | None)
role (Identifier | None)
parent_role (Identifier | None)
expected_eod_balance (Decimal | None)
description (str | None)
- description: str | None
- expected_eod_balance: Decimal | None
- id: Identifier
- name: Name | None
- parent_role: Identifier | None
- role: Identifier | None
- scope: Literal['internal', 'external']
- class recon_gen.common.l2.primitives.AccountTemplate(role, scope, parent_role=None, expected_eod_balance=None, description=None, instance_id_template=None, instance_name_template=None)[source]
Bases:
objectA class of accounts that exists in many instances at runtime.
Per SPEC: declares the SHAPE; the specific account instance for a given posting is selected at posting time (typically from
Transaction.Metadata).parent_roleMUST resolve to a singletonAccount(never anotherAccountTemplate) — enforced by the validator at load time per the SPEC’s “singleton parent only” constraint.instance_id_template+instance_name_template(M.4.2b) — optional Python str.format() templates the demo seed’s_materialize_instancesuses when synthesizing per-template instances. Both default toNone; the seed falls back to the legacy synthetic patterns ("cust-{n:03d}"for id,"Customer {n}"for name) so existing L2 fixtures don’t drift. Integrators opt in via YAML to control the persona’s per-template naming, e.g.:instance_id_template: “cust-{n:03d}-bigfoot” instance_name_template: “Bigfoot-{n}”
Both templates support the placeholders
{role}(the template’srolefield) and{n}(1-indexed instance number). Loader rejects format strings that reference any other placeholder.- Parameters:
role (Identifier)
scope (Literal['internal', 'external'])
parent_role (Identifier | None)
expected_eod_balance (Decimal | None)
description (str | None)
instance_id_template (str | None)
instance_name_template (str | None)
- description: str | None
- expected_eod_balance: Decimal | None
- instance_id_template: str | None
- instance_name_template: str | None
- parent_role: Identifier | None
- role: Identifier
- scope: Literal['internal', 'external']
- class recon_gen.common.l2.primitives.Chain(parent, children, description=None)[source]
Bases:
objectA firing rule: one parent + one list of candidate children.
Per SPEC: list cardinality carries the entire firing semantic — singleton ⇒ required (the child SHOULD fire; missing surfaces as an orphan exception); multi ⇒ XOR (exactly one of the listed children SHOULD fire per parent instance). The legacy
required/xor_groupflags collapse intolen(children)(Z.A — locked 2026-05-13).Aggregating rails MUST NOT appear in
children(they don’t have per-Transfer parents — they sweep on cadence). Validator enforces.AB.6 (2026-05-19):
childrenis nowtuple[ChainChildSpec, ...]— the AB.4 chain-levelfan_in+expected_parent_countflags moved per-child to allow mixed-cardinality chains. Loader rejects chain-level fan_in / expected_parent_count with an actionable error pointing at the per-child shape (hard cut per AB.6.0 lock, no deprecation grace window).- Parameters:
parent (Identifier)
children (tuple[ChainChildSpec, ...])
description (str | None)
- children: tuple[ChainChildSpec, ...]
- description: str | None
- parent: Identifier
- class recon_gen.common.l2.primitives.ChainChildSpec(name, fan_in=False, expected_parent_count=None)[source]
Bases:
objectOne entry in
Chain.children— name plus optional fan-in flag.AB.6 (2026-05-19) relocated
fan_in+expected_parent_countfrom chain-level to per-child. Motivation (per SPEC_gap_feedback §5): a single chain may carry mixed-cardinality children — some 1:1 (ACH / wire / check) AND one N:1 (batched payout) — which a single chain-level flag can’t express.nameresolves to either a Rail or a TransferTemplate (same resolution rules R5 / S4 apply per-child entry).fan_in=Truedeclares THIS child is N:1 — N parent firings may share one child Transfer (the batched-payout pattern). Validator requires fan_in children to resolve to TransferTemplates only.expected_parent_count(when set) declares the exact number of parent firings per child Transfer. Set + matview flags exact-mismatch (parent count != expected). Unset + matview falls back to orphan-only detection (parent count < 2). Must be None whenfan_in=False(validator C8b).- Parameters:
name (Identifier)
fan_in (bool)
expected_parent_count (int | None)
- expected_parent_count: int | None
- fan_in: bool
- name: Identifier
- class recon_gen.common.l2.primitives.FiringsTypicalPerPeriod(period, count_range)[source]
Bases:
objectAF (E8): a soft per-period firing-COUNT bound on a Rail / Template.
The complement to AB.5’s
amount_typical_range(per-firing magnitude): this bounds how MANY times the rail/template fires perperiod, institution-wide. The generator samples a count uniform-randomly fromcount_rangeper period when set; absent, it falls back to the per-kind firing-count heuristic. Per-firing count × per-firing amount = realistic per-period aggregates — the dashboard top-line operators scan first.periodis a bounded enum (Period);count_rangeis(min, max)non-negative integers withmin <= max(validator W1a-c). Frozen + slotted to match the rest of the L2 primitives.- Parameters:
period (Literal['business_day', 'pay_period', 'week', 'month'])
count_range (tuple[int, int])
- count_range: tuple[int, int]
- period: Literal['business_day', 'pay_period', 'week', 'month']
- class recon_gen.common.l2.primitives.InvestigationPersona(name, account_id, role)[source]
Bases:
objectA curated AML / compliance scenario actor for handbook walkthroughs.
BXa.1 (2026-05-30): promoted from the hardcoded table that lived inside
common/handbook/vocabulary.py::_sasquatch_pr_vocabulary. The handbook’s Investigation walkthroughs substitute these display names ({{ vocab.demo.investigation.layering_chain[0].name }}etc.) — curated narrative the L2 author writes, not deriveable from L2 topology. Sasquatch fixture carries 6 entries (Juniper Ridge LLC + Cascadia Trust Bank + Cascadia—Operations + Shell Company A/B/C); other operator L2s default to empty tuple and the existing{% if %}gates in the docs hide the walkthroughs that depend on the curated names.rolevalues currently in use:convergence_anchor,counterparty_bank,operations_account,shell_entity. Open enum — handbook template gates on specific role strings.- Parameters:
name (str)
account_id (str)
role (str)
- account_id: str
- name: str
- role: str
- class recon_gen.common.l2.primitives.L2Instance(accounts, account_templates, rails, transfer_templates, chains, limit_schedules, description=None, institution_name=None, institution_acronym=None, role_business_day_offsets=None, theme=None, investigation_personas=())[source]
Bases:
objectA loaded + parsed L2 institutional model.
Z.C (2026-05-15) — the legacy
instancefield has been dropped. The DB-table prefix lives on the cfg ascfg.db_table_prefix; the QS-resource-ID prefix lives ascfg.deployment_name. Each L2 YAML is pure topology + persona + theme; the cfg yaml carries the deployment-specific identifiers.- Parameters:
accounts (tuple[Account, ...])
account_templates (tuple[AccountTemplate, ...])
rails (tuple[TwoLegRail | SingleLegRail, ...])
transfer_templates (tuple[TransferTemplate, ...])
chains (tuple[Chain, ...])
limit_schedules (tuple[LimitSchedule, ...])
description (str | None)
institution_name (str | None)
institution_acronym (str | None)
role_business_day_offsets (dict[str, int] | None)
theme (ThemePreset | None)
investigation_personas (tuple[InvestigationPersona, ...])
- account_templates: tuple[AccountTemplate, ...]
- description: str | None
- institution_acronym: str | None
- institution_name: str | None
- investigation_personas: tuple[InvestigationPersona, ...]
- limit_schedules: tuple[LimitSchedule, ...]
- rails: tuple[TwoLegRail | SingleLegRail, ...]
- role_business_day_offsets: dict[str, int] | None
- theme: ThemePreset | None
- transfer_templates: tuple[TransferTemplate, ...]
- class recon_gen.common.l2.primitives.LimitSchedule(parent_role, rail, cap, direction='Outbound', description=None)[source]
Bases:
objectA daily cap on per-direction flow per (parent role, rail, direction).
Per SPEC: time-invariant in v1. The library projects each entry into the relevant
StoredBalance.Limitsmap; L1’s Limit Breach invariant evaluates per child individually (the cap is per-child, not aggregated across siblings of the parent). Z.B (2026-05-15) renamedtransfer_type→rail— the field now references a Rail name directly, eliminating the templated-leg footgun where a LimitSchedule on transfer_type=<leg_rail_type> failed to fire on transactions tagged with the template’s transfer_type instead. AB.1 (2026-05-19) addeddirection: a single(parent_role, rail)may now carry two schedules — oneOutbound(classic per-rail send cap) and oneInbound(AML / structuring threshold on inbound volume). The validator broadens uniqueness from(parent_role, rail)to(parent_role, rail, direction). DefaultOutboundkeeps every pre-AB.1 YAML byte-equivalent.- Parameters:
parent_role (Identifier)
rail (RailName)
cap (Decimal)
direction (Literal['Outbound', 'Inbound'])
description (str | None)
- cap: Decimal
- description: str | None
- direction: Literal['Outbound', 'Inbound']
- parent_role: Identifier
- rail: RailName
- class recon_gen.common.l2.primitives.SingleLegRail(name, metadata_keys, leg_role, leg_direction, origin=None, posted_requirements=<factory>, max_pending_age=None, max_unbundled_age=None, aggregating=False, bundles_activity=<factory>, cadence=None, description=None, metadata_value_examples=<factory>, amount_typical_range=None, firings_typical_per_period=None)[source]
Bases:
objectA Rail that produces one Transaction leg per firing.
Per SPEC: single-leg rails MUST be reconciled by EITHER a
TransferTemplatewhoseleg_railsincludes this rail OR an aggregating rail whosebundles_activityincludes this rail’sname. A single-leg rail without either reconciliation path is a configuration error (validator catches at load).leg_direction = Variablemeans the leg’s amount AND direction are determined at posting time by a containing TransferTemplate’s ExpectedNet closure requirement. Each TransferTemplate MUST contain at most one Variable-direction leg.Per-leg Origin overrides (
source_origin/destination_origin) are deliberately absent here — they only make sense on a 2-leg rail. The loader rejects them at load if they appear in YAML for a single-leg rail (hard error, per the M.1a design call).- Parameters:
name (Identifier)
metadata_keys (tuple[Identifier, ...])
leg_role (tuple[Identifier, ...])
leg_direction (Literal['Debit', 'Credit', 'Variable'])
origin (str | None)
posted_requirements (tuple[Identifier, ...])
max_pending_age (timedelta | None)
max_unbundled_age (timedelta | None)
aggregating (bool)
bundles_activity (tuple[Identifier, ...])
cadence (str | None)
description (str | None)
metadata_value_examples (tuple[tuple[Identifier, tuple[str, ...]], ...])
amount_typical_range (tuple[Decimal, Decimal] | None)
firings_typical_per_period (FiringsTypicalPerPeriod | None)
- aggregating: bool
- amount_typical_range: tuple[Decimal, Decimal] | None
- bundles_activity: tuple[Identifier, ...]
- cadence: str | None
- description: str | None
- firings_typical_per_period: FiringsTypicalPerPeriod | None
- leg_direction: Literal['Debit', 'Credit', 'Variable']
- leg_role: tuple[Identifier, ...]
- max_pending_age: timedelta | None
- max_unbundled_age: timedelta | None
- metadata_keys: tuple[Identifier, ...]
- metadata_value_examples: tuple[tuple[Identifier, tuple[str, ...]], ...]
- name: Identifier
- origin: str | None
- posted_requirements: tuple[Identifier, ...]
- class recon_gen.common.l2.primitives.TransferTemplate(name, expected_net, transfer_key, completion, leg_rails, leg_rail_xor_groups=(), description=None, firings_typical_per_period=None)[source]
Bases:
objectA multi-leg shared Transfer that bundles many Rail firings.
Per SPEC: every firing of a
leg_railsrail with the sametransfer_keyMetadata values posts to the same shared Transfer. L1 Conservation flags the Transfer if its legs don’t sum toexpected_net; L1 Timeliness flags any leg that posts after the derivedTransfer.Completion.A Rail listed in
leg_railsMUST NOT also fire standalone Transfers — its firings always join the shared Transfer matching thetransfer_keyvalues.leg_rail_xor_groups(AB.3) declares mutually-exclusive subsets ofleg_rails— exactly one member of each inner tuple SHOULD fire per Transfer. Empty default keeps every pre-AB.3 template byte- equivalent; the structural validator (C1a-d) enforces members ⊆ leg_rails, members are Variable-direction, no overlap between groups, ≥2 members per group. Runtime “exactly one fires” check lives in the_xor_group_violationmatview (AB.3.3).- Parameters:
name (Identifier)
expected_net (Decimal)
transfer_key (tuple[Identifier, ...])
completion (str)
leg_rails (tuple[Identifier, ...])
leg_rail_xor_groups (tuple[tuple[Identifier, ...], ...])
description (str | None)
firings_typical_per_period (FiringsTypicalPerPeriod | None)
- completion: str
- description: str | None
- expected_net: Decimal
- firings_typical_per_period: FiringsTypicalPerPeriod | None
- leg_rail_xor_groups: tuple[tuple[Identifier, ...], ...]
- leg_rails: tuple[Identifier, ...]
- name: Identifier
- transfer_key: tuple[Identifier, ...]
- class recon_gen.common.l2.primitives.TwoLegRail(name, metadata_keys, source_role, destination_role, origin=None, source_origin=None, destination_origin=None, expected_net=None, posted_requirements=<factory>, max_pending_age=None, max_unbundled_age=None, aggregating=False, bundles_activity=<factory>, cadence=None, description=None, metadata_value_examples=<factory>, amount_typical_range=None, firings_typical_per_period=None)[source]
Bases:
objectA Rail that produces two Transaction legs (debit + credit) per firing.
When fired as a standalone Transfer,
expected_netMUST be set (typically0); L1 Conservation enforcesΣ legs = expected_net. When the rail is a leg-pattern of a TransferTemplate,expected_netMUST be unset — the template owns the bundle’s ExpectedNet. Per F3 this is a cross-entity validation rule (the validator’s pass 2).Per-leg Origin:
originshorthands “both legs share this Origin”;source_origin/destination_originoverride per leg when the legs differ (e.g., the leg touching an external counterparty isExternalForcePostedwhile the internal counterpart isInternalInitiated). The validator (rule O1) checks every leg resolves to an Origin under the SPEC’s resolution table.PostedRequirements / aging:
posted_requirementsadds Rail-specific fields beyond the auto-derived TransferKey + chain-Required-true parent_transfer_id (seederived.posted_requirements_for);max_pending_age+max_unbundled_ageare aging-watch durations.- Parameters:
name (Identifier)
metadata_keys (tuple[Identifier, ...])
source_role (tuple[Identifier, ...])
destination_role (tuple[Identifier, ...])
origin (str | None)
source_origin (str | None)
destination_origin (str | None)
expected_net (Decimal | None)
posted_requirements (tuple[Identifier, ...])
max_pending_age (timedelta | None)
max_unbundled_age (timedelta | None)
aggregating (bool)
bundles_activity (tuple[Identifier, ...])
cadence (str | None)
description (str | None)
metadata_value_examples (tuple[tuple[Identifier, tuple[str, ...]], ...])
amount_typical_range (tuple[Decimal, Decimal] | None)
firings_typical_per_period (FiringsTypicalPerPeriod | None)
- aggregating: bool
- amount_typical_range: tuple[Decimal, Decimal] | None
- bundles_activity: tuple[Identifier, ...]
- cadence: str | None
- description: str | None
- destination_origin: str | None
- destination_role: tuple[Identifier, ...]
- expected_net: Decimal | None
- firings_typical_per_period: FiringsTypicalPerPeriod | None
- max_pending_age: timedelta | None
- max_unbundled_age: timedelta | None
- metadata_keys: tuple[Identifier, ...]
- metadata_value_examples: tuple[tuple[Identifier, tuple[str, ...]], ...]
- name: Identifier
- origin: str | None
- posted_requirements: tuple[Identifier, ...]
- source_origin: str | None
- source_role: tuple[Identifier, ...]