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

FanInChainGenerator(chain_parent_name, ...)

Emit parent_count synthetic parent legs + 1 child Transfer whose legs each carry a contributing parent's transfer_parent_id.

FanInDisagreementInvariant([prefix])

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: object

Emit 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_override wins 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: object

Detector 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)

detect(conn)[source]
Return type:

set[Violation]

Parameters:

conn (SyncConnection)

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:

FanInChainGenerator

Parameters:
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:

FanInChainGenerator

Parameters:
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:

FanInChainGenerator

Parameters: