fix(regime): reseed stored history on a sensor change, and stop faking an observation

Two review findings on 46ace50.

[P1] Raising HY_OAS_WINDOW_DAYS to 700 only reached newly computed rows. A
routine run recomputes the latest trading date alone, and `rebuilding` was keyed
on "no v3 snapshot exists at all", which is false once the cutover has run --
so every row already written kept the credit gap the wider window exists to
close, indefinitely.

Adds SENSOR_REVISION: stamped into each snapshot, absent on pre-marker rows
(read as 1), and a stored revision below the current one triggers exactly one
reseed. Deliberately not METHODOLOGY, which would partition the history API and
discard the cached event study -- neither warranted, since the study recomputes
its Warning series from source rather than reading snapshots and so cannot be
staled by a reseed.

The reseed is bounded by REBUILD_LOOKBACK_DAYS in calendar days rather than a
session count, because the binding constraint is the OAS fetch: each replayed
row needs W3's 20-business-day lookback inside HY_OAS_WINDOW_DAYS. Replaying by
session count would have left the oldest stored rows unrepaired -- the exact
rows the fix targets. At 672 days the replay covers ~464 sessions, W3's oldest
requirement lands on the first fetched OAS day, and the ~400-session series the
cutover wrote is fully covered. A test asserts that relationship so the two
constants cannot drift back into recreating the gap.

[P2] With nothing ever collected, current_observation returned available=true
and the default placeholders -- "unknown" for every hyperscaler, "mixed" for the
reaction -- so the card announced a reading that never happened. Those are the
absence of an observation, not an observation of absence. Gated on `observed`
(non-null fetched_at, the one field every path writing real content stamps),
which blanks the content and drives a proper empty state naming where an admin
collects one. This was a regression from 46ace50; fundamental_overlay never had
it, since no observation means no effective date means pending.

Also renames the leftover v2 identifiers in the touched paths
(rewrite_existing_v2, latest_v2).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 18:22:25 +02:00
co-authored by Claude Opus 5
parent 7dc804be2b
commit 3483797e75
5 changed files with 233 additions and 25 deletions
+121 -5
View File
@@ -279,6 +279,37 @@ def test_live_observation_is_visible_before_its_effective_date():
assert current_observation(overrides, config, date(2026, 8, 22))["available"] is False
def test_an_uncollected_observation_is_not_reported_as_collected():
"""The default override is placeholders, not a reading.
``capex`` defaults to "unknown" for every hyperscaler and the reaction to
"mixed". Surfacing those as an observation made the card claim a read that
never happened.
"""
names = DEFAULT_CONFIG["tickers"]["hyperscalers"]
nothing_collected = {
"f1_score": None,
"f3_score": None,
"capex": {name: "unknown" for name in names},
"good_news_stock_down": "mixed",
"reasoning": None,
"fetched_at": None,
"effective_date": None,
"source": "default",
}
blank = current_observation(nothing_collected, DEFAULT_CONFIG, date(2026, 8, 7))
assert blank["observed"] is False
assert blank["available"] is False
assert blank["capex"] is None
assert blank["good_news_stock_down"] is None
assert blank["reasoning"] is None
# One real observation flips it, placeholders and all.
collected = {**nothing_collected, "fetched_at": "2026-08-07T10:00:00+00:00", "source": "gemini"}
assert current_observation(collected, DEFAULT_CONFIG, date(2026, 8, 7))["observed"] is True
def test_fundamentals_do_not_move_the_warning_score():
"""The v3 complaint: a maxed-out LLM read must not silently do nothing.
@@ -462,11 +493,11 @@ async def test_prior_snapshot_is_immutable_without_explicit_rebuild(db_session):
changed["state"] = {"score": 90.0, "band": "breaking"}
written, _ = await rms._upsert_snapshot(
db_session, first, rewrite_existing_v2=True
db_session, first, rewrite_existing=True
)
await db_session.flush()
rewritten, persisted = await rms._upsert_snapshot(
db_session, changed, rewrite_existing_v2=False
db_session, changed, rewrite_existing=False
)
row = (
await db_session.execute(
@@ -509,10 +540,10 @@ async def test_routine_can_refresh_latest_trading_session_after_civil_day_rolls(
return {}, {}
async def fake_latest(_db):
return object(), {"methodology": "v3"}
return object(), {"methodology": "v3", "sensor_revision": rms.SENSOR_REVISION}
async def fake_upsert(_db, result, *, rewrite_existing_v2):
rewrites.append(rewrite_existing_v2)
async def fake_upsert(_db, result, *, rewrite_existing):
rewrites.append(rewrite_existing)
return True, result
class FakeDB:
@@ -533,6 +564,91 @@ async def test_routine_can_refresh_latest_trading_session_after_civil_day_rolls(
assert rewrites == [True]
@pytest.mark.asyncio
@pytest.mark.parametrize(
("stored", "expect_reseed"),
[
({"methodology": "v3"}, True), # written before the marker existed
({"methodology": "v3", "sensor_revision": 1}, True),
({"methodology": "v3", "sensor_revision": rms.SENSOR_REVISION}, False),
],
)
async def test_a_stale_sensor_revision_reseeds_stored_history(
monkeypatch, stored, expect_reseed
):
"""Widening the OAS window has to reach rows that are already stored.
Routine runs recompute only the latest date, so without this trigger every
older row would keep the credit gap the wider window exists to close.
"""
sessions = [date.today() - timedelta(days=offset) for offset in reversed(range(10))]
prices = {symbol: [(day, 100.0) for day in sessions] for symbol in ("SMH", "QQQ", "SPY")}
written: list[date] = []
revisions: list[int] = []
async def fake_config(_db):
return copy.deepcopy(DEFAULT_CONFIG)
async def fake_overrides(_db):
return {"locked": True, "fetched_at": None, "effective_date": None}
async def fake_prices(_config, _start, _end):
return prices
async def fake_fred(_series_id, _start, _end):
return None
async def fake_breadth(_db, _symbols, window, min_tickers):
return {}, {}
async def fake_latest(_db):
return object(), stored
async def fake_upsert(_db, result, *, rewrite_existing):
written.append(date.fromisoformat(result["date"]))
revisions.append(result["sensor_revision"])
# Every replayed row must be rewritable, or a reseed writes one row.
assert rewrite_existing is True
return True, result
class FakeDB:
async def commit(self):
return None
for name, value in (
("get_regime_config", fake_config),
("get_fundamental_overrides", fake_overrides),
("_fetch_prices", fake_prices),
("_fetch_fred_series", fake_fred),
("_latest_snapshot_row", fake_latest),
("_upsert_snapshot", fake_upsert),
):
monkeypatch.setattr(rms, name, value)
monkeypatch.setattr(rms.breadth_service, "compute_breadth_details", fake_breadth)
await rms.update_regime_monitor(FakeDB())
if expect_reseed:
assert written == sessions, "a reseed must replay the whole stored span"
else:
assert written == [sessions[-1]], "a current revision must not reseed"
assert set(revisions) == {rms.SENSOR_REVISION}
def test_the_rebuild_span_stays_inside_the_oas_window():
"""The reseed must not replay rows it cannot compute credit for.
Each replayed row needs W3's lookback inside the fetched OAS window; if the
replay reached further back than the fetch, the reseed would recreate the
very gap it exists to close.
"""
replay_calendar_days = rms.REBUILD_LOOKBACK_DAYS
w3_lookback_calendar = rms.W3_OAS_LOOKBACK * 7 / 5 # business days -> calendar
assert replay_calendar_days + w3_lookback_calendar <= rms.HY_OAS_WINDOW_DAYS
# ...and still covers the 400-session series the v3 cutover wrote.
assert replay_calendar_days >= 400 * 365 / 252
@pytest.mark.asyncio
async def test_manual_llm_refresh_recomputes_latest_regime_snapshot(monkeypatch):
calls: list[str] = []