recon_gen.common.intervals

BC.1 — Typed time-range interval value types.

Four types, two responsibilities:

  • Time-range: DateInterval (closed-closed [start, end] over calendar dates) and DateTimeInterval (half-open [start, end_exclusive) over naive timestamps). One named type per endpoint convention. The business-facing convention pairs with the granularity that uses it most: dates are closed-closed (audit reports say “week of May 17 - May 23” meaning both endpoints), timestamps are half-open (math + SQL col >= start AND col < end).

  • Plant schedule: SingleDayPlant (one day, derived from a window via at_window_end / at_window_start / at_offset_from_end) and MultiDayPlant (spans every day, via spans). Each plant generator declares which schedule it consumes in its type signature; the factory in plant_adapter.py constructs the right shape per generator from the test’s window. No “where in the window do I plant?” convention lives in generator bodies — it lives in the type.

Single-TZ invariant

All datetimes in this codebase are NAIVE (no tzinfo); their LOCAL meaning is the operator’s machine TZ by convention (see project_local_tz_convention in the auto-memory). The system assumes ONE consistent TZ end-to-end. DateTimeInterval.__post_init__ rejects aware datetimes at construction; DateInterval.as_half_open_datetimes() returns naive datetimes. The intervals layer refuses to be the place where TZ policy lives.

Wiring-site enforcement

Two AST lints in tests/unit/test_typing_smells.py:

  • no-naked-interval-ctor: bare DateInterval(...) / etc. calls outside this module must be a .classmethod_name(...) form. The named-convention constructors are the only way wiring sites should mint these.

  • no-raw-temporal-args: function/method parameters annotated date or datetime in src/recon_gen/** are a smell — wrap in one of these types or RunContext (Phase BD). Dataclass field annotations are unaffected (point values of real events, not policy-carrying params). Staged: enabled at end of BC.5 once the migration surface is wrapped.

Per feedback_invariants_in_types: types bring meaning with them, convention hides it.

Classes

DateInterval(start, end)

Closed-closed [start, end] interval over calendar dates.

DateTimeInterval(start, end_exclusive)

Half-open [start, end_exclusive) interval over NAIVE timestamps.

MultiDayPlant(window)

A plant whose effect spans every day of a window.

SingleDayPlant(day)

A plant that lands on exactly one calendar day.

class recon_gen.common.intervals.DateInterval(start, end)[source]

Bases: object

Closed-closed [start, end] interval over calendar dates.

Business-facing convention — audit reports, matview BETWEEN clauses, dashboard date-range filters. For SQL/math half-open shape convert via .as_half_open_datetimes() — the +1-day flip lives inside that method, not at each callsite.

Invariant: start <= end (a one-day interval is start == end).

Parameters:
  • start (date)

  • end (date)

as_half_open_datetimes()[source]

Convert [start, end] (closed dates) to [start 00:00, end+1 00:00) (half-open naive datetimes).

Single-TZ invariant — returned datetimes are NAIVE. The +1-day flip widens “end-of-day on end” into “start-of-day on end+1,” so callers don’t write end + timedelta(days=1) by hand.

Return type:

DateTimeInterval

classmethod closed(start, end)[source]

Explicit closed-closed construction.

Return type:

Self

Parameters:
  • start (date)

  • end (date)

contains(d)[source]

True iff d is in [start, end] (both endpoints inclusive).

Return type:

bool

Parameters:

d (date)

property days: int

Number of days in the interval, both endpoints counted.

single_day(x).days == 1.

end: date
iter_days()[source]

Yield every date from start to end inclusive.

Return type:

Iterable[date]

classmethod single_day(d)[source]

A one-day interval [d, d].

Return type:

Self

Parameters:

d (date)

start: date
classmethod trailing_days_ending_today(today, days)[source]

[today - days + 1, today] — “last N days including today”.

For live-streaming dashboards where today’s partial data counts.

Example: trailing_days_ending_today(2026-05-24, 7)[2026-05-18, 2026-05-24] (7 days, today included).

Return type:

Self

Parameters:
  • today (date)

  • days (int)

classmethod trailing_days_ending_yesterday(today, days)[source]

[today - days, today - 1] — the audit window convention.

N days, ending on the most-recently-closed business day. today itself is excluded (the day isn’t closed yet — you can’t audit a day before it ends).

Example: trailing_days_ending_yesterday(2026-05-24, 7)[2026-05-17, 2026-05-23] (7 days, today excluded).

Return type:

Self

Parameters:
  • today (date)

  • days (int)

class recon_gen.common.intervals.DateTimeInterval(start, end_exclusive)[source]

Bases: object

Half-open [start, end_exclusive) interval over NAIVE timestamps.

The math/SQL convention — col >= start AND col < end_exclusive. Used by stuck_* matviews (posted_at >= now - interval), SQL BETWEEN callers that need “end of day” without hand-rolling +1 day, live-streaming dashboards that slide past midnight.

Closed-closed daily counterpart is DateInterval; convert via DateInterval.as_half_open_datetimes().

Single-TZ invariant: start and end_exclusive MUST be naive (tzinfo is None). Aware datetimes raise ValueError at construction. See module docstring.

Parameters:
  • start (datetime)

  • end_exclusive (datetime)

contains(dt)[source]

True iff dt is in [start, end_exclusive).

Return type:

bool

Parameters:

dt (datetime)

property duration: timedelta

end_exclusive - start.

end_exclusive: datetime
classmethod half_open(start, end_exclusive)[source]

Explicit half-open construction.

Return type:

Self

Parameters:
  • start (datetime)

  • end_exclusive (datetime)

start: datetime
classmethod trailing_duration_ending_now(now, duration)[source]

[now - duration, now) — the stuck_* matview convention.

Right edge is exclusive (the rendering of “in flight as of now” excludes the now-instant itself by convention).

Return type:

Self

Parameters:
  • now (datetime)

  • duration (timedelta)

class recon_gen.common.intervals.MultiDayPlant(window)[source]

Bases: object

A plant whose effect spans every day of a window.

StuckUnbundledGenerator (unbundled-bucket stuck for N days), RailFiringGenerator (a rail fires repeatedly across a span). Generators that consume MultiDayPlant walk iter_days(); they DO NOT pick “the right day” internally.

Parameters:

window (DateInterval)

iter_days()[source]

Yield every date in the underlying window (inclusive both ends).

Return type:

Iterable[date]

classmethod spans(window)[source]

Plant covers every day in window. Named to mirror its policy: the plant SPANS the window, it doesn’t sample within it.

Return type:

Self

Parameters:

window (DateInterval)

window: DateInterval
class recon_gen.common.intervals.SingleDayPlant(day)[source]

Bases: object

A plant that lands on exactly one calendar day.

DriftGenerator, OverdraftGenerator, LimitBreachGenerator — the invariants that fire on one day at a time consume this. The day is a derived value, not a free field: the factory uses one of the named at_* constructors so the call site declares its policy.

Parameters:

day (date)

classmethod at_offset_from_end(window, days_back)[source]

window.end - days_back, validated against window.

For generators that need to plant N days before the window’s tail (e.g. a drift that the audit catches late).

Return type:

Self

Parameters:
classmethod at_window_end(window)[source]

Most-recently-closed day in the window. The default for single-day plants whose existence implies a finished day.

Return type:

Self

Parameters:

window (DateInterval)

classmethod at_window_start(window)[source]

Earliest day in the window.

Return type:

Self

Parameters:

window (DateInterval)

day: date