recon_gen.common.spine.stuck_unbundled

Stuck-Unbundled family — twin of stuck_pending with disjoint conditions.

Same shape as stuck_pending:

  • Transaction-based plant (single tx, no balance row)

  • L2-coupled via rail.max_unbundled_age (vs max_pending_age)

  • Wall-clock matview using CURRENT_TIMESTAMP - posting

  • Single-edge registry entry (no overlap with drift / ledger_drift / overdraft / expected_eod — Posted but bundle_id IS NULL is exactly the matview’s filter, and Posted legs don’t trip stuck_pending)

The disjoint conditions vs stuck_pending (per schema.py:2091-2094):

  • status=’Posted’ (vs ‘Pending’) — AggregatingRails only bundle Posted legs; a Pending leg isn’t “stuck unbundled,” it’s just “stuck pending”

  • bundle_id IS NULL — the row has been Posted but no AggregatingRail has picked it up

  • Per validator R8, max_unbundled_age is only meaningful on rails whose rail_name appears in some AggregatingRail’s bundles_activity

  • posting + max_unbundled_age overshot

Same TZ convention as stuck_pending: datetime.now() LOCAL per [[project-local-tz-convention]]; SQLite test absorbs UTC skew via ±12h overshoot windows.

Classes

StuckUnbundledGenerator(transaction_id, ...)

Emit a single Posted transaction with bundle_id IS NULL whose posting is in the past of as_of by max_unbundled_age_seconds + overshoot_seconds.

StuckUnbundledInvariant([prefix])

Stuck-Unbundled detector.

class recon_gen.common.spine.stuck_unbundled.StuckUnbundledGenerator(transaction_id, transfer_id, rail_name, account_id, account_role, account_parent_role, max_unbundled_age_seconds, overshoot_seconds, as_of, prefix='spec_example')[source]

Bases: object

Emit a single Posted transaction with bundle_id IS NULL whose posting is in the past of as_of by max_unbundled_age_seconds + overshoot_seconds. NO balance row, NO related rows.

Post-AW.5: as_of is the owned temporal frame; matview reads the same value from <prefix>_config.as_of → tests deterministic, no TZ skew.

Parameters:
  • transaction_id (str)

  • transfer_id (str)

  • rail_name (str)

  • account_id (str)

  • account_role (str)

  • account_parent_role (str | None)

  • max_unbundled_age_seconds (int)

  • overshoot_seconds (int)

  • as_of (datetime)

  • prefix (str)

account_id: str
account_parent_role: str | None
account_role: str
as_of: datetime
property claimed_accounts: frozenset[str]

The single account_id this plant strands as Posted-unbundled. AV.5.

emit(conn, *, scenario_id=None)[source]
Return type:

None

Parameters:
  • conn (SyncConnection)

  • scenario_id (str | None)

property intended: RuleViolation
max_unbundled_age_seconds: int
overshoot_seconds: int
prefix: str = 'spec_example'
rail_name: str
transaction_id: str
transfer_id: str
class recon_gen.common.spine.stuck_unbundled.StuckUnbundledInvariant(prefix='spec_example')[source]

Bases: object

Stuck-Unbundled detector. The matview gates on status = 'Posted' AND bundle_id IS NULL AND age_seconds > max_unbundled_age_seconds (per-rail cap from L2). Identity is (transaction_id, rail_name).

Parameters:

prefix (str)

detect(conn)[source]
Return type:

set[Violation]

Parameters:

conn (SyncConnection)

name: ClassVar[str] = 'stuck_unbundled'
prefix: str = 'spec_example'
scenario_for(rail_name, *, as_of, overshoot_seconds=60, account_role='CustomerSubledger', instance=None, account_id=None)[source]

Resolve rail_name against the shape; plant a Posted-but- unbundled transaction with posting = as_of − (rail. max_unbundled_age + overshoot).

as_of is the owned temporal frame the matview reads from <prefix>_config.as_of (per AW.2). See StuckPendingInvariant .scenario_for for the full contract — same shape applies here.

Raises ValueError if rail doesn’t exist OR doesn’t have a max_unbundled_age (matview excludes those — uncovered scenario would silently inert).

AY.4.c — account_id overrides the default synthetic ID. The plant adapter (AY.4.c.3) threads OLD StuckUnbundledPlant.account_id through this kwarg so N plants on the same rail produce N distinct generators (the default f”acct-stuck-unbundled-{rail_name}” derivation would collide). Existing test callers can pass nothing → preserves the synthetic default byte-stable.

Return type:

StuckUnbundledGenerator

Parameters:
  • rail_name (str)

  • as_of (datetime)

  • overshoot_seconds (int)

  • account_role (str)

  • instance (L2Instance | None)

  • account_id (str | None)