From 1c105377ceac7e27e638d9e835ffaaec9d52a8fa Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sat, 23 May 2026 22:50:55 +0800 Subject: [PATCH] Tighten monthly release validation --- .github/workflows/monthly_publish.yml | 1 - src/release_contract.py | 5 +++ tests/test_monthly_publish_workflow_config.py | 1 + tests/test_release_contract.py | 35 +++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/.github/workflows/monthly_publish.yml b/.github/workflows/monthly_publish.yml index 1907199..f6bc3bc 100644 --- a/.github/workflows/monthly_publish.yml +++ b/.github/workflows/monthly_publish.yml @@ -106,7 +106,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_REPOSITORY: ${{ github.repository }} - GITHUB_OUTPUT: ${{ github.output }} run: | set -euo pipefail python - <<'PY' diff --git a/src/release_contract.py b/src/release_contract.py index 31910ff..2ddc055 100644 --- a/src/release_contract.py +++ b/src/release_contract.py @@ -436,11 +436,16 @@ def validate_release_outputs( errors, ) ) + selected_row_count = int(selected_mask.sum()) selected_symbols_by_rank = _selected_symbols_ordered_by_rank(latest_ranking, selected_mask, errors) if live_pool_symbols and not set(live_pool_symbols).issubset(set(ranking_symbols)): errors.append("live_pool.json symbols must all be present in latest_ranking.csv") if live_pool_symbols and not set(live_pool_symbols).issubset(selected_symbols): errors.append("live_pool.json symbols must all be selected in latest_ranking.csv") + if live_pool_symbols and selected_row_count != len(live_pool_symbols): + errors.append( + "latest_ranking.csv selected_flag row count must match live_pool.json symbols length" + ) if live_pool_symbols and selected_symbols_by_rank: expected_live_pool_symbols = selected_symbols_by_rank[: len(live_pool_symbols)] if live_pool_symbols != expected_live_pool_symbols: diff --git a/tests/test_monthly_publish_workflow_config.py b/tests/test_monthly_publish_workflow_config.py index 5e46d3d..2d24540 100644 --- a/tests/test_monthly_publish_workflow_config.py +++ b/tests/test_monthly_publish_workflow_config.py @@ -35,6 +35,7 @@ def test_monthly_review_issue_creation_does_not_require_gh_cli(self) -> None: self.assertIn("--shadow-universe-mode", workflow) self.assertIn("https://api.github.com/repos/{repository}", workflow) self.assertIn('GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}', workflow) + self.assertNotIn("GITHUB_OUTPUT: ${{ github.output }}", workflow) self.assertIn("issue_number=", workflow) self.assertIn("SELFHOSTED_CODEX_REVIEW_REPOSITORY", workflow) self.assertIn("QuantStrategyLab/CryptoCodexAuditBridge", workflow) diff --git a/tests/test_release_contract.py b/tests/test_release_contract.py index b19dcbd..1200f17 100644 --- a/tests/test_release_contract.py +++ b/tests/test_release_contract.py @@ -248,6 +248,41 @@ def test_validate_release_outputs_rejects_live_pool_order_mismatch(self) -> None validation["errors"], ) + def test_validate_release_outputs_rejects_extra_selected_ranking_rows(self) -> None: + with tempfile.TemporaryDirectory() as tmp_dir: + root = Path(tmp_dir) + self.build_outputs(root) + output_dir = root / "data" / "output" + ranking_path = output_dir / "latest_ranking.csv" + artifact_manifest_path = output_dir / "artifact_manifest.json" + + ranking = pd.read_csv(ranking_path) + ranking.loc[len(ranking)] = { + "as_of_date": "2026-03-13", + "symbol": "XRPUSDT", + "rule_score": 0.4, + "linear_score": 0.3, + "ml_score": 0.2, + "final_score": 0.4, + "regime": "risk_off", + "confidence": 0.5, + "selected_flag": True, + "current_rank": 6, + } + ranking.to_csv(ranking_path, index=False) + + artifact_manifest = json.loads(artifact_manifest_path.read_text(encoding="utf-8")) + artifact_manifest["artifacts"]["latest_ranking"]["sha256"] = sha256_file(ranking_path) + write_json(artifact_manifest_path, artifact_manifest) + + validation = validate_release_outputs(root / "data" / "output", require_artifact_manifest=True) + + self.assertFalse(validation["ok"]) + self.assertIn( + "latest_ranking.csv selected_flag row count must match live_pool.json symbols length", + validation["errors"], + ) + def test_validate_release_outputs_rejects_stale_outputs_when_required(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: root = Path(tmp_dir)