recon_gen.common.spine.fan_in_disagreement
Fan-in-disagreement family — Invariant + ViolationGenerator.
AX.3 promotion of the AB.4.5 / AB.4.5 family of plants (healthy / missing-parent / extra-parent). The matview <prefix>_fan_in_disagreement checks every child Transfer of every chain whose child declares fan_in=True against the expected parent count and surfaces rows where parent_count != expected:
orphan: expected_parent_count IS NULL (variable-batch) AND parent_count < 2 — a fan_in child should batch ≥2 parents.
missing: expected_parent_count IS NOT NULL AND parent_count < expected.
extra: expected_parent_count IS NOT NULL AND parent_count > expected.
Identity tuple: (child_transfer_id, disagreement_kind).
Single generator (all 3 variants share the emit shape: N parent legs + 1 child Transfer whose legs each carry a contributing parent’s transfer_parent_id; only N differs). Three smart constructors — scenario_for_healthy / scenario_for_missing_parent / scenario_for_extra_parent — pick a fan_in chain from the L2 and set the parent_count knob appropriately. The healthy constructor returns a generator whose emit produces NO matview row (the AP.2 non-violating convention).
The matview reads expected_parent_count from the L2 yaml’s inline fan_in_chains CTE; the generator queries the L2 to get the expected value when constructing the missing/extra variants. When expected is NULL on the L2 chain (variable-batch case), missing falls through to ‘orphan’ and extra is undefined (the generator raises).
Single-edge property: transfers-only emit → no daily_balances rows → no drift trip.
Classes
|
Emit parent_count synthetic parent legs + 1 child Transfer whose legs each carry a contributing parent's transfer_parent_id. |
|
Detector for the AB.4.7 matview. |
- class recon_gen.common.spine.fan_in_disagreement.FanInChainGenerator(chain_parent_name, child_template_name, expected_parent_count, parent_count, anchor_day, expected_kind, prefix='spec_example', account_id_override=None)[source]
Bases:
objectEmit parent_count synthetic parent legs + 1 child Transfer whose legs each carry a contributing parent’s transfer_parent_id. The matview reads COUNT(DISTINCT parent_transfer_id) for the child and compares to the L2’s declared expected_parent_count to derive the disagreement_kind row (or no row, in the healthy case).
Account fields are synthetic — the matview doesn’t filter on them. The parent_count knob differentiates the 3 variants: healthy (== expected), missing (< expected), extra (> expected).
Single-edge: transfers-only → no balance rows → no drift trip.
AY.4.c.2 — account_id_override allows the plant adapter (AY.4.c.3) to thread OLD plant account_ids through, preventing PK collisions when N plants of the same shape compose.
- Parameters:
chain_parent_name (str)
child_template_name (str)
expected_parent_count (int | None)
parent_count (int)
anchor_day (date)
expected_kind (str)
prefix (str)
account_id_override (str | None)
- property account_id: str
Derivation keys off
expected_kind+child_template_name(variant kind matters here — healthy/missing/orphan/extra each get a distinct account so a compose of multiple variants on the same chain doesn’t PK-collide).account_id_overridewins when set (AY.4.c.2).
- account_id_override: str | None = None
- anchor_day: date
- chain_parent_name: str
- child_template_name: str
- property child_transfer_id: str
- property claimed_accounts: frozenset[str]
- emit(conn, *, scenario_id=None)[source]
- Return type:
None- Parameters:
conn (SyncConnection)
scenario_id (str | None)
- expected_kind: str
- expected_parent_count: int | None
- property intended: Violation
The evidence this plant produces:
For ‘missing’ / ‘orphan’ / ‘extra’ variants — a RuleViolation (the matview surfaces the disagreement row).
For ‘healthy’ (parent_count == expected) — a CoverageObservation (the AY.2.b layering: the plant emits a chain firing that doesn’t trip the fan_in matview but DOES populate the chain’s parent_count seed coverage). Pre-AY.2.b this was None; AY.2.b promotes it to a typed CoverageObservation so the seed’s “I planted a healthy fan-in chain” claim has somewhere to land.
- parent_count: int
- prefix: str = 'spec_example'
- class recon_gen.common.spine.fan_in_disagreement.FanInDisagreementInvariant(prefix='spec_example')[source]
Bases:
objectDetector for the AB.4.7 matview.
Identity tuple: (child_transfer_id, disagreement_kind). Other columns (chain_parent_name, child_template_name, parent_count, expected_parent_count, business_day) are diagnostic.
- Parameters:
prefix (str)
- name: ClassVar[str] = 'fan_in_disagreement'
- prefix: str = 'spec_example'
- scenario_for_extra_parent(*, anchor_day=datetime.date(2030, 1, 1), instance=None)[source]
Extra-parent shape: parent_count = expected + 1. Only defined when expected is set on the L2 chain — the matview’s ‘extra’ branch gates on expected IS NOT NULL.
Raises ValueError if the picked chain has no expected_parent_count (the variable-batch case has no upper bound to exceed).
- Return type:
- Parameters:
anchor_day (date)
instance (L2Instance | None)
- scenario_for_healthy(*, anchor_day=datetime.date(2030, 1, 1), instance=None)[source]
Healthy non-violating shape per AP.2: parent_count == expected (when set; defaults to 2 for variable-batch). Emit produces NO matview row — the AP.2 convention’s positive-control for the test suite.
- Return type:
- Parameters:
anchor_day (date)
instance (L2Instance | None)
- scenario_for_missing_parent(*, anchor_day=datetime.date(2030, 1, 1), instance=None)[source]
Missing-parent shape: parent_count = expected - 1 (when expected is set) OR parent_count = 1 (orphan, when expected is unset on the L2 chain — variable-batch case where ≥2 is the implicit floor).
Disagreement_kind on the matview row: ‘missing’ when expected is set; ‘orphan’ when expected is unset and parent_count < 2.
- Return type:
- Parameters:
anchor_day (date)
instance (L2Instance | None)