"""Resolver — the SEPARATE forward pass that closes the loop (§6.2, §6.3). ARCHITECTURALLY ISOLATED from the scorers: it has no shared write path with them. Scorers write candidate_scores + ledger rows with outcome columns NULL and a FROZEN discourse_metric. The resolver runs later (larger as_of), reads ledger rows whose date_logged < as_of_now, and writes ONLY resolution_date / discourse_outcome / external_outcome / lead_time_days. It is FORBIDDEN from touching discourse_metric — that is the structural reason the ledger can't reward noticing what already happened. Implementation note: real resolutions need forward time (the clock can't be backfilled). For the backtest, the discourse leg can be resolved by re-running the discourse metric forward from date_logged; the external leg (price/filings/human check, §6.5) is filled as that evidence arrives. Stubbed now to lock the architecture; filled out for the forward pilot. """ from __future__ import annotations def resolve_discourse_leg(conn, sc, cfg, *, as_of_now: str) -> int: """For each ledger row logged before as_of_now without a resolution, re-measure discourse forward and set discourse_outcome + lead_time. (Forward-only; never reads/edits discourse_metric.) Returns count resolved. STUB — implemented for the forward pilot.""" rows = conn.execute( "SELECT signal_id, date_logged FROM ledger WHERE resolution_date IS NULL AND date_logged < ?", (as_of_now,), ).fetchall() # TODO(forward-pilot): re-run windowed independence from date_logged→as_of_now for each row's # origin derivative; set discourse_outcome in {up_cross_cluster,up_single_cluster,flat,down}. return 0