Skip to content

Commit acff78e

Browse files
authored
fix(cli): warn on stale warehouse reports in portfolio truth mode
Warn during portfolio-truth generation when the latest warehouse audit report is missing or stale, and include the remediation command so dual truth artifacts cannot silently drift.\n\nVerified with:\n- .venv/bin/python -m pytest -q -p no:cacheprovider tests/test_portfolio_truth.py\n- .venv/bin/ruff check src/ tests/
1 parent 70f30d3 commit acff78e

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

src/cli.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5310,6 +5310,46 @@ def _load_security_alerts_by_name(*, output_dir: Path, username: str) -> dict[st
53105310
return {name: entry for name, entry in data.items() if isinstance(entry, dict)}
53115311

53125312

5313+
WAREHOUSE_REPORT_STALE_DAYS = 7
5314+
5315+
5316+
def _warn_if_warehouse_report_stale(output_dir: Path, username: str) -> None:
5317+
"""Warn when the legacy warehouse report is missing or stale (F2).
5318+
5319+
Notion OS's external-signal-sync reads ``audit-report-<username>-*.json`` (the
5320+
3.7 warehouse report), but ``--portfolio-truth`` mode does NOT regenerate it —
5321+
the truth pipeline scans the workspace and never runs the GitHub audit that
5322+
produces warehouse data. Per the F2 "keep both artifacts live" decision, surface
5323+
the gap at generation time so the operator runs ``audit report <username>`` to
5324+
keep Notion's Repo Auditor signal fresh. Complements the cross-system-smoke C2
5325+
check, which catches the same drift at smoke time.
5326+
"""
5327+
from datetime import date
5328+
5329+
reports = sorted(output_dir.glob(f"audit-report-{username}-*.json"))
5330+
if not reports:
5331+
print_warning(
5332+
f"No audit-report-{username}-*.json in {output_dir}: Notion's Repo Auditor "
5333+
f"signal reads that warehouse report and this --portfolio-truth run did not "
5334+
f"create one. Run `audit report {username}` to generate it (F2)."
5335+
)
5336+
return
5337+
match = re.search(r"(\d{4}-\d{2}-\d{2})", reports[-1].name)
5338+
if not match:
5339+
return
5340+
try:
5341+
report_date = date.fromisoformat(match.group(1))
5342+
except ValueError:
5343+
return
5344+
age = (date.today() - report_date).days
5345+
if age > WAREHOUSE_REPORT_STALE_DAYS:
5346+
print_warning(
5347+
f"Newest warehouse report {reports[-1].name} is {age}d old: Notion's Repo "
5348+
f"Auditor signal reads it and is now stale. Run `audit report {username}` to "
5349+
f"refresh the warehouse report (F2 — both artifacts kept live by decision)."
5350+
)
5351+
5352+
53135353
def _run_portfolio_truth_mode(args) -> None:
53145354
from src.portfolio_truth_publish import publish_portfolio_truth
53155355

@@ -5361,6 +5401,7 @@ def _run_portfolio_truth_mode(args) -> None:
53615401
f"(registry {'updated' if result.registry_changed else 'unchanged'}, "
53625402
f"report {'updated' if result.report_changed else 'unchanged'})"
53635403
)
5404+
_warn_if_warehouse_report_stale(output_dir, args.username)
53645405

53655406

53665407
def _run_portfolio_context_recovery_mode(args) -> None:

tests/test_portfolio_truth.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,3 +1387,42 @@ def test_git_default_branch_empty_when_origin_head_unset(tmp_path: Path) -> None
13871387

13881388
# A freshly init'd repo has no origin/HEAD → "" so callers fall back.
13891389
assert _git_default_branch(repo) == ""
1390+
1391+
1392+
# ── F2: warehouse-report staleness reminder ────────────────────────────────
1393+
from src.cli import _warn_if_warehouse_report_stale # noqa: E402
1394+
1395+
1396+
def _write_warehouse_report(d: Path, username: str, date_str: str) -> None:
1397+
(d / f"audit-report-{username}-{date_str}.json").write_text("{}", encoding="utf-8")
1398+
1399+
1400+
class TestWarehouseStalenessReminder:
1401+
"""F2 (keep-dual): --portfolio-truth mode warns when the warehouse report Notion
1402+
reads is missing or stale, so the operator refreshes it."""
1403+
1404+
def test_missing_report_warns(self, tmp_path: Path, capsys) -> None:
1405+
import re
1406+
1407+
_warn_if_warehouse_report_stale(tmp_path, "saagpatel")
1408+
captured = capsys.readouterr()
1409+
# print_warning word-wraps, so normalize whitespace before substring checks
1410+
combined = re.sub(r"\s+", " ", captured.out + captured.err)
1411+
assert "No audit-report-saagpatel" in combined
1412+
assert "audit report saagpatel" in combined
1413+
1414+
def test_stale_report_warns(self, tmp_path: Path, capsys) -> None:
1415+
_write_warehouse_report(tmp_path, "saagpatel", "2020-01-01")
1416+
_warn_if_warehouse_report_stale(tmp_path, "saagpatel")
1417+
captured = capsys.readouterr()
1418+
assert "stale" in (captured.out + captured.err).lower()
1419+
1420+
def test_fresh_report_no_warning(self, tmp_path: Path, capsys) -> None:
1421+
from datetime import date
1422+
1423+
_write_warehouse_report(tmp_path, "saagpatel", date.today().isoformat())
1424+
_warn_if_warehouse_report_stale(tmp_path, "saagpatel")
1425+
captured = capsys.readouterr()
1426+
combined = captured.out + captured.err
1427+
assert "stale" not in combined.lower()
1428+
assert "No audit-report" not in combined

0 commit comments

Comments
 (0)