From 2815d2e9bb185a55ee8a77fd26c2743fb85c53cc Mon Sep 17 00:00:00 2001 From: saagpatel Date: Sun, 7 Jun 2026 12:45:30 -0700 Subject: [PATCH] fix(cli): warn when the warehouse report is stale in --portfolio-truth mode (F2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the F2 decision (keep both truth artifacts live), surface the dual-artifact liveness gap at generation time. `audit report --portfolio-truth` dispatches to _run_portfolio_truth_mode, which scans the workspace and publishes portfolio-truth-latest.json but never regenerates the legacy audit-report--*.json warehouse file — and it can't cheaply, since the truth pipeline doesn't run the GitHub audit that produces warehouse data. Notion OS's external-signal-sync reads that warehouse file, so it silently goes stale (live: 06-03 vs truth 06-07). Add _warn_if_warehouse_report_stale: after publishing truth, warn (with the exact remediation command) when the newest warehouse report is missing or older than WAREHOUSE_REPORT_STALE_DAYS (7). Self-announcing every truth run, so the gap can't rot unnoticed; complements the cross-system-smoke C2 check that catches it at smoke time. Tests cover missing / stale / fresh. 46 portfolio-truth tests pass, ruff clean, no new mypy errors. --- src/cli.py | 41 +++++++++++++++++++++++++++++++++++ tests/test_portfolio_truth.py | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/cli.py b/src/cli.py index 78a8357..cc845f4 100644 --- a/src/cli.py +++ b/src/cli.py @@ -5310,6 +5310,46 @@ def _load_security_alerts_by_name(*, output_dir: Path, username: str) -> dict[st return {name: entry for name, entry in data.items() if isinstance(entry, dict)} +WAREHOUSE_REPORT_STALE_DAYS = 7 + + +def _warn_if_warehouse_report_stale(output_dir: Path, username: str) -> None: + """Warn when the legacy warehouse report is missing or stale (F2). + + Notion OS's external-signal-sync reads ``audit-report--*.json`` (the + 3.7 warehouse report), but ``--portfolio-truth`` mode does NOT regenerate it — + the truth pipeline scans the workspace and never runs the GitHub audit that + produces warehouse data. Per the F2 "keep both artifacts live" decision, surface + the gap at generation time so the operator runs ``audit report `` to + keep Notion's Repo Auditor signal fresh. Complements the cross-system-smoke C2 + check, which catches the same drift at smoke time. + """ + from datetime import date + + reports = sorted(output_dir.glob(f"audit-report-{username}-*.json")) + if not reports: + print_warning( + f"No audit-report-{username}-*.json in {output_dir}: Notion's Repo Auditor " + f"signal reads that warehouse report and this --portfolio-truth run did not " + f"create one. Run `audit report {username}` to generate it (F2)." + ) + return + match = re.search(r"(\d{4}-\d{2}-\d{2})", reports[-1].name) + if not match: + return + try: + report_date = date.fromisoformat(match.group(1)) + except ValueError: + return + age = (date.today() - report_date).days + if age > WAREHOUSE_REPORT_STALE_DAYS: + print_warning( + f"Newest warehouse report {reports[-1].name} is {age}d old: Notion's Repo " + f"Auditor signal reads it and is now stale. Run `audit report {username}` to " + f"refresh the warehouse report (F2 — both artifacts kept live by decision)." + ) + + def _run_portfolio_truth_mode(args) -> None: from src.portfolio_truth_publish import publish_portfolio_truth @@ -5361,6 +5401,7 @@ def _run_portfolio_truth_mode(args) -> None: f"(registry {'updated' if result.registry_changed else 'unchanged'}, " f"report {'updated' if result.report_changed else 'unchanged'})" ) + _warn_if_warehouse_report_stale(output_dir, args.username) def _run_portfolio_context_recovery_mode(args) -> None: diff --git a/tests/test_portfolio_truth.py b/tests/test_portfolio_truth.py index a0ee94c..407dabc 100644 --- a/tests/test_portfolio_truth.py +++ b/tests/test_portfolio_truth.py @@ -1387,3 +1387,42 @@ def test_git_default_branch_empty_when_origin_head_unset(tmp_path: Path) -> None # A freshly init'd repo has no origin/HEAD → "" so callers fall back. assert _git_default_branch(repo) == "" + + +# ── F2: warehouse-report staleness reminder ──────────────────────────────── +from src.cli import _warn_if_warehouse_report_stale # noqa: E402 + + +def _write_warehouse_report(d: Path, username: str, date_str: str) -> None: + (d / f"audit-report-{username}-{date_str}.json").write_text("{}", encoding="utf-8") + + +class TestWarehouseStalenessReminder: + """F2 (keep-dual): --portfolio-truth mode warns when the warehouse report Notion + reads is missing or stale, so the operator refreshes it.""" + + def test_missing_report_warns(self, tmp_path: Path, capsys) -> None: + import re + + _warn_if_warehouse_report_stale(tmp_path, "saagpatel") + captured = capsys.readouterr() + # print_warning word-wraps, so normalize whitespace before substring checks + combined = re.sub(r"\s+", " ", captured.out + captured.err) + assert "No audit-report-saagpatel" in combined + assert "audit report saagpatel" in combined + + def test_stale_report_warns(self, tmp_path: Path, capsys) -> None: + _write_warehouse_report(tmp_path, "saagpatel", "2020-01-01") + _warn_if_warehouse_report_stale(tmp_path, "saagpatel") + captured = capsys.readouterr() + assert "stale" in (captured.out + captured.err).lower() + + def test_fresh_report_no_warning(self, tmp_path: Path, capsys) -> None: + from datetime import date + + _write_warehouse_report(tmp_path, "saagpatel", date.today().isoformat()) + _warn_if_warehouse_report_stale(tmp_path, "saagpatel") + captured = capsys.readouterr() + combined = captured.out + captured.err + assert "stale" not in combined.lower() + assert "No audit-report" not in combined