Skip to content

Commit 57963d5

Browse files
committed
chore(shadow): log incident before SystemExit(2) on missing asset
Self-audit weak-point closure — found by reviewing the runner for audit-trail completeness after the Codex P1 fixes. scripts/run_cross_asset_kuramoto_shadow.py::_target_run_date: Previous behaviour on missing-asset CAKInvariantError was a bare SystemExit(2), leaving the operator with only the process exit code to debug. Now appends a row to operational_incidents.csv with incident_type='missing_asset', severity=CRITICAL, description containing the asset name and data_dir, then raises SystemExit(2) from the original exception. Closes the audit gap that the existing hash-mismatch and invariant-violation paths already covered. tests/ops/test_codex_p1_regressions.py: - New test_missing_asset_logs_incident_before_exit pins the incident-before-exit behaviour (monkeypatched INCIDENTS path to tmp_path so real evidence rail is untouched). - Lifted two nested imports (pandas, datetime) to module top. - 6/6 tests pass locally; mypy --strict + ruff + black all clean. SOURCE_HASHES.json regenerated (runner .py byte-change); CI detect-secrets.baseline updated via 'detect-secrets scan' and verified via local invocation of the CI hook command (exit 0). 84 passed + 1 xfail across all cross-asset Kuramoto test suites. No signal logic touched. No frozen parameter modified. No evidence CSV edited. combo_v1 closure enforcement intact.
1 parent 178a185 commit 57963d5

4 files changed

Lines changed: 55 additions & 12 deletions

File tree

.github/detect-secrets.baseline

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6385,7 +6385,7 @@
63856385
{
63866386
"type": "Hex High Entropy String",
63876387
"filename": "results/cross_asset_kuramoto/offline_robustness/SOURCE_HASHES.json",
6388-
"hashed_secret": "738f0d6374500c97122e2788e5087509905a11e1",
6388+
"hashed_secret": "e7a923151affed41b0bc44e873fd946e4809b880",
63896389
"is_verified": false,
63906390
"line_number": 26
63916391
},
@@ -7257,5 +7257,5 @@
72577257
}
72587258
]
72597259
},
7260-
"generated_at": "2026-04-22T05:19:07Z"
7260+
"generated_at": "2026-04-22T06:08:57Z"
72617261
}

results/cross_asset_kuramoto/offline_robustness/SOURCE_HASHES.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@
2323
"core/cross_asset_kuramoto/engine.py": "2f1dc1c976e7c8f3a2e57c9e521541083fa568a0dc7b63e5aa40395ef9d8c59d",
2424
"core/cross_asset_kuramoto/invariants.py": "f5627c2ed1d25bab11f816c00f3af74bd23380725b1c01344f3b250e016e035e",
2525
"scripts/demo_cross_asset_kuramoto.py": "36041afa804e5ad46189eaa9166e064a0714aa6f47a6a16e4556054bc03deb79",
26-
"scripts/run_cross_asset_kuramoto_shadow.py": "2ca05d80215282eba0a9cb1ee371262775ca024286b3576d0cadc53a6ce37b82",
26+
"scripts/run_cross_asset_kuramoto_shadow.py": "1de3f495c7e657991ae63ad33a8342a6cbdbf814f4fa7fd8d2740f5aeb24b910",
2727
"scripts/evaluate_cross_asset_kuramoto_shadow.py": "35f8801a37df3280d727a1adf74ba03c386c3402024de4d2db146285c3da8fe6",
2828
"scripts/render_cross_asset_kuramoto_shadow_report.py": "b12b35a6989d61e7dbf1dadd08247f16fb0ab2a07659683eacf9553cf0425dbf",
2929
"scripts/push_shadow_evidence.sh": "33b91955c0ec61bd274e34d309421079b06dccd3026aa3109aaa8632614b442d",
3030
"ops/systemd/cross_asset_kuramoto_shadow.service": "673905e2206bacce78707a669d86f29b4a2f73eeb87a5fdfe820ae5460d54a44",
3131
"ops/systemd/cross_asset_kuramoto_shadow.timer": "b87272d9adb3ddd967d1b92e07301168d71a1fb1787ce396656103a197553015"
3232
},
33-
"regenerated_utc": "2026-04-22T05:00:14Z"
33+
"regenerated_utc": "2026-04-22T06:08:01Z"
3434
}

scripts/run_cross_asset_kuramoto_shadow.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,34 @@ def _fail_closed(run_dir: Path, msg: str, code: int) -> None:
137137

138138

139139
def _target_run_date(data_dir: Path, assets: list[str]) -> pd.Timestamp:
140-
"""Latest common business-day timestamp across the regime universe."""
140+
"""Latest common business-day timestamp across the regime universe.
141+
142+
On a missing asset the exit code is 2 and an operational incident
143+
row is appended — so the failure is auditable from the ledger
144+
rather than only the process-exit-status.
145+
"""
141146
from core.cross_asset_kuramoto.signal import load_asset_close
142147

143148
last_ts: list[pd.Timestamp] = []
144149
for a in assets:
145150
try:
146151
s = load_asset_close(a, data_dir)
147-
except CAKInvariantError:
148-
raise SystemExit(2) # missing asset
152+
except CAKInvariantError as exc:
153+
_append_incident(
154+
{
155+
"incident_ts": _now_utc(),
156+
"incident_type": "missing_asset",
157+
"severity": "CRITICAL",
158+
"affected_run_date": "",
159+
"description": (
160+
f"load_asset_close failed for asset {a!r} at data_dir={data_dir}: {exc}"
161+
),
162+
"resolved_yes_no": "no",
163+
"resolution_ts": "",
164+
"changed_artifacts_yes_no": "no",
165+
}
166+
)
167+
raise SystemExit(2) from exc
149168
last_ts.append(s.index.max())
150169
return min(last_ts).normalize()
151170

tests/ops/test_codex_p1_regressions.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import importlib.util
2222
import subprocess
2323
import sys
24+
from datetime import datetime, timezone
2425
from pathlib import Path
2526
from types import ModuleType
2627

@@ -120,14 +121,10 @@ def test_runner_quarantines_partial_daily_dir(
120121
# The function under test lives *between* `_already_written` and the
121122
# final `mkdir(exist_ok=False)`. We replicate its contract in-place
122123
# without running the full pipeline (which needs spike data).
123-
import pandas as _pd
124-
125-
run_date = _pd.Timestamp(partial_day)
124+
run_date = pd.Timestamp(partial_day)
126125
assert not runner._already_written(run_date)
127126

128127
# Simulate the logic path the runner takes on retry:
129-
from datetime import datetime, timezone
130-
131128
ts_suffix = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
132129
quarantine = partial_dir.with_name(f"{partial_dir.name}.incomplete.{ts_suffix}")
133130
partial_dir.rename(quarantine)
@@ -142,6 +139,33 @@ def test_runner_quarantines_partial_daily_dir(
142139
assert list(partial_dir.iterdir()) == []
143140

144141

142+
def test_missing_asset_logs_incident_before_exit(
143+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
144+
) -> None:
145+
"""Audit gap closed: a missing regime-panel asset must leave a
146+
row in operational_incidents.csv before the runner exits 2.
147+
Without this, the operator has only the exit status to debug."""
148+
runner = _load_module(RUNNER_SCRIPT, "shadow_runner_missing_asset")
149+
150+
incidents = tmp_path / "operational_incidents.csv"
151+
monkeypatch.setattr(runner, "INCIDENTS", incidents)
152+
153+
missing_data_dir = tmp_path / "definitely_not_a_data_bundle"
154+
with pytest.raises(SystemExit) as exc_info:
155+
runner._target_run_date(missing_data_dir, ["BTC"])
156+
157+
assert exc_info.value.code == 2
158+
assert incidents.is_file(), "incident ledger must exist after missing-asset exit"
159+
rows = pd.read_csv(incidents)
160+
assert len(rows) >= 1
161+
# Find our row (may coexist with earlier test pollution)
162+
ours = rows[rows["incident_type"] == "missing_asset"]
163+
assert len(ours) >= 1
164+
latest = ours.iloc[-1]
165+
assert latest["severity"] == "CRITICAL"
166+
assert "BTC" in str(latest["description"])
167+
168+
145169
def test_runner_retry_logic_matches_source_flow() -> None:
146170
"""Meta-regression: the runner source actually contains the
147171
partial-dir-retry branch. Catches accidental revert."""

0 commit comments

Comments
 (0)